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

Commit

Permalink
add base auth
Browse files Browse the repository at this point in the history
  • Loading branch information
scffs committed Dec 9, 2023
1 parent 4d79c9f commit 7e4d01d
Show file tree
Hide file tree
Showing 23 changed files with 215 additions and 15 deletions.
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)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This logs sensitive data returned by
an access to password
as clear text.
This logs sensitive data returned by
an access to password
as clear text.
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

0 comments on commit 7e4d01d

Please sign in to comment.