mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Signup flow servers and repositories for user create.
This commit is contained in:
parent
2652d4fef6
commit
80b956b35c
11 changed files with 291 additions and 4 deletions
1
src/lib/dtos/create-user-role.dto.ts
Normal file
1
src/lib/dtos/create-user-role.dto.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -15,4 +15,6 @@ export const signupUsernameEmailDto = z.object({
|
||||||
})
|
})
|
||||||
.superRefine(({ confirm_password, password }, ctx) => {
|
.superRefine(({ confirm_password, password }, ctx) => {
|
||||||
refinePasswords(confirm_password, password, ctx);
|
refinePasswords(confirm_password, password, ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { injectable } from 'tsyringe';
|
import {inject, injectable} from 'tsyringe';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
import type { HonoTypes } from '../types';
|
import type { HonoTypes } from '../types';
|
||||||
import type { Controller } from '../interfaces/controller.interface';
|
import type { Controller } from '../interfaces/controller.interface';
|
||||||
import { signupUsernameEmailDto } from "$lib/dtos/signup-username-email.dto";
|
import { signupUsernameEmailDto } from "$lib/dtos/signup-username-email.dto";
|
||||||
import {limiter} from "$lib/server/api/middleware/rate-limiter.middleware";
|
import {limiter} from "$lib/server/api/middleware/rate-limiter.middleware";
|
||||||
|
import {UsersService} from "$lib/server/api/services/users.service";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SignupController implements Controller {
|
export class SignupController implements Controller {
|
||||||
controller = new Hono<HonoTypes>();
|
controller = new Hono<HonoTypes>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@inject(UsersService) private readonly usersService: UsersService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
routes() {
|
routes() {
|
||||||
return this.controller
|
return this.controller
|
||||||
.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||||
const { firstName, lastName, email, username, password } = await c.req.valid('json');
|
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({
|
// const existing_user = await db.query.usersTable.findFirst({
|
||||||
// where: eq(usersTable.username, form.data.username),
|
// where: eq(usersTable.username, form.data.username),
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ import { and, eq, type InferInsertModel } from "drizzle-orm";
|
||||||
import { credentialsTable, CredentialsType } from "../infrastructure/database/tables/credentials.table";
|
import { credentialsTable, CredentialsType } from "../infrastructure/database/tables/credentials.table";
|
||||||
import { db } from "../infrastructure/database";
|
import { db } from "../infrastructure/database";
|
||||||
import { takeFirstOrThrow } from "../infrastructure/database/utils";
|
import { takeFirstOrThrow } from "../infrastructure/database/utils";
|
||||||
|
import {injectable} from "tsyringe";
|
||||||
|
|
||||||
export type CreateCredentials = InferInsertModel<typeof credentialsTable>;
|
export type CreateCredentials = InferInsertModel<typeof credentialsTable>;
|
||||||
export type UpdateCredentials = Partial<CreateCredentials>;
|
export type UpdateCredentials = Partial<CreateCredentials>;
|
||||||
|
|
||||||
|
@injectable()
|
||||||
export class CredentialsRepository {
|
export class CredentialsRepository {
|
||||||
|
|
||||||
async findOneByUserId(userId: string) {
|
async findOneByUserId(userId: string) {
|
||||||
|
|
|
||||||
76
src/lib/server/api/repositories/roles.repository.ts
Normal file
76
src/lib/server/api/repositories/roles.repository.ts
Normal file
|
|
@ -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<typeof roles>;
|
||||||
|
export type UpdateRole = Partial<CreateRole>;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/lib/server/api/repositories/user_roles.repository.ts
Normal file
58
src/lib/server/api/repositories/user_roles.repository.ts
Normal file
|
|
@ -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<typeof user_roles>;
|
||||||
|
export type UpdateUserRole = Partial<CreateUserRole>;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { eq, type InferInsertModel } from 'drizzle-orm';
|
||||||
import { usersTable } from '../infrastructure/database/tables/users.table';
|
import { usersTable } from '../infrastructure/database/tables/users.table';
|
||||||
import { takeFirstOrThrow } from '../infrastructure/database/utils';
|
import { takeFirstOrThrow } from '../infrastructure/database/utils';
|
||||||
import { db } from '../infrastructure/database';
|
import { db } from '../infrastructure/database';
|
||||||
|
import {injectable} from "tsyringe";
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Repository */
|
/* Repository */
|
||||||
|
|
@ -22,6 +23,7 @@ storing data. They should not contain any business logic, only database queries.
|
||||||
export type CreateUser = InferInsertModel<typeof usersTable>;
|
export type CreateUser = InferInsertModel<typeof usersTable>;
|
||||||
export type UpdateUser = Partial<CreateUser>;
|
export type UpdateUser = Partial<CreateUser>;
|
||||||
|
|
||||||
|
@injectable()
|
||||||
export class UsersRepository {
|
export class UsersRepository {
|
||||||
async findOneById(id: string) {
|
async findOneById(id: string) {
|
||||||
return db.query.usersTable.findFirst({
|
return db.query.usersTable.findFirst({
|
||||||
|
|
@ -59,4 +61,12 @@ export class UsersRepository {
|
||||||
.returning()
|
.returning()
|
||||||
.then(takeFirstOrThrow);
|
.then(takeFirstOrThrow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
return db
|
||||||
|
.delete(usersTable)
|
||||||
|
.where(eq(usersTable.id, id))
|
||||||
|
.returning()
|
||||||
|
.then(takeFirstOrThrow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
src/lib/server/api/services/roles.service.ts
Normal file
14
src/lib/server/api/services/roles.service.ts
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,10 @@ export class TokensService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createHashedToken(token: string) {
|
||||||
|
return this.hashingService.hash(token)
|
||||||
|
}
|
||||||
|
|
||||||
async verifyHashedToken(hashedToken: string, token: string) {
|
async verifyHashedToken(hashedToken: string, token: string) {
|
||||||
return this.hashingService.verify(hashedToken, token)
|
return this.hashingService.verify(hashedToken, token)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
src/lib/server/api/services/user_roles.service.ts
Normal file
42
src/lib/server/api/services/user_roles.service.ts
Normal file
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,81 @@
|
||||||
import { inject, injectable } from 'tsyringe';
|
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()
|
@injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(
|
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) {
|
async findOneByUsername(username: string) {
|
||||||
return this.usersRepository.findOneByUsername(username);
|
return this.usersRepository.findOneByUsername(username);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue