Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add authentication using signed cookie #3

Merged
merged 5 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_NAME=ara

COOKIE_SECRET=
68 changes: 55 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,65 @@
# Ara API

Restful API for Ara, KAIST's official community service.
[![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white)](https://bun.sh)
[![Hono](https://img.shields.io/badge/Hono-ffffff.svg?style=for-the-badge&logo=hono)](https://hono.dev)
[![Drizzle](https://img.shields.io/badge/drizzle-000.svg?style=for-the-badge&logo=drizzle)](https://orm.drizzle.team)
[![MySQL](https://img.shields.io/badge/mysql-4479A1.svg?style=for-the-badge&logo=mysql&logoColor=white)](https://www.mysql.com)

## Getting Started

Copy the environment variables:
### Prerequisites

```bash
cp .env.example .env
```
Before you begin, ensure you have the following installed:

To install dependencies:
- Docker
- [Bun v1.1](https://bun.sh/) (JavaScript runtime and package manager)

```bash
bun install
```
### Installation

To run:
1. **Set up environment variables:**

```bash
bun dev
```
Copy the example environment file and if necessary, modify it according to your
local environment settings.

```bash
cp .env.example .env
```

2. **Install dependencies:**

Use Bun to install all necessary dependencies.

```bash
bun install
```

3. **Database Setup:**

Start the database container using Docker.

```bash
docker compose up -d # This will start the database container in detached mode
```

Run database migrations to set up the required database schema.

```bash
bun db:migrate
```

4. **Running the application:**

To start the application in development mode, use the following command:

```bash
bun dev
```

## Commit Guidelines

This project uses [gitmoji](https://gitmoji.dev) for commit messages.
Check the [gitmoji specification](https://gitmoji.dev/specification) for more details.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
Binary file modified bun.lockb
Binary file not shown.
10 changes: 8 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ export default tseslint.config(
drizzle,
},
rules: {
'drizzle/enforce-delete-with-where': 'error',
'drizzle/enforce-update-with-where': 'error',
'drizzle/enforce-delete-with-where': [
'error',
{ drizzleObjectName: 'db' },
],
'drizzle/enforce-update-with-where': [
'error',
{ drizzleObjectName: 'db' },
],
},
},
)
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"package.json": "sort-package-json"
},
"dependencies": {
"@hono/zod-validator": "^0.2.1",
"drizzle-orm": "^0.30.10",
"hono": "^4.2.6",
"drizzle-zod": "^0.5.1",
"hono": "^4.3.7",
"mysql2": "^3.9.7",
"zod": "^3.23.8"
},
Expand Down
51 changes: 51 additions & 0 deletions src/common/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createSelectSchema } from 'drizzle-zod'
import type { Context } from 'hono'
import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie'
import type { CookieOptions } from 'hono/utils/cookie'
import { z } from 'zod'

import { user as userSchema } from '@/db/schema'
import { env } from '@/env'

const cookieSchema = z.object({
user: createSelectSchema(userSchema, {
signUpDate: z.coerce.date(),
}).omit({
password: true,
}),
})
export type Cookie = z.infer<typeof cookieSchema>
type CookieKey = keyof Cookie

const defaultSetOptions = {
httpOnly: true,
secure: true,
sameSite: 'Strict',
} as const satisfies CookieOptions
type SetCookieOptions = Omit<CookieOptions, keyof typeof defaultSetOptions>

export const cookie = {
get: async <TKey extends CookieKey>(
c: Context,
key: TKey,
): Promise<{ ok: true; data: Cookie[TKey] | undefined } | { ok: false }> => {
const value = await getSignedCookie(c, env.COOKIE_SECRET, key)

if (value === false) return { ok: false }
if (value === undefined) return { ok: true, data: undefined }
return { ok: true, data: cookieSchema.shape[key].parse(JSON.parse(value)) }
},
set: <TKey extends CookieKey>(
c: Context,
key: TKey,
value: Cookie[TKey],
options?: SetCookieOptions,
) =>
setSignedCookie(c, key, JSON.stringify(value), env.COOKIE_SECRET, {
...defaultSetOptions,
...options,
}),
delete: <TKey extends CookieKey>(c: Context, key: TKey) => {
deleteCookie(c, key, { secure: true })
},
}
3 changes: 3 additions & 0 deletions src/common/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const consoleLogger = (message: string, ...rest: string[]) => {
console.log(message, ...rest)
}
7 changes: 6 additions & 1 deletion src/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { drizzle } from 'drizzle-orm/mysql2'
import { createPool } from 'mysql2/promise'

import * as schema from '@/db/schema'
import { env } from '@/env'

const connectionPool = createPool({
Expand All @@ -11,4 +12,8 @@ const connectionPool = createPool({
database: env.DB_NAME,
})

export const db = drizzle(connectionPool)
export const db = drizzle(connectionPool, {
schema,
mode: 'default',
logger: true,
})
10 changes: 10 additions & 0 deletions src/db/migration/0000_cloudy_daredevil.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE `auth_user` (
`id` int AUTO_INCREMENT NOT NULL,
`password` varchar(128) NOT NULL,
`nickname` varchar(32) NOT NULL,
`email` varchar(255) NOT NULL,
`signup_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT `auth_user_id` PRIMARY KEY(`id`),
CONSTRAINT `auth_user_nickname_unique` UNIQUE(`nickname`),
CONSTRAINT `auth_user_email_unique` UNIQUE(`email`)
);
72 changes: 72 additions & 0 deletions src/db/migration/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"version": "5",
"dialect": "mysql",
"id": "a824d5e5-dff7-4abc-8eaf-6a4f119cafa3",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"auth_user": {
"name": "auth_user",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"password": {
"name": "password",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"nickname": {
"name": "nickname",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"signup_date": {
"name": "signup_date",
"type": "datetime",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"auth_user_id": {
"name": "auth_user_id",
"columns": ["id"]
}
},
"uniqueConstraints": {
"auth_user_nickname_unique": {
"name": "auth_user_nickname_unique",
"columns": ["nickname"]
},
"auth_user_email_unique": {
"name": "auth_user_email_unique",
"columns": ["email"]
}
}
}
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
14 changes: 13 additions & 1 deletion src/db/migration/meta/_journal.json
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
{ "version": "6", "dialect": "mysql", "entries": [] }
{
"version": "6",
"dialect": "mysql",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1715756185055,
"tag": "0000_cloudy_daredevil",
"breakpoints": true
}
]
}
1 change: 1 addition & 0 deletions src/db/schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './user'
20 changes: 20 additions & 0 deletions src/db/schema/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { sql } from 'drizzle-orm'
import { datetime, int, mysqlTable, varchar } from 'drizzle-orm/mysql-core'
import { createInsertSchema } from 'drizzle-zod'
import { z } from 'zod'

export const user = mysqlTable('auth_user', {
id: int('id').primaryKey().autoincrement(),
password: varchar('password', { length: 128 }).notNull(),
nickname: varchar('nickname', { length: 32 }).notNull().unique(),
email: varchar('email', { length: 255 }).notNull().unique(),
signUpDate: datetime('signup_date')
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
})

export const insertUserSchema = createInsertSchema(user, {
password: z.string().min(8),
nickname: (schema) => schema.nickname.min(3),
email: (schema) => schema.email.email(),
})
2 changes: 2 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ export const env = z
DB_USER: z.string(),
DB_PASSWORD: z.string(),
DB_NAME: z.string(),

COOKIE_SECRET: z.string(),
})
.parse(process.env)
16 changes: 15 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'

import { consoleLogger } from '@/common/log'
import auth from '@/route/auth'
import users from '@/route/users'

const app = new Hono({ strict: false })

app.get('/', (c) => c.text('Hello Ara!'))
// Debugging
app.use(logger(consoleLogger))

// Middlewares
app.use(cors({ origin: 'http://localhost:5173', credentials: true }))

// Routes
app.route('/auth', auth)
app.route('/users', users)

export default app
15 changes: 15 additions & 0 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception'

import { type Cookie, cookie } from '@/common/cookie'

export const authGuard = createMiddleware<{
Variables: { user: Cookie['user'] }
}>(async (c, next) => {
const res = await cookie.get(c, 'user')
if (!res.ok || !res.data)
throw new HTTPException(401, { message: 'Authentication required' })

c.set('user', res.data)
await next()
})
Loading