Signup flow servers and repositories for user create.

This commit is contained in:
Bradley Shellnut 2024-08-10 10:03:30 -07:00
parent 2652d4fef6
commit 80b956b35c
11 changed files with 291 additions and 4 deletions

View file

@ -0,0 +1 @@

View file

@ -15,4 +15,6 @@ export const signupUsernameEmailDto = z.object({
})
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx);
});
});
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>

View file

@ -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<HonoTypes>();
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),

View file

@ -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<typeof credentialsTable>;
export type UpdateCredentials = Partial<CreateCredentials>;
@injectable()
export class CredentialsRepository {
async findOneByUserId(userId: string) {

View 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);
}
}

View 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);
}
}

View file

@ -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<typeof usersTable>;
export type UpdateUser = Partial<CreateUser>;
@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);
}
}

View 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);
}
}

View file

@ -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)
}

View 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,
});
}
}

View file

@ -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);
}