Skip to content

Commit

Permalink
fix: move register to auth domain
Browse files Browse the repository at this point in the history
cause it more make sense and it simplify the dependency injection, minimize duplication in user repository and auth repository
  • Loading branch information
zulfikarrosadi committed Sep 23, 2024
1 parent 5953bb0 commit 70708ab
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 142 deletions.
41 changes: 40 additions & 1 deletion src/auth/handler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { Request, Response } from 'express'
import { accessTokenMaxAge, refreshTokenMaxAge } from '../lib/token'
import type ApiResponse from '../schema'
import type { Login } from './schema'
import type { Login, RegisterUser } from './schema'
import type { User } from './service'

interface AuthService {
registerUser(data: RegisterUser): Promise<{
response: ApiResponse<Omit<User, 'password'>>
token?: { accessToken: string; refreshToken: string }
}>
login(data: Login): Promise<{
response: ApiResponse<Omit<User, 'password'>>
token?: { accessToken: string; refreshToken: string }
Expand All @@ -18,6 +22,41 @@ interface AuthService {
class AuthHandler {
constructor(private service: AuthService) {}

registerUser = async (
req: Request<
Record<string, unknown>,
Record<string, unknown>,
RegisterUser
>,
res: Response<ApiResponse<Omit<User, 'password'>>>,
) => {
const result = await this.service.registerUser({
fullname: req.body.fullname,
password: req.body.password,
email: req.body.email,
})
if (result.response.status === 'fail') {
return res.status(result.response.errors.code).json(result.response)
}

return res
.status(201)
.cookie('accessToken', result.token?.accessToken, {
secure: true,
sameSite: 'none',
httpOnly: true,
maxAge: accessTokenMaxAge,
})
.cookie('refreshToken', result.token?.refreshToken, {
secure: true,
sameSite: 'none',
httpOnly: true,
path: '/api/refresh',
maxAge: refreshTokenMaxAge,
})
.json(result.response)
}

login = async (
req: Request<Record<string, unknown>, Record<string, unknown>, Login>,
res: Response,
Expand Down
55 changes: 52 additions & 3 deletions src/auth/repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Pool, ResultSetHeader, RowDataPacket } from 'mysql2/promise'
import { AuthCredentialError } from '../lib/Error'
import {
AuthCredentialError,
EmailAlreadyExistsError,
ServerError,
} from '../lib/Error'
import type { RegisterUser } from './schema'

type User = {
email: string
Expand All @@ -11,6 +16,31 @@ type User = {
class AuthRepository {
constructor(private db: Pool) {}

private USER_ALREADY_EXISTS = 1062
private ER_BAD_DB_ERROR = 'ER_BAD_DB_ERROR'

async createUser(data: RegisterUser) {
try {
const [rows] = await this.db.execute(
'INSERT INTO users (fullname, email, password) VALUES (?,?,?)',
[data.fullname, data.email, data.password],
)
const result = rows as ResultSetHeader

return { userId: result.insertId }
} catch (error: any) {
if (error.errno === this.USER_ALREADY_EXISTS) {
throw new EmailAlreadyExistsError()
}
if (error.code && error.code === this.ER_BAD_DB_ERROR) {
throw new ServerError(
"error while creating the account, this is not your fault, we're working on it. please try again later",
)
}
throw new Error(error)
}
}

async getUserByEmail(email: string) {
const [rows] = await this.db.query<RowDataPacket[]>(
'SELECT id, email, fullname, password from users WHERE email = ?',
Expand All @@ -30,13 +60,32 @@ class AuthRepository {
}
}

async getUserById(
id: number,
): Promise<{ id: number; fullname: string; email: string }> {
const [rows] = await this.db.query<RowDataPacket[]>(
'SELECT id, fullname, email FROM users WHERE id = ?',
[id],
)

if (!rows.length) {
throw new Error('user not found')
}

return {
id: rows[0]?.id,
fullname: rows[0]?.fullname,
email: rows[0]?.email,
}
}

async saveTokenToDb(token: string, userId: number) {
const [rows] = await this.db.execute(
'UPDATE users SET refresh_token = ? WHERE id = ?',
[token, userId],
)

return rows as ResultSetHeader
const result = rows as ResultSetHeader
return { affectedRows: result.affectedRows }
}

async getTokenByUserId(userId: number) {
Expand Down
21 changes: 21 additions & 0 deletions src/auth/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,25 @@ export const loginSchema = z.object({
.min(1, 'password is required'),
})

export const registerUser = z
.object({
fullname: z.string().min(1, 'fullname should have minimum 1 characters'),
email: z
.string()
.min(1, 'email is required')
.email('your email is in invalid format'),
password: z.string().min(1, 'password is required'),
passwordConfirmation: z
.string({ required_error: 'password confirmation is required' })
.min(1, 'password confirmation is required'),
})
.refine((data) => data.password === data.passwordConfirmation, {
message: 'password and password confirmation is not match',
path: ['password'],
})

export type RegisterUser = Omit<
z.TypeOf<typeof registerUser>,
'passwordConfirmation'
>
export type Login = z.TypeOf<typeof loginSchema>
57 changes: 56 additions & 1 deletion src/auth/service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Auth } from '../lib/Auth'
import { AuthCredentialError } from '../lib/Error'
import type ApiResponse from '../schema'
import type { Login } from './schema'
import type { Login, RegisterUser } from './schema'

export interface User {
id: number
Expand All @@ -11,7 +11,9 @@ export interface User {
}

interface AuthRepository {
createUser(data: RegisterUser): Promise<{ userId: number }>
getUserByEmail(email: string): Promise<Partial<User>>
getUserById(id: number): Promise<Omit<User, 'password'>>
saveTokenToDb(
token: string,
userId: number,
Expand All @@ -24,6 +26,59 @@ class AuthService extends Auth {
super()
}

async registerUser(data: RegisterUser): Promise<{
response: ApiResponse<Omit<User, 'password'>>
token?: { accessToken: string; refreshToken: string }
}> {
try {
const newUser = await this.repository.createUser({
fullname: data.fullname,
email: data.email,
password: await this.hashPassword(data.password),
})

const user = await this.repository.getUserById(newUser.userId)
if (!user.email || !user.fullname) {
throw new Error('create user is fail, please try again')
}

const accessToken = this.createAccessToken({
fullname: user.fullname,
email: user.email,
userId: newUser.userId,
})
const refreshToken = this.createRefreshToken({
fullname: user.fullname,
email: user.email,
userId: newUser.userId,
})
await this.repository.saveTokenToDb(refreshToken, newUser.userId)
return {
response: {
status: 'success',
data: {
user: {
id: newUser.userId,
fullname: user.fullname,
email: user.email,
},
},
},
token: { accessToken, refreshToken },
}
} catch (error: any) {
return {
response: {
status: 'fail',
errors: {
code: typeof error.code === 'number' ? error.code : 400,
message: error.message || error,
},
},
}
}
}

async login(data: Login): Promise<{
response: ApiResponse<Omit<User, 'password'>>
token?: { accessToken: string; refreshToken: string }
Expand Down
6 changes: 2 additions & 4 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,14 @@ import ProductRepository from './product/repository'
import { createProduct, updateProduct } from './product/schema'
import ProductService from './product/service'
import UserHandler from './user/handler'
import UserRepository from './user/repository'
import UserSerivce from './user/service'

export default function routes(app: Express) {
const authRepo = new AuthRepository(connection)
const authService = new AuthService(authRepo)
const authHandler = new AuthHandler(authService)

const userRepo = new UserRepository(connection)
const userService = new UserSerivce(userRepo)
const userService = new UserSerivce(authRepo)
const userHandler = new UserHandler(userService)

const productRepo = new ProductRepository(connection)
Expand All @@ -38,7 +36,7 @@ export default function routes(app: Express) {
'/api/register',
//@ts-ignore
validateInput(createUserSchema),
userHandler.registerUser,
authHandler.registerUser,
)
app.post('/api/#', validateInput(loginSchema), authHandler.login)
app.get('/api/refresh', authHandler.refreshToken)
Expand Down
38 changes: 1 addition & 37 deletions src/user/handler.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,14 @@
import type { Request, Response } from 'express'
import { accessTokenMaxAge, refreshTokenMaxAge } from '../lib/token'
import type ApiResponse from '../schema'
import type { CreateUser, User } from './schema'
import type { User } from './schema'

interface UserService {
registerUser(data: CreateUser): Promise<{
response: ApiResponse<User>
token?: { accessToken: string; refreshToken: string }
}>
getUserById(idFromUrlPath: string): Promise<{ response: ApiResponse<User> }>
}

class UserHandler {
constructor(public service: UserService) {}

registerUser = async (
req: Request<Record<string, unknown>, Record<string, unknown>, CreateUser>,
res: Response<ApiResponse<User>>,
) => {
const result = await this.service.registerUser({
fullname: req.body.fullname,
password: req.body.password,
email: req.body.email,
})
if (result.response.status === 'fail') {
return res.status(result.response.errors.code).json(result.response)
}

return res
.status(201)
.cookie('accessToken', result.token?.accessToken, {
secure: true,
sameSite: 'none',
httpOnly: true,
maxAge: accessTokenMaxAge,
})
.cookie('refreshToken', result.token?.refreshToken, {
secure: true,
sameSite: 'none',
httpOnly: true,
path: '/api/refresh',
maxAge: refreshTokenMaxAge,
})
.json(result.response)
}

getUserById = async (
req: Request<{ id: string }>,
res: Response<ApiResponse<User>>,
Expand Down
37 changes: 1 addition & 36 deletions src/user/repository.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,8 @@
import type { Pool, ResultSetHeader, RowDataPacket } from 'mysql2/promise'
import { EmailAlreadyExistsError, ServerError } from '../lib/Error'
import type { CreateUser } from './schema'
import type { Pool, RowDataPacket } from 'mysql2/promise'

class UserRepository {
private USER_ALREADY_EXISTS = 1062
private ER_BAD_DB_ERROR = 'ER_BAD_DB_ERROR'
constructor(private db: Pool) {}

async createUser(data: CreateUser) {
try {
const [rows] = await this.db.execute(
'INSERT INTO users (fullname, email, password) VALUES (?,?,?)',
[data.fullname, data.email, data.password],
)
const result = rows as ResultSetHeader

return { userId: result.insertId }
} catch (error: any) {
if (error.errno === this.USER_ALREADY_EXISTS) {
throw new EmailAlreadyExistsError()
}
if (error.code && error.code === this.ER_BAD_DB_ERROR) {
throw new ServerError(
"error while creating the account, this is not your fault, we're working on it. please try again later",
)
}
throw new Error(error)
}
}

async getUserById(
id: number,
): Promise<{ id: number; fullname: string; email: string }> {
Expand All @@ -47,15 +21,6 @@ class UserRepository {
email: rows[0]?.email,
}
}

async saveTokenToDb(refreshToken: string, userId: number) {
const [rows] = await this.db.execute(
'UPDATE users SET refresh_token = ? WHERE id = ?',
[refreshToken, userId],
)
const result = rows as ResultSetHeader
return { affectedRows: result.affectedRows }
}
}

export default UserRepository
Loading

0 comments on commit 70708ab

Please # to comment.