Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat: add auth #33

Merged
merged 19 commits into from
Dec 12, 2023
1 change: 1 addition & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=12345678
POSTGRES_DB=diary-db
JWT_SECRET=itssecret

DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?schema=public
1 change: 0 additions & 1 deletion apps/api/__tests__/services/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,4 @@ describe('userService', () => {
expect(user).not.toBeNull()
expect(user?.name).toBe('Test User')
})

})
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"tests": "bun test --coverage"
},
"dependencies": {
"@elysiajs/cookie": "^0.7.0",
"@elysiajs/jwt": "^0.7.0",
"@elysiajs/swagger": "^0.7.4",
"@prisma/client": "5.7.0",
"elysia": "latest",
Expand Down
8 changes: 5 additions & 3 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ datasource db {
}

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
id Int @id @default(autoincrement())
email String @unique
password String
salt String
name String?
}
42 changes: 42 additions & 0 deletions apps/api/src/handlers/authHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ApiResponse, ContextWith } from '../types'
import { authenticateUser } from '@services'

interface Body {
password: string
login: string
id: number
}

export const postAuth = async ({
body,
jwt,
setCookie,
cookie
}: ContextWith<never, Body>): Promise<ApiResponse<string>> => {
if (typeof body === 'undefined') {
return {
success: false,
data: 'Body is required'
}
}

const { login, password, id } = body

const { success } = await authenticateUser(id, login, password)

setCookie('auth', await jwt.sign(body), {
httpOnly: true,
maxAge: 7 * 86400,
secure: true
})

const verify = await jwt.verify(cookie.auth)

console.debug('verify', verify)
console.debug('cookie.auth', cookie.auth)
console.debug('success', success)

return {
success,
}
}
1 change: 1 addition & 0 deletions apps/api/src/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './userHandler'
export * from './authHandler'
4 changes: 2 additions & 2 deletions apps/api/src/handlers/userHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getAllUsers, getUserById } from '@services'
import { User } from 'shared'
import { getAllUsers, getUserById } from '../services/userService'
import { ApiResponse, ContextWith } from '../types'

