Skip to content

Commit

Permalink
start
Browse files Browse the repository at this point in the history
  • Loading branch information
okalil committed Jan 2, 2025
0 parents commit 1f66278
Show file tree
Hide file tree
Showing 68 changed files with 13,938 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md
9 changes: 9 additions & 0 deletions .gitignore
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
22 changes: 22 additions & 0 deletions Dockerfile
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"]
25 changes: 25 additions & 0 deletions Dockerfile.bun
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"]
26 changes: 26 additions & 0 deletions Dockerfile.pnpm
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"]
98 changes: 98 additions & 0 deletions README.md
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.
127 changes: 127 additions & 0 deletions app/.server/auth.ts
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();
42 changes: 42 additions & 0 deletions app/.server/body-parser.ts
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();
Loading

0 comments on commit 1f66278

Please sign in to comment.