feat: initial commit
This commit is contained in:
14
src/lib/feature/user/clerk.model.ts
Normal file
14
src/lib/feature/user/clerk.model.ts
Normal 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>;
|
||||
49
src/lib/feature/user/user.entity.ts
Normal file
49
src/lib/feature/user/user.entity.ts
Normal 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;
|
||||
}
|
||||
30
src/lib/feature/user/user.external.ts
Normal file
30
src/lib/feature/user/user.external.ts
Normal 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);
|
||||
};
|
||||
41
src/lib/feature/user/user.model.ts
Normal file
41
src/lib/feature/user/user.model.ts
Normal 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>;
|
||||
110
src/lib/feature/user/user.service.ts
Normal file
110
src/lib/feature/user/user.service.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user