type Params = { id: string }
Expand All @@ -15,7 +15,7 @@ export const getAllUsersHandler = async (): Promise<ApiResponse<User[]>> => {

export const getUserByIdHandler = async ({
params
}: ContextWith<Params>): Promise<ApiResponse<User | null>> => {
}: ContextWith<Params, unknown>): Promise<ApiResponse<User | null>> => {
const userId = parseInt(params.id, 10)
const user = await getUserById(userId)

Expand Down
16 changes: 15 additions & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import cookie from '@elysiajs/cookie'
import jwt, { JWTOption } from '@elysiajs/jwt'
import { handleErrors } from '@utils'
import Elysia from 'elysia'
import auth from './routes/auth/authRoutes'
import users from './routes/user/userRoutes'

const app = new Elysia().use(users).listen(3000).onError(handleErrors)
const authConfig: JWTOption<string> = {
name: 'jwt',
secret: Bun.env.JWT_SECRET ?? 'secret'
}

const app = new Elysia()
.use(jwt(authConfig))
.use(cookie())
.use(users)
.use(auth)
.listen(3000)
.onError(handleErrors)

console.log(
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
Expand Down
Empty file.
19 changes: 19 additions & 0 deletions apps/api/src/routes/auth/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { postAuth } from '@handlers'
import Elysia, { t } from 'elysia'
import { authBody } from './authSchema'
import { getUserById } from '@services'

const app = new Elysia().post('/auth', postAuth, {
body: t.Object(authBody),
// TODO: move to custom plugin (middleware)
beforeHandle: async (context) => {
const user = await getUserById(context.body.id)

if (!user || user.id !== context.body.id || user.login !== context.body.login) {
context.set.status = 400
return 'Bad request'
}
}
})

export default app
7 changes: 7 additions & 0 deletions apps/api/src/routes/auth/authSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { t } from 'elysia'

export const authBody = {
id: t.Number(),
password: t.String(),
login: t.String()
}
39 changes: 39 additions & 0 deletions apps/api/src/services/authService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { comparePassword } from '@utils'
import { ApiResponse } from '../types'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export const authenticateUser = async (
id: number,
login: string,
password: string,
): Promise<ApiResponse<string>> => {
console.log(id)
console.log(password)
Fixed Show fixed Hide fixed
console.log(login)
const user = await prisma.user.findUnique({
where: {
email: login,
id
},
})

const isValidPassword = await comparePassword(
password,
user!.salt,
user!.password,
)

if (!isValidPassword) {
return {
success: false,
data: 'Invalid password',
}
}

return {
success: true,
data: 'Password',
}
}
1 change: 1 addition & 0 deletions apps/api/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './userService'
export * from './authService'
6 changes: 4 additions & 2 deletions apps/api/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Context as ElysiaContext } from 'elysia'
/** Стандартизирует ответ API: флаг `success` и данные с кастомным типом T */
export interface ApiResponse<T> {
success: boolean
data: T
data?: T
}

/** Расширяет контекст Elysia с сильно-типизированным объектом `params` */
export interface ContextWith<T> extends Omit<ElysiaContext, 'params'> {
export interface ContextWith<T, U>
extends Omit<ElysiaContext, 'params' | 'body'> {
params: T
body?: U
}
17 changes: 17 additions & 0 deletions apps/api/src/utils/comparePassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { pbkdf2Hash } from './pbkdf2Hash'

/**
* Сравнивает пароль с хешем и солью для проверки соответствия.
* @param password - Пароль, который нужно сравнить.
* @param salt - Соль, использованная при хешировании.
* @param hash - Хеш, с которым сравнивается пароль.
* @returns Промис, который разрешается булевым значением, указывающим соответствие пароля хешу.
*/
export const comparePassword = async (
password: string,
salt: string,
hash: string
): Promise<boolean> => {
const derivedKey = await pbkdf2Hash(password, salt)
return hash === derivedKey
}
4 changes: 2 additions & 2 deletions apps/api/src/utils/getRandomNumber.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const getRandomID = () => {
let array = new Uint32Array(1)
const array = new Uint32Array(1)
crypto.getRandomValues(array)

const maxIntValue = 2147483647
return array[0] % maxIntValue
}
15 changes: 15 additions & 0 deletions apps/api/src/utils/hashPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { randomBytes } from 'crypto'
import { pbkdf2Hash } from './pbkdf2Hash'

/**
* Хеширует пароль с использованием PBKDF2 и случайно сгенерированной соли.
* @param password - Пароль, который нужно захешировать.
* @returns Промис, который разрешается объектом с хешем и солью.
*/
export const hashPassword = async (
password: string
): Promise<{ hash: string; salt: string }> => {
const salt = randomBytes(16).toString('hex')
const hash = await pbkdf2Hash(password, salt)
return { hash, salt }
}
4 changes: 4 additions & 0 deletions apps/api/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export * from './handleErrors'
export * from './getRandomNumber'
export * from './pbkdf2Hash'
export * from './md5hash'
export * from './hashPassword'
export * from './comparePassword'
11 changes: 11 additions & 0 deletions apps/api/src/utils/md5hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createHash } from 'crypto'

/**
* Генерирует MD5-хеш для заданного текста.
* @param text - Текст, который нужно захешировать.
* @returns MD5-хеш введенного текста.
*/
function md5hash(text: string) {
// Создаем объект хеша MD5, обновляем его текстом и получаем результат в шестнадцатеричном формате.
return createHash('md5').update(text).digest('hex')
}
22 changes: 22 additions & 0 deletions apps/api/src/utils/pbkdf2Hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { pbkdf2 } from 'crypto'

/**
* Хеширует данные с использованием PBKDF2.
* @param password - Данные для хеширования (например, пароль).
* @param salt - Соль для хеширования.
* @returns Промис, который разрешается строкой с хешем.
*/
export const pbkdf2Hash = async (
password: string,
salt: string
): Promise<string> => {
return new Promise((resolve, reject) => {
pbkdf2(password, salt, 1000, 64, 'sha512', (error, derivedKey) => {
if (error) {
return reject(error)
}

return resolve(derivedKey.toString('hex'))
})
})
}
4 changes: 3 additions & 1 deletion apps/shared/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export interface User {
id: number
email: string
name?: string | null
password: string
salt: string
name?: string
}
Binary file modified bun.lockb
Binary file not shown.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
"test": "echo \"Error: no test specified\" && exit 1",
"prepare": "husky install && bun format",
"dev:api": "cd apps/api && bun dev",
"dev:startDB": "cd apps/api && db:start",
"dev:web": "cd apps/web && bun dev",
"dev:startDB": "cd apps/api && bun db:start",
"lint": "biome lint . --apply",
"format": "biome format . --write",
"checkAll": "biome check . --apply",
"build:web": "cd apps/web && bun run build",
"build:api": "cd apps/api && bun run build",
"build:api:types": "cd apps/api && bun run build:types",
"build:shared": "cd apps/shared && bun run build"
"build:api:types": "cd apps/api && bun run build:types"
},
"workspaces": [
"apps/*"
Expand Down
Loading