feat: initial commit

This commit is contained in:
2026-04-09 20:52:10 -03:00
commit 1a92cc6c11
86 changed files with 19533 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
export const SessionClaims = z.object({
user: z.object({
email: z.email(),
username: z.string(),
full_name: z.string(),
image_url: z.string().optional(),
public_metadata: z.object({
role: z.enum(['admin', 'internal', 'user']).default('user'),
}),
}),
});
export type SessionClaims = z.infer<typeof SessionClaims>;

View File

@@ -0,0 +1,49 @@
import { UserRole, UserRoleValues } from '@/lib/feature/user/user.model';
import type { UUIDv4 } from '@/utils/types/uuid';
import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity('users')
export class UserEntity {
@PrimaryColumn({
type: 'bigint',
primary: true,
generated: true,
})
id: string;
@Column({ type: 'text' })
name: string;
@Column({ type: 'text', unique: true })
email: string;
@Column({
type: 'enum',
enum: UserRoleValues,
enumName: 'user_role',
default: UserRole.USER,
})
role: UserRole;
@Column({
name: 'created_at',
type: 'timestamp with time zone',
default: () => 'now()',
})
createdAt: Date;
@Column({
name: 'updated_at',
type: 'timestamp with time zone',
default: () => 'now()',
})
updatedAt: Date;
@Column({
name: 'external_id',
type: 'uuid',
unique: true,
default: () => 'gen_random_uuid()',
})
externalId: UUIDv4;
}

View File

@@ -0,0 +1,30 @@
'use server';
import { getRepository } from '@/lib/db/client';
import { UserEntity } from '@/lib/db/entities';
import { userEntityToModel } from '@/lib/feature/user/user.service';
import { getSessionData } from '@/lib/session/session-storage';
import { UUIDv4 } from '@/utils/types/uuid';
export const getUserByExternalId = async (externalId: UUIDv4) => {
const sessionData = await getSessionData();
if (
!sessionData ||
!sessionData.user ||
sessionData.user.externalId !== externalId
) {
throw new Error('User not permitted to access this data');
}
const userRepository = await getRepository(UserEntity);
const userEntity = await userRepository.findOne({
where: { externalId: externalId },
});
if (!userEntity) {
return null;
}
return userEntityToModel(userEntity);
};

View File

@@ -0,0 +1,41 @@
import { z } from 'zod';
export const UserRole = {
ADMIN: 'admin',
INTERNAL: 'internal',
USER: 'user',
} as const;
export type UserRole = (typeof UserRole)[keyof typeof UserRole];
export const UserRoleValues: UserRole[] = Object.values(UserRole);
export const CreateUserModel = z.object({
name: z.string(),
email: z.email(),
role: z.enum(UserRoleValues),
});
export type CreateUserModel = z.infer<typeof CreateUserModel>;
export const UpdateUserModel = z.object({
name: z.string().optional(),
email: z.email().optional(),
role: z.enum(UserRoleValues).optional(),
});
export type UpdateUserModel = z.infer<typeof UpdateUserModel>;
export const UserModel = z.object({
id: z.string(),
name: z.string(),
email: z.email(),
active: z.boolean().optional(),
role: z.enum(UserRoleValues),
imageUrl: z.string().optional(),
externalId: z.uuid(),
});
export type UserModel = z.infer<typeof UserModel>;
export const UserInfoModel = z.object({
externalId: z.uuid(),
name: z.string(),
email: z.email(),
});
export type UserInfoModel = z.infer<typeof UserInfoModel>;

View File

@@ -0,0 +1,110 @@
import { getRepository } from '@/lib/db/client';
import { SessionClaims } from '@/lib/feature/user/clerk.model';
import { UserEntity } from '@/lib/feature/user/user.entity';
import {
CreateUserModel,
UpdateUserModel,
UserModel,
} from '@/lib/feature/user/user.model';
export const userEntityToModel = (userEntity: UserEntity): UserModel => {
return {
id: userEntity.id,
name: userEntity.name,
email: userEntity.email,
role: userEntity.role,
externalId: userEntity.externalId,
};
};
/**
* Retrieves a user by their email address.
* @param email - The email address of the user to retrieve.
* @returns {Promise<UserModel | null>} The user model if found, otherwise null.
*/
export const getUserByEmail = async (
email: string
): Promise<UserModel | null> => {
const userRepository = await getRepository(UserEntity);
const userEntity = await userRepository.findOneBy({ email });
if (!userEntity) {
return null;
}
return userEntityToModel(userEntity);
};
/**
* Saves a new user to the database.
* @param user - The user data to save.
* @returns {Promise<UserModel>} The saved user model.
* @throws {Error} If a user with the same email already exists.
*/
export const saveUser = async (user: CreateUserModel): Promise<UserModel> => {
const userRepository = await getRepository(UserEntity);
if (!!(await userRepository.findOneBy({ email: user.email }))) {
throw new Error(`User with email ${user.email} already exists`);
}
const newUser = userRepository.create(user);
return userEntityToModel(await userRepository.save(newUser));
};
/**
* Updates an existing user in the database.
* @param userId - The ID of the user to update.
* @param user - The new user data.
* @returns {Promise<UserModel>} The updated user model.
* @throws {Error} If the user with the given ID does not exist.
*/
export const updateUser = async (
userId: string,
user: UpdateUserModel
): Promise<UserModel> => {
const userRepository = await getRepository(UserEntity);
const existingUser = await userRepository.findOneBy({ id: userId });
if (!existingUser) {
throw new Error(`User with ID ${userId} not found`);
}
if (!!user.email) existingUser.email = user.email;
if (!!user.name) existingUser.name = user.name;
if (!!user.role) existingUser.role = user.role;
return userEntityToModel(await userRepository.save(existingUser));
};
/**
* Synchronizes a user with the database.
* If the user already exists, it skips saving and returns the existing user.
* If the user does not exist, it creates a new user record.
* @returns {Promise<UserModel>} The synchronized user model.
* @throws {Error} If the user email is not provided or if there is an issue
* saving the user.
* @param sessionClaims Session Claims from the Auth Provider
*/
export const syncUser = async (
sessionClaims: SessionClaims
): Promise<UserModel> => {
const { full_name, email } = sessionClaims.user;
const role = sessionClaims.user.public_metadata.role;
const existingUser = await getUserByEmail(email);
if (!existingUser) {
return await saveUser({
name: full_name,
email: email,
role: role,
});
}
return await updateUser(existingUser.id, {
name: full_name,
email: existingUser.email,
role: role,
});
};