diff --git a/src/lib/dtos/create-user-role.dto.ts b/src/lib/dtos/create-user-role.dto.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib/dtos/create-user-role.dto.ts @@ -0,0 +1 @@ + diff --git a/src/lib/dtos/signup-username-email.dto.ts b/src/lib/dtos/signup-username-email.dto.ts index 1cd0b87..e0acae7 100644 --- a/src/lib/dtos/signup-username-email.dto.ts +++ b/src/lib/dtos/signup-username-email.dto.ts @@ -15,4 +15,6 @@ export const signupUsernameEmailDto = z.object({ }) .superRefine(({ confirm_password, password }, ctx) => { refinePasswords(confirm_password, password, ctx); - }); \ No newline at end of file + }); + +export type SignupUsernameEmailDto = z.infer diff --git a/src/lib/server/api/controllers/signup.controller.ts b/src/lib/server/api/controllers/signup.controller.ts index ca795af..d7ae203 100644 --- a/src/lib/server/api/controllers/signup.controller.ts +++ b/src/lib/server/api/controllers/signup.controller.ts @@ -1,23 +1,32 @@ import 'reflect-metadata'; import { Hono } from 'hono'; -import { injectable } from 'tsyringe'; +import {inject, injectable} from 'tsyringe'; import { zValidator } from '@hono/zod-validator'; import type { HonoTypes } from '../types'; import type { Controller } from '../interfaces/controller.interface'; import { signupUsernameEmailDto } from "$lib/dtos/signup-username-email.dto"; import {limiter} from "$lib/server/api/middleware/rate-limiter.middleware"; +import {UsersService} from "$lib/server/api/services/users.service"; @injectable() export class SignupController implements Controller { controller = new Hono(); constructor( + @inject(UsersService) private readonly usersService: UsersService ) { } routes() { return this.controller .post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { const { firstName, lastName, email, username, password } = await c.req.valid('json'); + const existingUser = await this.usersService.findOneByUsername(username); + + if (existingUser) { + return c.body("User already exists", 400); + } + + const user = await this.usersService.create(signupUsernameEmailDto); // const existing_user = await db.query.usersTable.findFirst({ // where: eq(usersTable.username, form.data.username), diff --git a/src/lib/server/api/repositories/credentials.repository.ts b/src/lib/server/api/repositories/credentials.repository.ts index 3c9ee07..3cc4e6c 100644 --- a/src/lib/server/api/repositories/credentials.repository.ts +++ b/src/lib/server/api/repositories/credentials.repository.ts @@ -2,10 +2,12 @@ import { and, eq, type InferInsertModel } from "drizzle-orm"; import { credentialsTable, CredentialsType } from "../infrastructure/database/tables/credentials.table"; import { db } from "../infrastructure/database"; import { takeFirstOrThrow } from "../infrastructure/database/utils"; +import {injectable} from "tsyringe"; export type CreateCredentials = InferInsertModel; export type UpdateCredentials = Partial; +@injectable() export class CredentialsRepository { async findOneByUserId(userId: string) { diff --git a/src/lib/server/api/repositories/roles.repository.ts b/src/lib/server/api/repositories/roles.repository.ts new file mode 100644 index 0000000..7ced97f --- /dev/null +++ b/src/lib/server/api/repositories/roles.repository.ts @@ -0,0 +1,76 @@ +import { eq, type InferInsertModel } from 'drizzle-orm'; +import { takeFirstOrThrow } from '../infrastructure/database/utils'; +import { db } from '../infrastructure/database'; +import {roles} from "$lib/server/api/infrastructure/database/tables"; +import {injectable} from "tsyringe"; + +/* -------------------------------------------------------------------------- */ +/* Repository */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Repositories are the layer that interacts with the database. They are responsible for retrieving and +storing data. They should not contain any business logic, only database queries. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* + Repositories should only contain methods for CRUD operations and any other database interactions. + Any complex logic should be delegated to a service. If a repository method requires a transaction, + it should be passed in as an argument or the class should have a method to set the transaction. + In our case the method 'trxHost' is used to set the transaction context. +*/ + +export type CreateRole = InferInsertModel; +export type UpdateRole = Partial; + +@injectable() +export class RolesRepository { + async findOneById(id: string) { + return db.query.roles.findFirst({ + where: eq(roles.id, id) + }); + } + + async findOneByIdOrThrow(id: string) { + const role = await this.findOneById(id); + if (!role) throw Error('Role not found'); + return role; + } + + async findAll() { + return db.query.roles.findMany(); + } + + async findOneByName(name: string) { + return db.query.roles.findFirst({ + where: eq(roles.name, name) + }); + } + + async findOneByNameOrThrow(name: string) { + const role = await this.findOneByName(name); + if (!role) throw Error('Role not found'); + return role; + } + + async create(data: CreateRole) { + return db.insert(roles).values(data).returning().then(takeFirstOrThrow); + } + + async update(id: string, data: UpdateRole) { + return db + .update(roles) + .set(data) + .where(eq(roles.id, id)) + .returning() + .then(takeFirstOrThrow); + } + + async delete(id: string) { + return db + .delete(roles) + .where(eq(roles.id, id)) + .returning() + .then(takeFirstOrThrow); + } +} diff --git a/src/lib/server/api/repositories/user_roles.repository.ts b/src/lib/server/api/repositories/user_roles.repository.ts new file mode 100644 index 0000000..d553a69 --- /dev/null +++ b/src/lib/server/api/repositories/user_roles.repository.ts @@ -0,0 +1,58 @@ +import { eq, type InferInsertModel } from 'drizzle-orm'; +import { usersTable } from '../infrastructure/database/tables/users.table'; +import { takeFirstOrThrow } from '../infrastructure/database/utils'; +import { db } from '../infrastructure/database'; +import {user_roles} from "$lib/server/api/infrastructure/database/tables"; +import {injectable} from "tsyringe"; + +/* -------------------------------------------------------------------------- */ +/* Repository */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Repositories are the layer that interacts with the database. They are responsible for retrieving and +storing data. They should not contain any business logic, only database queries. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* + Repositories should only contain methods for CRUD operations and any other database interactions. + Any complex logic should be delegated to a service. If a repository method requires a transaction, + it should be passed in as an argument or the class should have a method to set the transaction. + In our case the method 'trxHost' is used to set the transaction context. +*/ + +export type CreateUserRole = InferInsertModel; +export type UpdateUserRole = Partial; + +@injectable() +export class UserRolesRepository { + async findOneById(id: string) { + return db.query.user_roles.findFirst({ + where: eq(user_roles.id, id) + }); + } + + async findOneByIdOrThrow(id: string) { + const userRole = await this.findOneById(id); + if (!userRole) throw Error('User not found'); + return userRole; + } + + async findAllByUserId(userId: string) { + return db.query.user_roles.findMany({ + where: eq(user_roles.user_id, userId) + }); + } + + async create(data: CreateUserRole) { + return db.insert(user_roles).values(data).returning().then(takeFirstOrThrow); + } + + async delete(id: string) { + return db + .delete(user_roles) + .where(eq(user_roles.id, id)) + .returning() + .then(takeFirstOrThrow); + } +} diff --git a/src/lib/server/api/repositories/users.repository.ts b/src/lib/server/api/repositories/users.repository.ts index addeaf8..5c60ba6 100644 --- a/src/lib/server/api/repositories/users.repository.ts +++ b/src/lib/server/api/repositories/users.repository.ts @@ -2,6 +2,7 @@ import { eq, type InferInsertModel } from 'drizzle-orm'; import { usersTable } from '../infrastructure/database/tables/users.table'; import { takeFirstOrThrow } from '../infrastructure/database/utils'; import { db } from '../infrastructure/database'; +import {injectable} from "tsyringe"; /* -------------------------------------------------------------------------- */ /* Repository */ @@ -22,6 +23,7 @@ storing data. They should not contain any business logic, only database queries. export type CreateUser = InferInsertModel; export type UpdateUser = Partial; +@injectable() export class UsersRepository { async findOneById(id: string) { return db.query.usersTable.findFirst({ @@ -59,4 +61,12 @@ export class UsersRepository { .returning() .then(takeFirstOrThrow); } + + async delete(id: string) { + return db + .delete(usersTable) + .where(eq(usersTable.id, id)) + .returning() + .then(takeFirstOrThrow); + } } diff --git a/src/lib/server/api/services/roles.service.ts b/src/lib/server/api/services/roles.service.ts new file mode 100644 index 0000000..fad30e5 --- /dev/null +++ b/src/lib/server/api/services/roles.service.ts @@ -0,0 +1,14 @@ +import {inject, injectable} from "tsyringe"; +import { RolesRepository } from "$lib/server/api/repositories/roles.repository"; + +@injectable() +export class RolesService { + constructor( + @inject(RolesRepository) private readonly rolesRepository: RolesRepository + ) { } + + + async findOneByNameOrThrow(name: string) { + return this.rolesRepository.findOneByNameOrThrow(name); + } +} \ No newline at end of file diff --git a/src/lib/server/api/services/tokens.service.ts b/src/lib/server/api/services/tokens.service.ts index a3f712e..c204fb0 100644 --- a/src/lib/server/api/services/tokens.service.ts +++ b/src/lib/server/api/services/tokens.service.ts @@ -29,6 +29,10 @@ export class TokensService { } } + async createHashedToken(token: string) { + return this.hashingService.hash(token) + } + async verifyHashedToken(hashedToken: string, token: string) { return this.hashingService.verify(hashedToken, token) } diff --git a/src/lib/server/api/services/user_roles.service.ts b/src/lib/server/api/services/user_roles.service.ts new file mode 100644 index 0000000..25044bd --- /dev/null +++ b/src/lib/server/api/services/user_roles.service.ts @@ -0,0 +1,42 @@ +import {inject, injectable} from "tsyringe"; +import {type CreateUserRole, UserRolesRepository} from "$lib/server/api/repositories/user_roles.repository"; +import db from "$db"; +import {eq} from "drizzle-orm"; +import {roles, userRoles} from "$db/schema"; +import {RolesService} from "$lib/server/api/services/roles.service"; + +@injectable() +export class UserRolesService { + constructor( + @inject(UserRolesRepository) private readonly userRolesRepository: UserRolesRepository, + @inject(RolesService) private readonly rolesService: RolesService + ) { } + + async findOneById(id: string) { + return this.userRolesRepository.findOneById(id); + } + + async findAllByUserId(userId: string) { + return this.userRolesRepository.findAllByUserId(userId); + } + + async create(data: CreateUserRole) { + return this.userRolesRepository.create(data); + } + + async addRoleToUser(userId: string, roleName: string, primary = false) { + // Find the role by its name + const role = await this.rolesService.findOneByNameOrThrow(roleName); + + if (!role || !role.id) { + throw new Error(`Role with name ${roleName} not found`); + } + + // Create a UserRole entry linking the user and the role + return db.insert(userRoles).values({ + user_id: userId, + role_id: role.id, + primary, + }); + } +} \ No newline at end of file diff --git a/src/lib/server/api/services/users.service.ts b/src/lib/server/api/services/users.service.ts index 8040442..ee1411c 100644 --- a/src/lib/server/api/services/users.service.ts +++ b/src/lib/server/api/services/users.service.ts @@ -1,12 +1,81 @@ import { inject, injectable } from 'tsyringe'; -import type { UsersRepository } from '../repositories/users.repository'; +import { UsersRepository } from '../repositories/users.repository'; +import type {SignupUsernameEmailDto} from "$lib/dtos/signup-username-email.dto"; +import {TokensService} from "$lib/server/api/services/tokens.service"; +import {CredentialsRepository} from "$lib/server/api/repositories/credentials.repository"; +import {CredentialsType} from "$lib/server/api/infrastructure/database/tables"; +import {UserRolesService} from "$lib/server/api/services/user_roles.service"; @injectable() export class UsersService { constructor( - @inject('UsersRepository') private readonly usersRepository: UsersRepository + @inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository, + @inject(TokensService) private readonly tokenService: TokensService, + @inject(UsersRepository) private readonly usersRepository: UsersRepository, + @inject(UserRolesService) private readonly userRolesService: UserRolesService, ) { } + async create(data: SignupUsernameEmailDto) { + const { firstName, lastName, email, username, password } = data; + + const hashedPassword = await this.tokenService.createHashedToken(password); + const user = await this.usersRepository.create({ + first_name: firstName, + last_name: lastName, + email, + username, + }); + + if (!user) { + return null; + } + + const credentials = await this.credentialsRepository.create({ + user_id: user.id, + type: CredentialsType.PASSWORD, + secret_data: hashedPassword, + }); + + if (!credentials) { + await this.usersRepository.delete(user.id); + return null; + } + + this.userRolesService.addRoleToUser(user.id, 'user', true); +// +// const user = await db +// .insert(usersTable) +// .values({ +// username: form.data.username, +// hashed_password: hashedPassword, +// email: form.data.email, +// first_name: form.data.firstName ?? '', +// last_name: form.data.lastName ?? '', +// verified: false, +// receive_email: false, +// theme: 'system', +// }) +// .returning(); +// console.log('signup user', user); +// +// if (!user || user.length === 0) { +// return fail(400, { +// form, +// message: `Could not create your account. Please try again. If the problem persists, please contact support. Error ID: ${cuid2()}`, +// }); +// } +// +// await add_user_to_role(user[0].id, 'user', true); +// await db.insert(collections).values({ +// user_id: user[0].id, +// }); +// await db.insert(wishlists).values({ +// user_id: user[0].id, +// }); + + return this.usersRepository.create(data); + } + async findOneByUsername(username: string) { return this.usersRepository.findOneByUsername(username); }