-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1f66278
Showing
68 changed files
with
13,938 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.react-router | ||
build | ||
node_modules | ||
README.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.env | ||
!.env.example | ||
.DS_Store | ||
.react-router | ||
build | ||
node_modules | ||
uploads | ||
*.db | ||
*.tsbuildinfo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
FROM node:20-alpine AS development-dependencies-env | ||
COPY . /app | ||
WORKDIR /app | ||
RUN npm ci | ||
|
||
FROM node:20-alpine AS production-dependencies-env | ||
COPY ./package.json package-lock.json /app/ | ||
WORKDIR /app | ||
RUN npm ci --omit=dev | ||
|
||
FROM node:20-alpine AS build-env | ||
COPY . /app/ | ||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules | ||
WORKDIR /app | ||
RUN npm run build | ||
|
||
FROM node:20-alpine | ||
COPY ./package.json package-lock.json /app/ | ||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules | ||
COPY --from=build-env /app/build /app/build | ||
WORKDIR /app | ||
CMD ["npm", "run", "start"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
FROM oven/bun:1 AS dependencies-env | ||
COPY . /app | ||
|
||
FROM dependencies-env AS development-dependencies-env | ||
COPY ./package.json bun.lockb /app/ | ||
WORKDIR /app | ||
RUN bun i --frozen-lockfile | ||
|
||
FROM dependencies-env AS production-dependencies-env | ||
COPY ./package.json bun.lockb /app/ | ||
WORKDIR /app | ||
RUN bun i --production | ||
|
||
FROM dependencies-env AS build-env | ||
COPY ./package.json bun.lockb /app/ | ||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules | ||
WORKDIR /app | ||
RUN bun run build | ||
|
||
FROM dependencies-env | ||
COPY ./package.json bun.lockb /app/ | ||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules | ||
COPY --from=build-env /app/build /app/build | ||
WORKDIR /app | ||
CMD ["bun", "run", "start"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
FROM node:20-alpine AS dependencies-env | ||
RUN npm i -g pnpm | ||
COPY . /app | ||
|
||
FROM dependencies-env AS development-dependencies-env | ||
COPY ./package.json pnpm-lock.yaml /app/ | ||
WORKDIR /app | ||
RUN pnpm i --frozen-lockfile | ||
|
||
FROM dependencies-env AS production-dependencies-env | ||
COPY ./package.json pnpm-lock.yaml /app/ | ||
WORKDIR /app | ||
RUN pnpm i --prod --frozen-lockfile | ||
|
||
FROM dependencies-env AS build-env | ||
COPY ./package.json pnpm-lock.yaml /app/ | ||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules | ||
WORKDIR /app | ||
RUN pnpm build | ||
|
||
FROM dependencies-env | ||
COPY ./package.json pnpm-lock.yaml /app/ | ||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules | ||
COPY --from=build-env /app/build /app/build | ||
WORKDIR /app | ||
CMD ["pnpm", "start"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Welcome to React Router! | ||
|
||
A modern, production-ready template for building full-stack React applications using React Router. | ||
|
||
## Features | ||
|
||
- 🚀 Server-side rendering | ||
- ⚡️ Hot Module Replacement (HMR) | ||
- 📦 Asset bundling and optimization | ||
- 🔄 Data loading and mutations | ||
- 🔒 TypeScript by default | ||
- 🎉 TailwindCSS for styling | ||
- 📖 [React Router docs](https://reactrouter.com/) | ||
|
||
## Getting Started | ||
|
||
### Installation | ||
|
||
Install the dependencies: | ||
|
||
```bash | ||
npm install | ||
``` | ||
|
||
### Development | ||
|
||
Start the development server with HMR: | ||
|
||
```bash | ||
npm run dev | ||
``` | ||
|
||
Your application will be available at `http://localhost:5173`. | ||
|
||
## Building for Production | ||
|
||
Create a production build: | ||
|
||
```bash | ||
npm run build | ||
``` | ||
|
||
## Deployment | ||
|
||
### Docker Deployment | ||
|
||
This template includes three Dockerfiles optimized for different package managers: | ||
|
||
- `Dockerfile` - for npm | ||
- `Dockerfile.pnpm` - for pnpm | ||
- `Dockerfile.bun` - for bun | ||
|
||
To build and run using Docker: | ||
|
||
```bash | ||
# For npm | ||
docker build -t my-app . | ||
|
||
# For pnpm | ||
docker build -f Dockerfile.pnpm -t my-app . | ||
|
||
# For bun | ||
docker build -f Dockerfile.bun -t my-app . | ||
|
||
# Run the container | ||
docker run -p 3000:3000 my-app | ||
``` | ||
|
||
The containerized application can be deployed to any platform that supports Docker, including: | ||
|
||
- AWS ECS | ||
- Google Cloud Run | ||
- Azure Container Apps | ||
- Digital Ocean App Platform | ||
- Fly.io | ||
- Railway | ||
|
||
### DIY Deployment | ||
|
||
If you're familiar with deploying Node applications, the built-in app server is production-ready. | ||
|
||
Make sure to deploy the output of `npm run build` | ||
|
||
``` | ||
├── package.json | ||
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) | ||
├── build/ | ||
│ ├── client/ # Static assets | ||
│ └── server/ # Server-side code | ||
``` | ||
|
||
## Styling | ||
|
||
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. | ||
|
||
--- | ||
|
||
Built with ❤️ using React Router. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import bcrypt from "bcrypt"; | ||
import crypto from "node:crypto"; | ||
import { createCookieSessionStorage, redirect } from "react-router"; | ||
|
||
import { env } from "./env"; | ||
import { | ||
createUser, | ||
createVerificationToken, | ||
deleteVerificationToken, | ||
getCredentialAccount, | ||
getUser, | ||
getUserByEmail, | ||
getVerificationToken, | ||
updatePassword, | ||
} from "./data/user"; | ||
|
||
const authSessionStorage = createCookieSessionStorage({ | ||
cookie: { | ||
name: "__session", | ||
httpOnly: true, | ||
secure: process.env.NODE_ENV === "production", | ||
secrets: [env.SESSION_SECRET], | ||
sameSite: "lax", | ||
path: "/", | ||
}, | ||
}); | ||
|
||
class Auth { | ||
async #getSession(request?: Request) { | ||
const session = await authSessionStorage.getSession( | ||
request?.headers.get("Cookie") | ||
); | ||
return session; | ||
} | ||
|
||
async getUserId(request: Request) { | ||
const session = await this.#getSession(request); | ||
return session.get("userId"); | ||
} | ||
|
||
async getUser(request: Request) { | ||
const userId = await this.getUserId(request); | ||
if (!userId) return null; | ||
const user = await getUser(userId); | ||
return user; | ||
} | ||
|
||
async getUserOrFail(request: Request) { | ||
const user = await this.getUser(request); | ||
const url = new URL(request.url); | ||
const searchParams = | ||
url.pathname && | ||
new URLSearchParams([["redirect", url.pathname + url.search]]); | ||
if (!user) throw redirect(`/login?${searchParams}`); | ||
return user; | ||
} | ||
|
||
async login(userId: number) { | ||
const session = await this.#getSession(); | ||
session.set("userId", userId); | ||
const cookie = await authSessionStorage.commitSession(session); | ||
return cookie; | ||
} | ||
|
||
async signIn(email: string, password: string) { | ||
const account = await getCredentialAccount(email); | ||
if (!account || !account.password) { | ||
throw new Error("Invalid email or password"); | ||
} | ||
|
||
const isValid = await bcrypt.compare(password, account.password); | ||
if (!isValid) { | ||
throw new Error("Invalid email or password"); | ||
} | ||
|
||
const cookie = await this.login(account.userId); | ||
return cookie; | ||
} | ||
|
||
async signUp(name: string, email: string, password: string) { | ||
password = await bcrypt.hash(password, 10); | ||
const user = await createUser(name, email, password); | ||
const cookie = await this.login(user.id); | ||
return cookie; | ||
} | ||
|
||
async logout() { | ||
const session = await this.#getSession(); | ||
const cookie = await authSessionStorage.destroySession(session); | ||
return cookie; | ||
} | ||
|
||
async forgetPassword(email: string) { | ||
const user = await getUserByEmail(email); | ||
if (!user) throw new Error("User not found"); | ||
|
||
const expiresAt = new Date(); | ||
expiresAt.setHours(expiresAt.getHours() + 1); | ||
const token = crypto.randomBytes(32).toString("hex"); | ||
|
||
await createVerificationToken(email, expiresAt.toISOString(), token); | ||
return { user, token }; | ||
} | ||
|
||
async resetPassword(email: string, password: string, token: string) { | ||
const verificationToken = await getVerificationToken(email); | ||
|
||
if ( | ||
!verificationToken || | ||
new Date(verificationToken.expires) < new Date() | ||
) { | ||
throw new Error("Invalid or expired token"); | ||
} | ||
|
||
const isValid = await bcrypt.compare(token, verificationToken.token); | ||
if (!isValid) { | ||
throw new Error("Invalid token"); | ||
} | ||
|
||
password = await bcrypt.hash(password, 10); | ||
|
||
await updatePassword(verificationToken.identifier, password); | ||
await deleteVerificationToken(verificationToken.token); | ||
} | ||
} | ||
|
||
export const auth = new Auth(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
class BodyParser { | ||
async parse(request: Request) { | ||
const searchParams = new URL(request.url).searchParams; | ||
const formData = await request.formData(); | ||
return { ...this.parseForm(searchParams), ...this.parseForm(formData) }; | ||
} | ||
|
||
parseForm(form: FormData | URLSearchParams) { | ||
const object: Record<string, any> = {}; | ||
form.forEach((value, key) => { | ||
const parts = key.split(/[.[\]]+/).filter(Boolean); | ||
let current = object; | ||
|
||
parts.forEach((part, index) => { | ||
const isLast = index === parts.length - 1; | ||
const nextPart = parts[index + 1]; | ||
const isNextArray = !isLast && !isNaN(Number(nextPart)); | ||
|
||
if (isLast) { | ||
current[part] = value; | ||
} else { | ||
if (isNextArray) { | ||
if (!Array.isArray(current[part])) { | ||
current[part] = []; | ||
} | ||
current = current[part]; | ||
} else { | ||
if ( | ||
!(typeof current[part] === "object" && current[part] !== null) | ||
) { | ||
current[part] = {}; | ||
} | ||
current = current[part]; | ||
} | ||
} | ||
}); | ||
}); | ||
return object; | ||
} | ||
} | ||
|
||
export const bodyParser = new BodyParser(); |
Oops, something went wrong.