refactor project structure (backups)

This commit is contained in:
vergiLgood1 2025-03-15 22:49:23 +07:00
parent 5830eedb18
commit 693f5d265e
46 changed files with 2408 additions and 553 deletions

View File

@ -6,10 +6,10 @@ export default async function DashboardPage() {
const supabase = await createClient(); const supabase = await createClient();
const { const {
data: { user }, data: { session },
} = await supabase.auth.getUser(); } = await supabase.auth.getSession();
if (!user) { if (!session) {
return redirect("/sign-in"); return redirect("/sign-in");
} }
@ -20,7 +20,7 @@ export default async function DashboardPage() {
<div className="grid auto-rows-min gap-4 md:grid-cols-3"> <div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50"> <div className="aspect-video rounded-xl bg-muted/50">
<pre className="text-xs font-mono p-3 rounded border overflow-auto"> <pre className="text-xs font-mono p-3 rounded border overflow-auto">
{JSON.stringify(user, null, 2)} {JSON.stringify(session, null, 2)}
</pre> </pre>
</div> </div>

View File

@ -8,13 +8,33 @@ import { SubmitButton } from "@/app/_components/submit-button";
import Link from "next/link"; import Link from "next/link";
import { FormField } from "@/app/_components/form-field"; import { FormField } from "@/app/_components/form-field";
import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in-controller"; import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in-controller";
import { useState } from "react";
import { signIn } from "../action";
export function SignInForm({ export function SignInForm({
className, className,
...props ...props
}: React.ComponentPropsWithoutRef<"form">) { }: React.ComponentPropsWithoutRef<"form">) {
const { register, isPending, handleSubmit, errors } = useSignInController(); const [error, setError] = useState<string>();
const [loading, setLoading] = useState(false);
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (loading) return;
const formData = new FormData(event.currentTarget);
setLoading(true);
const res = await signIn(formData);
if (res && res.error) {
setError(res.error);
}
setLoading(false);
};
// const { register, isPending, handleSubmit, errors } = useSignInController();
return ( return (
<div> <div>
@ -41,7 +61,7 @@ export function SignInForm({
variant="outline" variant="outline"
className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700" className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700"
size="lg" size="lg"
disabled={isPending} disabled={loading}
> >
<Lock className="mr-2 h-5 w-5" /> <Lock className="mr-2 h-5 w-5" />
Continue with SSO Continue with SSO
@ -57,28 +77,28 @@ export function SignInForm({
</div> </div>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4" {...props} noValidate> <form onSubmit={onSubmit} className="space-y-4" {...props} noValidate>
<FormField <FormField
label="Email" label="Email"
input={ input={
<Input <Input
id="email" id="email"
type="email" type="email"
{...register("email")} name="email"
placeholder="you@example.com" placeholder="you@example.com"
className={`bg-[#1C1C1C] border-gray-800 ${errors.email ? "ring-red-500 focus-visible:ring-red-500" : "" className={`bg-[#1C1C1C] border-gray-800 ${error ? "ring-red-500 focus-visible:ring-red-500" : ""
}`} }`}
disabled={isPending} disabled={loading}
/> />
} }
error={errors.email ? errors.email.message : undefined} error={error ? error : undefined}
/> />
<Button <Button
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white" className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
size="lg" size="lg"
disabled={isPending} disabled={loading}
> >
{isPending ? ( {loading ? (
<> <>
<Loader2 className="h-5 w-5 animate-spin" /> <Loader2 className="h-5 w-5 animate-spin" />
Signing in... Signing in...

View File

@ -0,0 +1,119 @@
"use server";
import { redirect } from "next/navigation"
import { getInjection } from "@/di/container"
import { revalidatePath } from "next/cache"
import { InputParseError } from "@/src/entities/errors/common"
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth"
import { createClient } from "@/app/_utils/supabase/server"
export async function signIn(formData: FormData) {
const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("signIn", {
recordResponse: true
},
async () => {
try {
const email = formData.get("email")?.toString()
const signInController = getInjection("ISignInController")
await signInController({ email })
if (email) redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
} catch (err) {
if (
err instanceof InputParseError ||
err instanceof AuthenticationError
) {
return {
error: 'Incorrect credential. Please try again.',
};
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
return {
error:
'An error happened. The developers have been notified. Please try again later.',
};
}
})
}
// export async function signUp(formData: FormData) {
// const instrumentationService = getInjection("IInstrumentationService")
// return await instrumentationService.instrumentServerAction("signUp", { recordResponse: true }, async () => {
// try {
// const data = Object.fromEntries(formData.entries())
// const signUpController = getInjection("ISignUpController")
// await signUpController(data)
// } catch (err) {
// if (err instanceof InputParseError) {
// return { error: err.message, success: false }
// }
// const crashReporterService = getInjection("ICrashReporterService")
// crashReporterService.report(err)
// return {
// error: "An error occurred during sign up. Please try again later.",
// success: false,
// }
// }
// })
// }
export async function signOut() {
const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("signOut", {
recordResponse: true
}, async () => {
try {
const signOutController = getInjection("ISignOutController")
await signOutController()
revalidatePath("/")
redirect("/sign-in") // Updated to match your route
} catch (err) {
const crashReporterService = getInjection("ICrashReporterService")
crashReporterService.report(err)
return {
error: "An error occurred during sign out. Please try again later.",
success: false,
}
}
})
}
export async function verifyOtp(formData: FormData) {
const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("verifyOtp", {
recordResponse: true
}, async () => {
try {
const email = formData.get("email")?.toString()
const token = formData.get("token")?.toString()
const verifyOtpController = getInjection("IVerifyOtpController")
await verifyOtpController({ email, token })
redirect("/dashboard") // Updated to match your route
} catch (err) {
if (err instanceof InputParseError || err instanceof AuthenticationError) {
return { error: err.message, success: false }
}
const crashReporterService = getInjection("ICrashReporterService")
crashReporterService.report(err)
return {
error: "An error occurred during OTP verification. Please try again later.",
success: false,
}
}
})
}

View File

@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { checkSession } from "./_actions/session"; // import { checkSession } from "./_actions/session";
import { createClient } from "@/app/_utils/supabase/client"; import { createClient } from "@/app/_utils/supabase/client";
export default async function Layout({ export default async function Layout({

View File

@ -27,3 +27,5 @@ export const createClient = async () => {
}, },
); );
}; };

View File

@ -0,0 +1,26 @@
import { createContainer } from '@evyweb/ioctopus';
import { DI_RETURN_TYPES, DI_SYMBOLS } from '@/di/types';
import { IInstrumentationService } from '@/src/application/services/instrumentation.service.interface';
import { createAuthenticationModule } from './modules/authentication.module';
const ApplicationContainer = createContainer();
ApplicationContainer.load(Symbol('AuthenticationModule'), createAuthenticationModule());
export function getInjection<K extends keyof typeof DI_SYMBOLS>(
symbol: K
): DI_RETURN_TYPES[K] {
const instrumentationService =
ApplicationContainer.get<IInstrumentationService>(
DI_SYMBOLS.IInstrumentationService
);
return instrumentationService.startSpan(
{
name: '(di) getInjection',
op: 'function',
attributes: { symbol: symbol.toString() },
},
() => ApplicationContainer.get(DI_SYMBOLS[symbol])
);
}

View File

@ -0,0 +1,41 @@
import { createModule } from '@evyweb/ioctopus';
import { DI_SYMBOLS } from '@/di/types';
import { signInController, signOutController, verifyOtpController } from '@/src/interface-adapters/controllers/auth/authentication-controller';
import { AuthenticationService } from '@/src/infrastructure/services/authentication.service';
import { IInstrumentationServiceImpl } from '@/src/application/services/instrumentation.service.interface';
export function createAuthenticationModule() {
const authenticationModule = createModule();
authenticationModule
.bind(DI_SYMBOLS.IAuthenticationService)
.toClass(AuthenticationService, [
DI_SYMBOLS.IUsersRepository,
DI_SYMBOLS.IInstrumentationService,
]);
// Rest of your bindings remain the same
authenticationModule
.bind(DI_SYMBOLS.ISignInController)
.toHigherOrderFunction(signInController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ISignInUseCase,
]);
authenticationModule
.bind(DI_SYMBOLS.IVerifyOtpController)
.toHigherOrderFunction(verifyOtpController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IVerifyOtpUseCase,
]);
authenticationModule
.bind(DI_SYMBOLS.ISignOutController)
.toHigherOrderFunction(signOutController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ISignOutUseCase,
]);
return authenticationModule;
}

View File

@ -0,0 +1,25 @@
import { createModule } from '@evyweb/ioctopus';
import { DI_SYMBOLS } from '@/di/types';
export function createMonitoringModule() {
const monitoringModule = createModule();
if (process.env.NODE_ENV === 'test') {
monitoringModule
.bind(DI_SYMBOLS.IInstrumentationService)
.toClass(MockInstrumentationService);
monitoringModule
.bind(DI_SYMBOLS.ICrashReporterService)
.toClass(MockCrashReporterService);
} else {
monitoringModule
.bind(DI_SYMBOLS.IInstrumentationService)
.toClass(InstrumentationService);
monitoringModule
.bind(DI_SYMBOLS.ICrashReporterService)
.toClass(CrashReporterService);
}
return monitoringModule;
}

55
sigap-website/di/types.ts Normal file
View File

@ -0,0 +1,55 @@
import { IAuthenticationService } from '@/src/application/services/authentication.service.interface';
import { ITransactionManagerService } from '@/src/application/services/transaction-manager.service.interface';
import { IInstrumentationService } from '@/src/application/services/instrumentation.service.interface';
import { ICrashReporterService } from '@/src/application/services/crash-reporter.service.interface';
import { ISignInUseCase } from '@/src/application/use-cases/auth/sign-in.use-case';
import { ISignUpUseCase } from '@/src/application/use-cases/auth/sign-up.use-case';
import { ISignOutUseCase } from '@/src/application/use-cases/auth/sign-out.use-case';
import { IUsersRepository } from '@/src/application/repositories/users.repository';
import { ISignInController, ISignOutController, IVerifyOtpController } from '@/src/interface-adapters/controllers/auth/authentication-controller';
import { IVerifyOtpUseCase } from '@/src/application/use-cases/auth/verify-otp.use-case';
import { IInviteUserUseCase } from '@/src/application/use-cases/users/invite-user.use-case';
export const DI_SYMBOLS = {
// Services
IAuthenticationService: Symbol.for('IAuthenticationService'),
ITransactionManagerService: Symbol.for('ITransactionManagerService'),
IInstrumentationService: Symbol.for('IInstrumentationService'),
ICrashReporterService: Symbol.for('ICrashReporterService'),
// Repositories
IUsersRepository: Symbol.for('IUsersRepository'),
// Use Cases
ISignInUseCase: Symbol.for('ISignInUseCase'),
ISignOutUseCase: Symbol.for('ISignOutUseCase'),
IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'),
// Controllers
ISignInController: Symbol.for('ISignInController'),
ISignOutController: Symbol.for('ISignOutController'),
IVerifyOtpController: Symbol.for('IVerifyOtpController'),
};
export interface DI_RETURN_TYPES {
// Services
IAuthenticationService: IAuthenticationService;
ITransactionManagerService: ITransactionManagerService;
IInstrumentationService: IInstrumentationService;
ICrashReporterService: ICrashReporterService;
// Repositories
IUsersRepository: IUsersRepository;
// Use Cases
ISignInUseCase: ISignInUseCase;
ISignOutUseCase: ISignOutUseCase;
IVerifyOtpUseCase: IVerifyOtpUseCase;
// Controllers
ISignInController: ISignInController;
ISignOutController: ISignOutController;
IVerifyOtpController: IVerifyOtpController;
}

View File

@ -5,6 +5,7 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@evyweb/ioctopus": "^1.2.0",
"@hookform/resolvers": "^4.1.2", "@hookform/resolvers": "^4.1.2",
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",
@ -1054,6 +1055,12 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@evyweb/ioctopus": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@evyweb/ioctopus/-/ioctopus-1.2.0.tgz",
"integrity": "sha512-OIISYUx7WZDm6uxQkVsKmNF13tEiA3gbUeboTkr4LUTmJffhSVswiWAs8Ng5DoyvUlmgteTYcHP5XzOtrPTxLw==",
"license": "MIT"
},
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.6.9", "version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",

View File

@ -10,6 +10,7 @@
"seed": "ts-node prisma/seed.ts" "seed": "ts-node prisma/seed.ts"
}, },
"dependencies": { "dependencies": {
"@evyweb/ioctopus": "^1.2.0",
"@hookform/resolvers": "^4.1.2", "@hookform/resolvers": "^4.1.2",
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",

View File

@ -1,7 +1,11 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
const prismaClientSingleton = () => { const prismaClientSingleton = () => {
return new PrismaClient(); return new PrismaClient({
log: [
"query",
]
});
}; };
declare const globalThis: { declare const globalThis: {
@ -16,3 +20,4 @@ if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;
export type Transaction = PrismaClient['$transaction']; export type Transaction = PrismaClient['$transaction'];

View File

@ -1,179 +1,6 @@
// // src/repositories/auth.repository.ts
// "use server";
// import { createClient } from "@/app/_utils/supabase/server";
// import { SignInFormData } from "@/src/entities/models/auth/sign-in.model";
// import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
// import { AuthenticationError } from "@/src/entities/errors/auth";
// import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
// import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
// import { createAdminClient } from "@/app/_utils/supabase/admin";
// import { DatabaseOperationError } from "@/src/entities/errors/common";
// export class AuthRepository {
// private static instance: AuthRepository;
// private constructor(
// private readonly instrumentationService: IInstrumentationService,
// private readonly crashReporterService: ICrashReporterService,
// private readonly supabaseAdmin = createAdminClient(),
// private readonly supabaseServer = createClient()
// ) { }
// // Method untuk mendapatkan singleton instance
// public static getInstance(
// instrumentationService: IInstrumentationService,
// crashReporterService: ICrashReporterService
// ): AuthRepository {
// if (!AuthRepository.instance) {
// AuthRepository.instance = new AuthRepository(instrumentationService, crashReporterService);
// }
// return AuthRepository.instance;
// }
// async signIn({ email }: SignInFormData) {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > signIn",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = await this.supabaseServer;
// const { data, error } = await supabase.auth.signInWithOtp({
// email,
// options: {
// shouldCreateUser: false,
// },
// });
// if (error) {
// console.error("Error signing in:", error);
// throw new AuthenticationError(error.message);
// }
// return {
// data,
// redirectTo: `/verify-otp?email=${encodeURIComponent(email)}`,
// };
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// });
// }
// async verifyOtp({ email, token }: VerifyOtpFormData) {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > verifyOtp",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = await this.supabaseServer;
// const { data, error } = await supabase.auth.verifyOtp({
// email,
// token,
// type: "email",
// });
// if (error) {
// console.error("Error verifying OTP:", error);
// throw new AuthenticationError(error.message);
// }
// return {
// data,
// redirectTo: "/dashboard",
// };
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// });
// }
// async signOut() {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > signOut",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = await this.supabaseServer;
// const { error } = await supabase.auth.signOut();
// if (error) {
// console.error("Error signing out:", error);
// throw new AuthenticationError(error.message);
// }
// return {
// success: true,
// redirectTo: "/",
// };
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// });
// }
// async sendPasswordRecovery(email: string): Promise<void> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > sendPasswordRecovery",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = this.supabaseAdmin;
// const { error } = await supabase.auth.resetPasswordForEmail(email, {
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
// });
// if (error) {
// console.error("Error sending password recovery:", error);
// throw new DatabaseOperationError(error.message);
// }
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// });
// }
// async sendMagicLink(email: string): Promise<void> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > sendMagicLink",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = this.supabaseAdmin;
// const { error } = await supabase.auth.signInWithOtp({
// email,
// options: {
// emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
// },
// });
// if (error) {
// console.error("Error sending magic link:", error);
// throw new DatabaseOperationError(error.message);
// }
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// });
// }
// }
// src/app/_actions/auth.actions.ts
"use server"; "use server";
import { createClient } from "@/app/_utils/supabase/server"; import { createClient as createServerClient } from "@/app/_utils/supabase/server";
import { SignInFormData } from "@/src/entities/models/auth/sign-in.model"; import { SignInFormData } from "@/src/entities/models/auth/sign-in.model";
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"; import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
import { AuthenticationError } from "@/src/entities/errors/auth"; import { AuthenticationError } from "@/src/entities/errors/auth";
@ -182,6 +9,9 @@ import { createAdminClient } from "@/app/_utils/supabase/admin";
import { IInstrumentationServiceImpl } from "@/src/application/services/instrumentation.service.interface"; import { IInstrumentationServiceImpl } from "@/src/application/services/instrumentation.service.interface";
import { ICrashReporterServiceImpl } from "@/src/application/services/crash-reporter.service.interface"; import { ICrashReporterServiceImpl } from "@/src/application/services/crash-reporter.service.interface";
let supabaseAdmin = createAdminClient();
let supabaseServer = createServerClient();
// Server actions for authentication // Server actions for authentication
export async function signIn({ email }: SignInFormData) { export async function signIn({ email }: SignInFormData) {
return await IInstrumentationServiceImpl.instrumentServerAction( return await IInstrumentationServiceImpl.instrumentServerAction(
@ -189,7 +19,7 @@ export async function signIn({ email }: SignInFormData) {
{ email }, { email },
async () => { async () => {
try { try {
const supabase = await createClient(); const supabase = await supabaseServer;
const { data, error } = await supabase.auth.signInWithOtp({ const { data, error } = await supabase.auth.signInWithOtp({
email, email,
options: { options: {
@ -225,7 +55,7 @@ export async function verifyOtp({ email, token }: VerifyOtpFormData) {
{ email }, { email },
async () => { async () => {
try { try {
const supabase = await createClient(); const supabase = await supabaseServer;
const { data, error } = await supabase.auth.verifyOtp({ const { data, error } = await supabase.auth.verifyOtp({
email, email,
token, token,
@ -260,7 +90,7 @@ export async function signOut() {
{}, {},
async () => { async () => {
try { try {
const supabase = await createClient(); const supabase = await supabaseServer;
const { error } = await supabase.auth.signOut(); const { error } = await supabase.auth.signOut();
if (error) { if (error) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
import { AuthResult } from "@/src/entities/models/auth/auth-result.model"
import { Session } from "@/src/entities/models/auth/session.model"
import { SignInFormData, SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model"
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model"
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"
import { User } from "@/src/entities/models/users/users.model"
export interface IAuthenticationService {
signInPasswordless(credentials: SignInPasswordless): Promise<void>
signInWithPassword(credentials: SignInWithPassword): Promise<void>
signUpWithEmail(credentials: SignUpWithEmail): Promise<User>
signUpWithPhone(credentials: SignUpWithPhone): Promise<User>
getSession(): Promise<Session | null>
signOut(): Promise<void>
sendMagicLink(email: string): Promise<void>
sendPasswordRecovery(email: string): Promise<void>
verifyOtp(credentials: VerifyOtpFormData): Promise<void>
}

View File

@ -12,11 +12,16 @@ export interface IInstrumentationService {
class InstrumentationService implements IInstrumentationService { class InstrumentationService implements IInstrumentationService {
startSpan<T>( startSpan<T>(
options: { name: string; op?: string; attributes?: Record<string, any> }, spanAttributes: { name: string; op: string; attributes: Record<string, string> },
callback: () => T callback: () => T
): T { ): T {
// Implementation of the startSpan method // Your implementation here
console.log(`Starting span: ${spanAttributes.name}`);
try {
return callback(); return callback();
} finally {
console.log(`Ending span: ${spanAttributes.name}`);
}
} }
async instrumentServerAction<T>( async instrumentServerAction<T>(

View File

@ -0,0 +1,25 @@
import { AuthenticationError } from "@/src/entities/errors/auth"
import { IUsersRepository } from "../../repositories/users.repository"
import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { NotFoundError } from "@/src/entities/errors/common"
export type ISendMagicLinkUseCase = ReturnType<typeof sendMagicLinkUseCase>
export const sendMagicLinkUseCase = (
instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService,
usersRepository: IUsersRepository
) => async (input: { email: string }): Promise<void> => {
return await instrumentationService.startSpan({ name: "sendMagicLink Use Case", op: "function" },
async () => {
const user = await usersRepository.getUserByEmail(input.email)
if (!user) {
throw new NotFoundError("User not found")
}
await authenticationService.sendMagicLink(input.email)
}
)
}

View File

@ -0,0 +1,25 @@
import { AuthenticationError } from "@/src/entities/errors/auth"
import { IUsersRepository } from "../../repositories/users.repository"
import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { NotFoundError } from "@/src/entities/errors/common"
export type ISendPasswordRecoveryUseCase = ReturnType<typeof sendPasswordRecoveryUseCase>
export const sendPasswordRecoveryUseCase = (
instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService,
usersRepository: IUsersRepository
) => async (input: { email: string }): Promise<void> => {
return await instrumentationService.startSpan({ name: "sendPasswordRecovery Use Case", op: "function" },
async () => {
const user = await usersRepository.getUserByEmail(input.email)
if (!user) {
throw new NotFoundError("User not found")
}
await authenticationService.sendPasswordRecovery(input.email)
}
)
}

View File

@ -0,0 +1,43 @@
import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { AuthenticationError } from "@/src/entities/errors/auth";
import { InputParseError, NotFoundError } from "@/src/entities/errors/common";
import { type SignInFormData, SignInPasswordless, SignInSchema } from "@/src/entities/models/auth/sign-in.model"
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
import { User } from "@/src/entities/models/users/users.model";
import { Session } from "@/src/entities/models/auth/session.model";
import { IUsersRepository } from "../../repositories/users.repository";
import { AuthResult } from "@/src/entities/models/auth/auth-result.model";
export type ISignInUseCase = ReturnType<typeof signInUseCase>
export const signInUseCase =
(
instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService,
usersRepository: IUsersRepository
) =>
async (input: SignInPasswordless): Promise<void> => {
return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByEmail(input.email)
if (!existingUser) {
throw new NotFoundError("User does not exist")
}
// Attempt to sign in
await authenticationService.signInPasswordless({
email: input.email
})
const session = await authenticationService.getSession();
if (!session) {
throw new NotFoundError("Session not found")
}
return
}
)
}

View File

@ -0,0 +1,15 @@
import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
export type ISignOutUseCase = ReturnType<typeof signOutUseCase>
export const signOutUseCase = (
instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService
) => async (): Promise<void> => {
return await instrumentationService.startSpan({ name: "signOut Use Case", op: "function" },
async () => {
await authenticationService.signOut()
}
)
}

View File

@ -0,0 +1,47 @@
import { CreateUser, User } from "@/src/entities/models/users/users.model"
import { IUsersRepository } from "../../repositories/users.repository"
import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { AuthenticationError } from "@/src/entities/errors/auth"
import { SignUpFormData } from "@/src/entities/models/auth/sign-up.model"
import { AuthResult } from "@/src/entities/models/auth/auth-result.model"
export type ISignUpUseCase = ReturnType<typeof signUpUseCase>
export const signUpUseCase = (
instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService,
usersRepository: IUsersRepository
) => async (input: SignUpFormData): Promise<User> => {
return await instrumentationService.startSpan({ name: "signUp Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByEmail(input.email)
if (existingUser) {
throw new AuthenticationError("User already exists")
}
const newUser = await authenticationService.signUpWithEmail({
email: input.email,
password: input.password
})
await authenticationService.signInWithPassword({
email: input.email,
password: input.password
})
const session = await authenticationService.getSession();
if (!session) {
throw new AuthenticationError("Session not found")
}
return {
...newUser
}
}
)
}

View File

@ -0,0 +1,30 @@
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"
import { IUsersRepository } from "../../repositories/users.repository"
import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { AuthenticationError } from "@/src/entities/errors/auth"
import { NotFoundError } from "@/src/entities/errors/common"
export type IVerifyOtpUseCase = ReturnType<typeof verifyOtpUseCase>
export const verifyOtpUseCase = (
instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService,
usersRepository: IUsersRepository
) => async (input: VerifyOtpFormData): Promise<void> => {
return await instrumentationService.startSpan({ name: "verifyOtp Use Case", op: "function" },
async () => {
const user = await usersRepository.getUserByEmail(input.email)
if (!user) {
throw new NotFoundError("User not found")
}
await authenticationService.verifyOtp({
email: input.email,
token: input.token
})
}
)
}

View File

@ -0,0 +1,27 @@
import { User } from "@/src/entities/models/users/users.model"
import { IUsersRepository } from "../../repositories/users.repository"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { NotFoundError } from "@/src/entities/errors/common"
export type IBanUserUseCase = ReturnType<typeof banUserUseCase>
export const banUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (id: string, ban_duration: string): Promise<User> => {
return await instrumentationService.startSpan({ name: "banUser Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserById(id)
if (!existingUser) {
throw new NotFoundError("User not found")
}
const bannedUser = await usersRepository.banUser(id, ban_duration)
return {
...bannedUser
}
}
)
}

View File

@ -0,0 +1,38 @@
import { AuthenticationError } from "@/src/entities/errors/auth"
import { IUsersRepository } from "../../repositories/users.repository"
import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { CreateUser, User } from "@/src/entities/models/users/users.model"
import { InputParseError } from "@/src/entities/errors/common"
export type ICreateUserUseCase = ReturnType<typeof createUserUseCase>
export const createUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository,
) => async (input: CreateUser): Promise<User> => {
return await instrumentationService.startSpan({ name: "createUser Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByEmail(input.email)
if (existingUser) {
throw new AuthenticationError("User already exists")
}
const newUser = await usersRepository.createUser({
email: input.email,
password: input.password,
email_confirm: true
})
if (!newUser) {
throw new InputParseError("User not created")
}
return {
...newUser
};
}
)
}

View File

@ -0,0 +1,15 @@
import { IUsersRepository } from "../../repositories/users.repository"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
export type IDeleteUserUseCase = ReturnType<typeof deleteUserUseCase>
const deleteUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (id: string): Promise<void> => {
return await instrumentationService.startSpan({ name: "deleteUser Use Case", op: "function" },
async () => {
await usersRepository.deleteUser(id)
}
)
}

View File

@ -0,0 +1,27 @@
import { NotFoundError } from "@/src/entities/errors/common"
import { IUsersRepository } from "../../repositories/users.repository"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { UserResponse } from "@/src/entities/models/users/users.model"
export type IGetCurrentUserUseCase = ReturnType<typeof getCurrentUserUseCase>
export const getCurrentUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (): Promise<UserResponse> => {
return await instrumentationService.startSpan({ name: "getCurrentUser Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getCurrentUser()
if (!existingUser) {
throw new NotFoundError("User not found")
}
return {
...existingUser
}
}
)
}

View File

@ -0,0 +1,16 @@
import { User } from "@/src/entities/models/users/users.model"
import { IUsersRepository } from "../../repositories/users.repository"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
export type IGetListUsersUseCase = ReturnType<typeof getListUsersUseCase>
export const getListUsersUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (): Promise<User[]> => {
return await instrumentationService.startSpan({ name: "getListUsers Use Case", op: "function" },
async () => {
return await usersRepository.listUsers()
}
)
}

View File

@ -0,0 +1,26 @@
import { User } from "@/src/entities/models/users/users.model"
import { IUsersRepository } from "../../repositories/users.repository"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { NotFoundError } from "@/src/entities/errors/common"
export type IGetUserByEmailUseCase = ReturnType<typeof getUserByEmailUseCase>
const getUserByEmailUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (email: string): Promise<User> => {
return await instrumentationService.startSpan({ name: "getUserByEmail Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByEmail(email)
if (!existingUser) {
throw new NotFoundError("User not found")
}
return {
...existingUser
}
}
)
}

View File

@ -0,0 +1,28 @@
import { User } from "@/src/entities/models/users/users.model"
import { IUsersRepository } from "../../repositories/users.repository"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { AuthenticationError } from "@/src/entities/errors/auth"
import { NotFoundError } from "@/src/entities/errors/common"
export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase>
export const getUserByIdUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (id: string): Promise<User> => {
return await instrumentationService.startSpan({ name: "getUserById Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserById(id)
if (!existingUser) {
throw new NotFoundError("User not found")
}
return {
...existingUser
}
}
)
}

View File

@ -0,0 +1,26 @@
import { NotFoundError } from "@/src/entities/errors/common"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { IUsersRepository } from "../../repositories/users.repository"
import { User } from "@/src/entities/models/users/users.model"
export type IGetUserByUsernameUseCase = ReturnType<typeof getUserByUsernameUseCase>
const getUserByUsernameUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (username: string): Promise<User> => {
return await instrumentationService.startSpan({ name: "getUserByUsername Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByUsername(username)
if (!existingUser) {
throw new NotFoundError("User not found")
}
return {
...existingUser
}
}
)
}

View File

@ -0,0 +1,34 @@
import { AuthenticationError } from "@/src/entities/errors/auth"
import { IUsersRepository } from "../../repositories/users.repository"
import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { User } from "@/src/entities/models/users/users.model"
export type IInviteUserUseCase = ReturnType<typeof inviteUserUseCase>
export const inviteUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository,
authenticationService: IAuthenticationService,
) => async (input: { email: string }): Promise<User> => {
return await instrumentationService.startSpan({ name: "inviteUser Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByEmail(input.email)
if (existingUser) {
throw new AuthenticationError("User already exists")
}
const newUser = await usersRepository.inviteUser(input.email)
if (!newUser) {
throw new AuthenticationError("User not invited")
}
return {
...newUser
}
}
)
}

View File

@ -0,0 +1,28 @@
import { UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model"
import { IUsersRepository } from "../../repositories/users.repository"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { NotFoundError } from "@/src/entities/errors/common"
export type IUpdateUserUseCase = ReturnType<typeof updateUserUseCase>
const updateUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (id: string, input: UpdateUser): Promise<UserResponse> => {
return await instrumentationService.startSpan({ name: "updateUser Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserById(id)
if (!existingUser) {
throw new NotFoundError("User not found")
}
const updatedUser = await usersRepository.updateUser(id, input)
return {
...updatedUser
}
}
)
}

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
import { Session } from '@/src/entities/models/auth/session.model';
export interface AuthResult {
data: {
user: null;
session: Session | null;
messageId?: string | null;
} | undefined;
}

View File

@ -0,0 +1,13 @@
import { z } from "zod";
import { UserSchema } from "@/src/entities/models/users/users.model";
export const SessionSchema = z.object({
user: UserSchema.pick({
id: true,
email: true,
role: true,
}),
expiresAt: z.number().optional(),
});
export type Session = z.infer<typeof SessionSchema>

View File

@ -6,12 +6,43 @@ export const SignInSchema = z.object({
.string() .string()
.min(1, { message: "Email is required" }) .min(1, { message: "Email is required" })
.email({ message: "Please enter a valid email address" }), .email({ message: "Please enter a valid email address" }),
password: z.string().min(1, { message: "Password is required" }),
phone: z.string().optional(),
}); });
// Export the type derived from the schema // Export the type derived from the schema
export type SignInFormData = z.infer<typeof SignInSchema>; export type SignInFormData = z.infer<typeof SignInSchema>;
export const SignInWithPassword = SignInSchema.pick({
email: true,
password: true,
phone: true
})
// Default values for the form // Default values for the form
export const defaultSignInValues: SignInFormData = { export const defaultSignInWithPasswordValues: SignInWithPassword = {
email: "", email: "",
password: "",
phone: ""
}; };
export type SignInWithPassword = z.infer<typeof SignInWithPassword>
export const SignInPasswordless = SignInSchema.pick({
email: true,
})
// Default values for the form
export const defaultSignInPasswordlessValues: SignInPasswordless = {
email: "",
}
export type SignInPasswordless = z.infer<typeof SignInPasswordless>
// Define the sign-in response schema using Zod
export const SignInResponseSchema = z.object({
success: z.boolean(),
message: z.string(),
redirectTo: z.string().optional(),
});

View File

@ -0,0 +1,36 @@
import { z } from "zod";
export const SignUpSchema = z.object({
email: z
.string()
.min(1, { message: "Email is required" })
.email({ message: "Please enter a valid email address" }),
password: z.string().min(1, { message: "Password is required" }),
phone: z.string().optional(),
})
export type SignUpFormData = z.infer<typeof SignUpSchema>;
export const SignUpWithEmail = SignUpSchema.pick({
email: true,
password: true,
})
export const defaultSignUpWithEmailValues: SignUpWithEmail = {
email: "",
password: "",
}
export type SignUpWithEmail = z.infer<typeof SignUpWithEmail>
export const SignUpWithPhone = SignUpSchema.pick({
phone: true,
password: true,
})
export const defaultSignUpWithPhoneValues: SignUpWithPhone = {
phone: "",
password: "",
}
export type SignUpWithPhone = z.infer<typeof SignUpWithPhone>

View File

@ -11,3 +11,4 @@ export const defaultVerifyOtpValues: VerifyOtpFormData = {
email: "", email: "",
token: "", token: "",
}; };

View File

@ -82,17 +82,16 @@ export const ProfileSchema = z.object({
export type Profile = z.infer<typeof ProfileSchema>; export type Profile = z.infer<typeof ProfileSchema>;
export const CreateUserParamsSchema = z.object({ export const CreateUserSchema = z.object({
email: z.string().email(), email: z.string().email(),
password: z.string(), password: z.string().min(8),
phone: z.string().optional(), phone: z.string().optional(),
user_metadata: z.record(z.any()).optional(),
email_confirm: z.boolean().optional(), email_confirm: z.boolean().optional(),
}); });
export type CreateUserParams = z.infer<typeof CreateUserParamsSchema>; export type CreateUser = z.infer<typeof CreateUserSchema>;
export const UpdateUserParamsSchema = z.object({ export const UpdateUserSchema = z.object({
email: z.string().email().optional(), email: z.string().email().optional(),
email_confirmed_at: z.boolean().optional(), email_confirmed_at: z.boolean().optional(),
encrypted_password: z.string().optional(), encrypted_password: z.string().optional(),
@ -120,17 +119,15 @@ export const UpdateUserParamsSchema = z.object({
address: z.any().optional(), address: z.any().optional(),
birth_date: z.date().optional(), birth_date: z.date().optional(),
}) })
.optional(),
}); });
export type UpdateUserParams = z.infer<typeof UpdateUserParamsSchema>; export type UpdateUser = z.infer<typeof UpdateUserSchema>;
export const InviteUserParamsSchema = z.object({ export const InviteUserSchema = z.object({
email: z.string().email(), email: z.string().email(),
}); });
export type InviteUserParams = z.infer<typeof InviteUserParamsSchema>; export type InviteUser = z.infer<typeof InviteUserSchema>;
export type UserResponse = export type UserResponse =
| { | {

View File

@ -0,0 +1,452 @@
import { IUsersRepository } from "@/src/application/repositories/users.repository";
import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { createAdminClient } from "@/app/_utils/supabase/admin";
import { createClient as createServerClient } from "@/app/_utils/supabase/server";
import { CreateUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
import { ITransaction } from "@/src/entities/models/transaction.interface";
import db from "@/prisma/db";
import { NotFoundError } from "@/src/entities/errors/common";
import { AuthenticationError } from "@/src/entities/errors/auth";
export class UsersRepository implements IUsersRepository {
constructor(
private readonly instrumentationService: IInstrumentationService,
private readonly crashReporterService: ICrashReporterService,
private readonly supabaseAdmin = createAdminClient(),
private readonly supabaseServer = createServerClient()
) { }
async listUsers(): Promise<User[]> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getUsers",
}, async () => {
try {
const query = db.users.findMany({
include: {
profile: true,
},
});
const users = await this.instrumentationService.startSpan({
name: `UsersRepository > getUsers > Prisma: db.users.findMany`,
op: "db:query",
attributes: { "system": "prisma" },
},
async () => {
return await query;
}
)
return users;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async getUserById(id: string): Promise<User | undefined> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserById",
}, async () => {
try {
const query = db.users.findUnique({
where: {
id,
},
include: {
profile: true,
},
})
const user = await this.instrumentationService.startSpan({
name: `UsersRepository > getUserById > Prisma: db.users.findUnique(${id})`,
op: "db:query",
attributes: { "system": "prisma" },
},
async () => {
return await query;
}
)
if (user)
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async getUserByUsername(username: string): Promise<User | undefined> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserByUsername",
}, async () => {
try {
const query = db.users.findFirst({
where: {
profile: {
username,
},
},
include: {
profile: true,
},
})
const user = await this.instrumentationService.startSpan({
name: `UsersRepository > getUserByUsername > Prisma: db.users.findFirst(${username})`,
op: "db:query",
attributes: { "system": "prisma" },
},
async () => {
return await query;
}
)
if (user)
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async getUserByEmail(email: string): Promise<User | undefined> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserByEmail",
}, async () => {
try {
const query = db.users.findUnique({
where: {
email,
},
include: {
profile: true,
},
})
const user = await this.instrumentationService.startSpan({
name: `UsersRepository > getUserByEmail > Prisma: db.users.findUnique(${email})`,
op: "db:query",
attributes: { "system": "prisma" },
},
async () => {
return await query;
}
)
if (user)
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async getCurrentUser(): Promise<UserResponse> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getCurrentUser",
}, async () => {
try {
const supabase = await this.supabaseServer;
const query = supabase.auth.getUser();
const user = await this.instrumentationService.startSpan({
name: "UsersRepository > getCurrentUser > supabase.auth.getUser",
op: "db:query",
attributes: { "system": "supabase.auth" },
},
async () => {
return await query;
}
)
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async createUser(input: CreateUser, tx?: ITransaction): Promise<User | null> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > createUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.createUser({
email: input.email,
password: input.password,
email_confirm: input.email_confirm,
})
const { data: { user } } = await this.instrumentationService.startSpan({
name: "UsersRepository > createUser > supabase.auth.admin.createUser",
op: "db:query",
attributes: { "system": "supabase.auth" },
},
async () => {
return await query;
}
)
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async inviteUser(email: string, tx?: ITransaction): Promise<User | null> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > inviteUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.inviteUserByEmail(email);
const { data: { user } } = await this.instrumentationService.startSpan({
name: "UsersRepository > inviteUser > supabase.auth.admin.inviteUserByEmail",
op: "db:query",
attributes: { "system": "supabase.auth" },
},
async () => {
return await query;
}
)
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<UserResponse> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > updateUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(id, {
email: input.email,
email_confirm: input.email_confirmed_at,
password: input.encrypted_password ?? undefined,
password_hash: input.encrypted_password ?? undefined,
phone: input.phone,
phone_confirm: input.phone_confirmed_at,
role: input.role,
user_metadata: input.user_metadata,
app_metadata: input.app_metadata,
});
const { data, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > updateUser > supabase.auth.updateUser",
op: "db:query",
attributes: { "system": "supabase.auth" },
},
async () => {
return await queryUpdateSupabaseUser;
}
)
if (error) {
throw new AuthenticationError(error.message);
}
const queryGetUser = db.users.findUnique({
where: {
id,
},
include: {
profile: true,
},
})
const user = await this.instrumentationService.startSpan({
name: "UsersRepository > updateUser > Prisma: db.users.update",
op: "db:query",
attributes: { "system": "prisma" },
},
async () => {
return await queryGetUser;
}
)
if (!user) {
throw new NotFoundError("User not found");
}
const queryUpdateUser = db.users.update({
where: {
id,
},
data: {
role: input.role || user.role,
invited_at: input.invited_at || user.invited_at,
confirmed_at: input.confirmed_at || user.confirmed_at,
last_sign_in_at: input.last_sign_in_at || user.last_sign_in_at,
is_anonymous: input.is_anonymous || user.is_anonymous,
created_at: input.created_at || user.created_at,
updated_at: input.updated_at || user.updated_at,
profile: {
update: {
avatar: input.profile?.avatar || user.profile?.avatar,
username: input.profile?.username || user.profile?.username,
first_name: input.profile?.first_name || user.profile?.first_name,
last_name: input.profile?.last_name || user.profile?.last_name,
bio: input.profile?.bio || user.profile?.bio,
address: input.profile?.address || user.profile?.address,
birth_date: input.profile?.birth_date || user.profile?.birth_date,
},
},
},
include: {
profile: true,
},
})
const updatedUser = await this.instrumentationService.startSpan({
name: "UsersRepository > updateUser > Prisma: db.users.update",
op: "db:query",
attributes: { "system": "prisma" },
},
async () => {
return await queryUpdateUser;
}
)
return {
data: {
user: {
...data.user,
role: updatedUser.role,
profile: {
user_id: id,
...updatedUser.profile,
},
},
},
error: null,
};
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async deleteUser(id: string, tx?: ITransaction): Promise<void> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.deleteUser(id);
await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser > supabase.auth.admin.deleteUser",
op: "db:query",
attributes: { "system": "supabase.auth" },
},
async () => {
return await query;
}
)
return;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > banUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.updateUserById(id, {
ban_duration: ban_duration ?? "100h",
})
const { data, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > banUser > supabase.auth.admin.updateUserById",
op: "db:query",
attributes: { "system": "supabase.auth" },
},
async () => {
return await query;
}
)
if (error) {
throw new AuthenticationError(error.message);
}
return data.user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async unbanUser(id: string, tx?: ITransaction): Promise<User> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > unbanUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.updateUserById(id, {
ban_duration: "none",
})
const { data, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > unbanUser > supabase.auth.admin.updateUserById",
op: "db:query",
attributes: { "system": "supabase.auth" },
},
async () => {
return await query;
}
)
if (error) {
throw new AuthenticationError(error.message);
}
return data.user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
}

View File

@ -0,0 +1,273 @@
import { createAdminClient } from "@/app/_utils/supabase/admin";
import { createClient } from "@/app/_utils/supabase/server";
import db from "@/prisma/db";
import { IUsersRepository } from "@/src/application/repositories/users.repository";
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { AuthenticationError } from "@/src/entities/errors/auth";
import { Session } from "@/src/entities/models/auth/session.model";
import { SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model";
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model";
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
import { User } from "@/src/entities/models/users/users.model";
export class AuthenticationService implements IAuthenticationService {
constructor(
private readonly usersRepository: IUsersRepository,
private readonly instrumentationService: IInstrumentationService,
private readonly crashReporterService: ICrashReporterService,
private readonly supabaseAdmin = createAdminClient(),
private readonly supabaseServer = createClient()
) { }
async signInPasswordless(credentials: SignInPasswordless): Promise<void> {
return await this.instrumentationService.startSpan({
name: "signInPasswordless Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const { email } = credentials
const signIn = supabase.auth.signInWithOtp({ email })
const { data: { session }, error } = await this.instrumentationService.startSpan({
name: "supabase.auth.signInWithOtp",
op: "db:query",
attributes: { "system": "supabase.auth" }
}, async () => {
return await signIn
})
return
} catch (err) {
this.crashReporterService.report(err)
throw err
}
})
}
async signInWithPassword(credentials: SignInWithPassword): Promise<void> {
return await this.instrumentationService.startSpan({
name: "signInWithPassword Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const { email, password } = credentials
const signIn = supabase.auth.signInWithPassword({ email, password })
const { data: { session }, error } = await this.instrumentationService.startSpan({
name: "supabase.auth.signIn",
op: "db:query",
attributes: { "system": "supabase.auth" }
}, async () => {
return await signIn
})
return
} catch (err) {
this.crashReporterService.report(err)
throw err
}
})
}
async signUpWithEmail(credentials: SignUpWithEmail): Promise<User> {
return await this.instrumentationService.startSpan({
name: "signUpWithEmail Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const { email, password } = credentials
const signUp = supabase.auth.signUp({ email, password })
const { data: { user, session }, error } = await this.instrumentationService.startSpan({
name: "supabase.auth.signUp",
op: "db:query",
attributes: { "system": "supabase.auth" }
}, async () => {
return await signUp
})
const newUser = db.users.findUnique({
where: {
id: user!.id
},
include: {
profile: true
}
})
const userDetail = await this.instrumentationService.startSpan({
name: "db.users.findUnique",
op: "db:query",
attributes: { "system": "prisma" }
}, async () => {
return await newUser
})
return userDetail!;
} catch (err) {
this.crashReporterService.report(err)
throw err
}
})
}
async signUpWithPhone(credentials: SignUpWithPhone): Promise<User> {
throw new Error("Method not implemented.");
}
async getSession(): Promise<Session | null> {
return await this.instrumentationService.startSpan({
name: "getSession Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const session = supabase.auth.getSession()
const { data, error } = await this.instrumentationService.startSpan({
name: "supabase.auth.session",
op: "db:query",
attributes: { "system": "supabase.auth" }
}, async () => {
return await session
})
if (!data.session) {
throw new AuthenticationError("Session not found")
}
return {
user: {
id: data.session.user.id,
role: data.session.user.role,
email: data.session.user.email
},
expiresAt: data.session.expires_at,
};
} catch (err) {
this.crashReporterService.report(err)
throw err
}
})
}
async signOut(): Promise<void> {
return await this.instrumentationService.startSpan({
name: "signOut Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const signOut = supabase.auth.signOut()
await this.instrumentationService.startSpan({
name: "supabase.auth.signOut",
op: "db:query",
attributes: { "system": "supabase.auth" }
}, async () => {
return await signOut
})
return;
} catch (err) {
this.crashReporterService.report(err)
throw err
}
})
}
async sendMagicLink(email: string): Promise<void> {
return await this.instrumentationService.startSpan({
name: "sendMagicLink Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const magicLink = supabase.auth.signInWithOtp({ email })
await this.instrumentationService.startSpan({
name: "supabase.auth.signIn",
op: "db:query",
attributes: { "system": "supabase.auth" }
}, async () => {
return await magicLink
})
return;
} catch (err) {
this.crashReporterService.report(err)
throw err
}
})
}
async sendPasswordRecovery(email: string): Promise<void> {
return await this.instrumentationService.startSpan({
name: "sendPasswordRecovery Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const passwordRecovery = supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
})
await this.instrumentationService.startSpan({
name: "supabase.auth.resetPasswordForEmail",
op: "db:query",
attributes: { "system": "supabase.auth" }
}, async () => {
return await passwordRecovery
})
return;
} catch (err) {
this.crashReporterService.report(err)
throw err
}
})
}
async verifyOtp(credentials: VerifyOtpFormData): Promise<void> {
return await this.instrumentationService.startSpan({
name: "verifyOtp Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const { email, token } = credentials
const verifyOtp = supabase.auth.verifyOtp({ email, token, type: "email" })
await this.instrumentationService.startSpan({
name: "supabase.auth.verifyOtp",
op: "db:query",
attributes: { "system": "supabase.auth" }
}, async () => {
return await verifyOtp
})
return;
} catch (err) {
this.crashReporterService.report(err)
throw err
}
})
}
}

View File

@ -0,0 +1,78 @@
import { z } from "zod"
import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { InputParseError } from "@/src/entities/errors/common"
import { ISignInUseCase } from "@/src/application/use-cases/auth/sign-in.use-case"
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
import { ISignUpUseCase } from "@/src/application/use-cases/auth/sign-up.use-case"
import { IVerifyOtpUseCase } from "@/src/application/use-cases/auth/verify-otp.use-case"
// Sign In Controller
const signInInputSchema = z.object({
email: z.string().email("Please enter a valid email address"),
})
export type ISignInController = ReturnType<typeof signInController>
export const signInController =
(
instrumentationService: IInstrumentationService,
signInUseCase: ISignInUseCase
) =>
async (input: Partial<z.infer<typeof signInInputSchema>>) => {
return await instrumentationService.startSpan({ name: "signIn Controller" }, async () => {
const { data, error: inputParseError } = signInInputSchema.safeParse(input)
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError })
}
return await signInUseCase({
email: data.email
})
})
}
// Verify OTP Controller
const verifyOtpInputSchema = z.object({
email: z.string().email("Please enter a valid email address"),
token: z.string().min(6, "Please enter a valid OTP")
})
export type IVerifyOtpController = ReturnType<typeof verifyOtpController>
export const verifyOtpController =
(
instrumentationService: IInstrumentationService,
verifyOtpUseCase: IVerifyOtpUseCase
) =>
async (input: Partial<z.infer<typeof verifyOtpInputSchema>>) => {
return await instrumentationService.startSpan({ name: "verifyOtp Controller" }, async () => {
const { data, error: inputParseError } = verifyOtpInputSchema.safeParse(input)
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError })
}
return await verifyOtpUseCase({
email: data.email,
token: data.token
})
})
}
// Sign Out Controller
export type ISignOutController = ReturnType<typeof signOutController>
export const signOutController =
(
instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService
) =>
async () => {
return await instrumentationService.startSpan({
name: "signOut Controller"
}, async () => {
return await authenticationService.signOut()
})
}

View File

@ -2,7 +2,7 @@
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import {
defaultSignInValues, defaultSignInPasswordlessValues,
SignInFormData, SignInFormData,
SignInSchema, SignInSchema,
} from "@/src/entities/models/auth/sign-in.model"; } from "@/src/entities/models/auth/sign-in.model";
@ -173,7 +173,7 @@ export function useSignInController() {
formState: { errors }, formState: { errors },
} = useForm<SignInFormData>({ } = useForm<SignInFormData>({
resolver: zodResolver(SignInSchema), resolver: zodResolver(SignInSchema),
defaultValues: defaultSignInValues, defaultValues: defaultSignInPasswordlessValues,
}); });
// Handler untuk submit form // Handler untuk submit form