diff --git a/sigap-website/prisma/db.ts b/sigap-website/prisma/db.ts
index 0e2ce72..96acbc4 100644
--- a/sigap-website/prisma/db.ts
+++ b/sigap-website/prisma/db.ts
@@ -13,3 +13,6 @@ const db = globalThis.prismaGlobal ?? prismaClientSingleton();
export default db;
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;
+
+
+export type Transaction = PrismaClient['$transaction'];
\ No newline at end of file
diff --git a/sigap-website/src/application/repositories/authentication.repository.ts b/sigap-website/src/application/repositories/authentication.repository.ts
index 3068ed6..86fbcca 100644
--- a/sigap-website/src/application/repositories/authentication.repository.ts
+++ b/sigap-website/src/application/repositories/authentication.repository.ts
@@ -1,65 +1,343 @@
-// src/repositories/auth.repository.ts
-import { createClient } from "@/app/_utils/supabase/server";
-import { SignInFormData } from "../entities/models/auth/sign-in.model";
-import { VerifyOtpFormData } from "../entities/models/auth/verify-otp.model";
+// // src/repositories/auth.repository.ts
+// "use server";
-export class AuthRepository {
- async signIn({ email }: SignInFormData) {
- const supabase = await createClient();
- const { data, error } = await supabase.auth.signInWithOtp({
- email,
- options: {
- shouldCreateUser: false,
- },
- });
-
- if (error) {
- throw new Error(error.message);
+// 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 {
+// 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 {
+// 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";
+
+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 { DatabaseOperationError } from "@/src/entities/errors/common";
+import { createAdminClient } from "@/app/_utils/supabase/admin";
+import { IInstrumentationServiceImpl } from "@/src/application/services/instrumentation.service.interface";
+import { ICrashReporterServiceImpl } from "@/src/application/services/crash-reporter.service.interface";
+
+// Server actions for authentication
+export async function signIn({ email }: SignInFormData) {
+ return await IInstrumentationServiceImpl.instrumentServerAction(
+ "auth.signIn",
+ { email },
+ async () => {
+ try {
+ const supabase = await createClient();
+ 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 {
+ success: true,
+ message: "Sign in email sent successfully",
+ data,
+ redirectTo: `/verify-otp?email=${encodeURIComponent(email)}`,
+ };
+ } catch (err) {
+ ICrashReporterServiceImpl.report(err);
+ if (err instanceof AuthenticationError) {
+ throw err;
+ }
+ throw new AuthenticationError("Failed to sign in. Please try again.");
+ }
}
-
- return {
- data,
- redirectTo: `/verify-otp?email=${encodeURIComponent(email)}`
- };
- }
-
- async signOut() {
- const supabase = await createClient();
- const { error } = await supabase.auth.signOut();
-
- if (error) {
- throw new Error(error.message);
- }
-
- return {
- success: true,
- redirectTo: "/"
- };
- }
-
- async getUser() {
- const supabase = await createClient();
- const { data: { user } } = await supabase.auth.getUser();
- return user;
- }
-
- async verifyOtp({ email, token }: VerifyOtpFormData) {
- const supabase = await createClient();
- const { data, error } = await supabase.auth.verifyOtp({
- email,
- token,
- type: "email",
- });
-
- if (error) {
- throw new Error(error.message);
- }
-
- return {
- data,
- redirectTo: "/dashboard",
- };
- }
+ );
}
-export const authRepository = new AuthRepository();
\ No newline at end of file
+export async function verifyOtp({ email, token }: VerifyOtpFormData) {
+ return await IInstrumentationServiceImpl.instrumentServerAction(
+ "auth.verifyOtp",
+ { email },
+ async () => {
+ try {
+ const supabase = await createClient();
+ 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 {
+ success: true,
+ message: "Successfully verified!",
+ data,
+ redirectTo: "/dashboard",
+ };
+ } catch (err) {
+ ICrashReporterServiceImpl.report(err);
+ if (err instanceof AuthenticationError) {
+ throw err;
+ }
+ throw new AuthenticationError("Failed to verify OTP. Please try again.");
+ }
+ }
+ );
+}
+
+export async function signOut() {
+ return await IInstrumentationServiceImpl.instrumentServerAction(
+ "auth.signOut",
+ {},
+ async () => {
+ try {
+ const supabase = await createClient();
+ const { error } = await supabase.auth.signOut();
+
+ if (error) {
+ console.error("Error signing out:", error);
+ throw new AuthenticationError(error.message);
+ }
+
+ return {
+ success: true,
+ message: "Sign out successful",
+ redirectTo: "/",
+ };
+ } catch (err) {
+ ICrashReporterServiceImpl.report(err);
+ throw new AuthenticationError("Failed to sign out. Please try again.");
+ }
+ }
+ );
+}
+
+export async function sendPasswordRecovery(email: string) {
+ return await IInstrumentationServiceImpl.instrumentServerAction(
+ "auth.sendPasswordRecovery",
+ { email },
+ async () => {
+ try {
+ const supabase = createAdminClient();
+
+ 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);
+ }
+
+ return {
+ success: true,
+ message: "Password recovery email sent successfully",
+ };
+ } catch (err) {
+ ICrashReporterServiceImpl.report(err);
+ throw new DatabaseOperationError("Failed to send password recovery email. Please try again.");
+ }
+ }
+ );
+}
+
+export async function sendMagicLink(email: string) {
+ return await IInstrumentationServiceImpl.instrumentServerAction(
+ "auth.sendMagicLink",
+ { email },
+ async () => {
+ try {
+ const supabase = createAdminClient();
+
+ 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);
+ }
+
+ return {
+ success: true,
+ message: "Magic link email sent successfully",
+ };
+ } catch (err) {
+ ICrashReporterServiceImpl.report(err);
+ throw new DatabaseOperationError("Failed to send magic link email. Please try again.");
+ }
+ }
+ );
+}
\ No newline at end of file
diff --git a/sigap-website/src/application/repositories/users.repository.ts b/sigap-website/src/application/repositories/users.repository.ts
index 0cc5508..f2a9658 100644
--- a/sigap-website/src/application/repositories/users.repository.ts
+++ b/sigap-website/src/application/repositories/users.repository.ts
@@ -2,313 +2,386 @@ import { createAdminClient } from "@/app/_utils/supabase/admin";
import { createClient } from "@/app/_utils/supabase/client";
import { CreateUserParams, InviteUserParams, UpdateUserParams, User, UserResponse } from "@/src/entities/models/users/users.model";
import db from "@/prisma/db";
-import { DatabaseOperationError, NotFoundError } from "../entities/errors/common";
-import { AuthenticationError } from "../entities/errors/auth";
+import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
+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";
export class UsersRepository {
- private supabaseAdmin = createAdminClient();
- private supabaseClient = createClient();
+ constructor(
+ private readonly instrumentationService: IInstrumentationService,
+ private readonly crashReporterService: ICrashReporterService,
+ private readonly supabaseAdmin = createAdminClient(),
+ private readonly supabaseClient = createClient()
+ ) { }
- async fetchUsers(): Promise {
- const users = await db.users.findMany({
- include: {
- profile: true,
- },
- });
+ async getUsers(): Promise {
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > getUsers",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
- if (!users) {
- throw new NotFoundError("Users not found");
- }
+ const users = await db.users.findMany({
+ include: {
+ profile: true,
+ },
+ });
- return users;
+ if (!users) {
+ throw new NotFoundError("Users not found");
+ }
+
+ return users;
+
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
+ }
+ })
}
async getCurrentUser(): Promise {
- const supabase = await this.supabaseClient;
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > getCurrentUser",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
+ const supabase = await this.supabaseClient;
- const {
- data: { user },
- error,
- } = await supabase.auth.getUser();
+ const {
+ data: { user },
+ error,
+ } = await supabase.auth.getUser();
- if (error) {
- console.error("Error fetching current user:", error);
- throw new AuthenticationError(error.message);
- }
+ if (error) {
+ console.error("Error fetching current user:", error);
+ throw new AuthenticationError(error.message);
+ }
- const userDetail = await db.users.findUnique({
- where: {
- id: user?.id,
- },
- include: {
- profile: true,
- },
- });
+ const userDetail = await db.users.findUnique({
+ where: {
+ id: user?.id,
+ },
+ include: {
+ profile: true,
+ },
+ });
- if (!userDetail) {
- throw new NotFoundError("User not found");
- }
+ if (!userDetail) {
+ throw new NotFoundError("User not found");
+ }
- return {
- data: {
- user: userDetail,
- },
- error: null,
- };
+ return {
+ data: {
+ user: userDetail,
+ },
+ error: null,
+ };
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
+ }
+ })
}
async createUser(params: CreateUserParams): Promise {
- const supabase = this.supabaseAdmin;
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > createUser",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
+ const supabase = this.supabaseAdmin;
- const { data, error } = await supabase.auth.admin.createUser({
- email: params.email,
- password: params.password,
- phone: params.phone,
- email_confirm: params.email_confirm,
- });
-
- if (error) {
- console.error("Error creating user:", error);
- throw new DatabaseOperationError(error.message);
- }
-
- return {
- data: {
- user: data.user,
- },
- error: null,
- };
- }
-
- async uploadAvatar(userId: string, email: string, file: File) {
- try {
- const supabase = await this.supabaseClient;
-
- const fileExt = file.name.split(".").pop();
- const emailName = email.split("@")[0];
- const fileName = `AVR-${emailName}.${fileExt}`;
-
- const filePath = `${userId}/${fileName}`;
-
- const { error: uploadError } = await supabase.storage
- .from("avatars")
- .upload(filePath, file, {
- upsert: true,
- contentType: file.type,
+ const { data, error } = await supabase.auth.admin.createUser({
+ email: params.email,
+ password: params.password,
+ phone: params.phone,
+ email_confirm: params.email_confirm,
});
- if (uploadError) {
- console.error("Error uploading avatar:", uploadError);
- throw new DatabaseOperationError(uploadError.message);
+ if (error) {
+ console.error("Error creating user:", error);
+ throw new DatabaseOperationError(error.message);
+ }
+
+ return {
+ data: {
+ user: data.user,
+ },
+ error: null,
+ };
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
}
-
- const {
- data: { publicUrl },
- } = supabase.storage.from("avatars").getPublicUrl(filePath);
-
- await db.users.update({
- where: {
- id: userId,
- },
- data: {
- profile: {
- update: {
- avatar: publicUrl,
- },
- },
- },
- });
-
- return publicUrl;
- } catch (error) {
- console.error("Error uploading avatar:", error);
- throw new DatabaseOperationError(error.message);
- }
- }
-
- async updateUser(userId: string, params: UpdateUserParams): Promise {
- const supabase = this.supabaseAdmin;
-
- const { data, error } = await supabase.auth.admin.updateUserById(userId, {
- email: params.email,
- email_confirm: params.email_confirmed_at,
- password: params.encrypted_password ?? undefined,
- password_hash: params.encrypted_password ?? undefined,
- phone: params.phone,
- phone_confirm: params.phone_confirmed_at,
- role: params.role,
- user_metadata: params.user_metadata,
- app_metadata: params.app_metadata,
- });
-
- if (error) {
- console.error("Error updating user:", error);
- throw new DatabaseOperationError(error.message);
- }
-
- const user = await db.users.findUnique({
- where: {
- id: userId,
- },
- include: {
- profile: true,
- },
- });
-
- if (!user) {
- throw new NotFoundError("User not found");
- }
-
- const updateUser = await db.users.update({
- where: {
- id: userId,
- },
- data: {
- role: params.role || user.role,
- invited_at: params.invited_at || user.invited_at,
- confirmed_at: params.confirmed_at || user.confirmed_at,
- last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
- is_anonymous: params.is_anonymous || user.is_anonymous,
- created_at: params.created_at || user.created_at,
- updated_at: params.updated_at || user.updated_at,
- profile: {
- update: {
- avatar: params.profile?.avatar || user.profile?.avatar,
- username: params.profile?.username || user.profile?.username,
- first_name: params.profile?.first_name || user.profile?.first_name,
- last_name: params.profile?.last_name || user.profile?.last_name,
- bio: params.profile?.bio || user.profile?.bio,
- address: params.profile?.address || user.profile?.address,
- birth_date: params.profile?.birth_date || user.profile?.birth_date,
- },
- },
- },
- include: {
- profile: true,
- },
- });
-
- return {
- data: {
- user: {
- ...data.user,
- role: params.role,
- profile: {
- user_id: userId,
- ...updateUser.profile,
- },
- },
- },
- error: null,
- };
- }
-
- async deleteUser(userId: string): Promise {
- const supabase = this.supabaseAdmin;
-
- const { error } = await supabase.auth.admin.deleteUser(userId);
-
- if (error) {
- console.error("Error deleting user:", error);
- throw new DatabaseOperationError(error.message);
- }
- }
-
- async sendPasswordRecovery(email: string): Promise {
- 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);
- }
- }
-
- async sendMagicLink(email: string): Promise {
- 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);
- }
- }
-
- async banUser(userId: string): Promise {
- const supabase = this.supabaseAdmin;
-
- const banUntil = new Date();
- banUntil.setFullYear(banUntil.getFullYear() + 100);
-
- const { data, error } = await supabase.auth.admin.updateUserById(userId, {
- ban_duration: "100h",
- });
-
- if (error) {
- console.error("Error banning user:", error);
- throw new DatabaseOperationError(error.message);
- }
-
- return {
- data: {
- user: data.user,
- },
- error: null,
- };
- }
-
- async unbanUser(userId: string): Promise {
- const supabase = this.supabaseAdmin;
-
- const { data, error } = await supabase.auth.admin.updateUserById(userId, {
- ban_duration: "none",
- });
-
- if (error) {
- console.error("Error unbanning user:", error);
- throw new DatabaseOperationError(error.message);
- }
-
- const user = await db.users.findUnique({
- where: {
- id: userId,
- },
- select: {
- banned_until: true,
- }
- });
-
- if (!user) {
- throw new NotFoundError("User not found");
- }
-
- return {
- data: {
- user: data.user,
- },
- error: null,
- };
+ })
}
async inviteUser(params: InviteUserParams): Promise {
- const supabase = this.supabaseAdmin;
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > inviteUser",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
+ const supabase = this.supabaseAdmin;
- const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
- redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`,
- });
+ const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
+ redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`,
+ });
- if (error) {
- console.error("Error inviting user:", error);
- throw new DatabaseOperationError(error.message);
- }
+ if (error) {
+ console.error("Error inviting user:", error);
+ throw new DatabaseOperationError(error.message);
+ }
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
+ }
+ })
+ }
+
+ async uploadAvatar(userId: string, email: string, file: File) {
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > uploadAvatar",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
+ const supabase = await this.supabaseClient;
+
+ const fileExt = file.name.split(".").pop();
+ const emailName = email.split("@")[0];
+ const fileName = `AVR-${emailName}.${fileExt}`;
+
+ const filePath = `${userId}/${fileName}`;
+
+ const { error: uploadError } = await supabase.storage
+ .from("avatars")
+ .upload(filePath, file, {
+ upsert: true,
+ contentType: file.type,
+ });
+
+ if (uploadError) {
+ console.error("Error uploading avatar:", uploadError);
+ throw new DatabaseOperationError(uploadError.message);
+ }
+
+ const {
+ data: { publicUrl },
+ } = supabase.storage.from("avatars").getPublicUrl(filePath);
+
+ await db.users.update({
+ where: {
+ id: userId,
+ },
+ data: {
+ profile: {
+ update: {
+ avatar: publicUrl,
+ },
+ },
+ },
+ });
+
+ return publicUrl;
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
+ }
+ })
+ }
+
+ async updateUser(userId: string, params: UpdateUserParams): Promise {
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > updateUser",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
+ const supabase = this.supabaseAdmin;
+
+ const { data, error } = await supabase.auth.admin.updateUserById(userId, {
+ email: params.email,
+ email_confirm: params.email_confirmed_at,
+ password: params.encrypted_password ?? undefined,
+ password_hash: params.encrypted_password ?? undefined,
+ phone: params.phone,
+ phone_confirm: params.phone_confirmed_at,
+ role: params.role,
+ user_metadata: params.user_metadata,
+ app_metadata: params.app_metadata,
+ });
+
+ if (error) {
+ console.error("Error updating user:", error);
+ throw new DatabaseOperationError(error.message);
+ }
+
+ const user = await db.users.findUnique({
+ where: {
+ id: userId,
+ },
+ include: {
+ profile: true,
+ },
+ });
+
+ if (!user) {
+ throw new NotFoundError("User not found");
+ }
+
+ const updateUser = await db.users.update({
+ where: {
+ id: userId,
+ },
+ data: {
+ role: params.role || user.role,
+ invited_at: params.invited_at || user.invited_at,
+ confirmed_at: params.confirmed_at || user.confirmed_at,
+ last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
+ is_anonymous: params.is_anonymous || user.is_anonymous,
+ created_at: params.created_at || user.created_at,
+ updated_at: params.updated_at || user.updated_at,
+ profile: {
+ update: {
+ avatar: params.profile?.avatar || user.profile?.avatar,
+ username: params.profile?.username || user.profile?.username,
+ first_name: params.profile?.first_name || user.profile?.first_name,
+ last_name: params.profile?.last_name || user.profile?.last_name,
+ bio: params.profile?.bio || user.profile?.bio,
+ address: params.profile?.address || user.profile?.address,
+ birth_date: params.profile?.birth_date || user.profile?.birth_date,
+ },
+ },
+ },
+ include: {
+ profile: true,
+ },
+ });
+
+ return {
+ data: {
+ user: {
+ ...data.user,
+ role: params.role,
+ profile: {
+ user_id: userId,
+ ...updateUser.profile,
+ },
+ },
+ },
+ error: null,
+ };
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
+ }
+ })
+ }
+
+ async deleteUser(userId: string): Promise {
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > deleteUser",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
+ const supabase = this.supabaseAdmin;
+
+ const { error } = await supabase.auth.admin.deleteUser(userId);
+
+ if (error) {
+ console.error("Error deleting user:", error);
+ throw new DatabaseOperationError(error.message);
+ }
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
+ }
+ })
+ }
+
+ async banUser(userId: string): Promise {
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > banUser",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
+ const supabase = this.supabaseAdmin;
+
+ const banUntil = new Date();
+ banUntil.setFullYear(banUntil.getFullYear() + 100);
+
+ const { data, error } = await supabase.auth.admin.updateUserById(userId, {
+ ban_duration: "100h",
+ });
+
+ if (error) {
+ console.error("Error banning user:", error);
+ throw new DatabaseOperationError(error.message);
+ }
+
+ return {
+ data: {
+ user: data.user,
+ },
+ error: null,
+ };
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
+ }
+ })
+ }
+
+ async unbanUser(userId: string): Promise {
+ return await this.instrumentationService.startSpan({
+ name: "UsersRepository > unbanUser",
+ op: 'db.query',
+ attributes: { 'db.system': 'postgres' },
+ }, async () => {
+ try {
+ const supabase = this.supabaseAdmin;
+
+ const { data, error } = await supabase.auth.admin.updateUserById(userId, {
+ ban_duration: "none",
+ });
+
+ if (error) {
+ console.error("Error unbanning user:", error);
+ throw new DatabaseOperationError(error.message);
+ }
+
+ const user = await db.users.findUnique({
+ where: {
+ id: userId,
+ },
+ select: {
+ banned_until: true,
+ }
+ });
+
+ if (!user) {
+ throw new NotFoundError("User not found");
+ }
+
+ return {
+ data: {
+ user: data.user,
+ },
+ error: null,
+ };
+ } catch (err) {
+ this.crashReporterService.report(err);
+ throw err;
+ }
+ })
}
}
\ No newline at end of file
diff --git a/sigap-website/src/application/services/crash-reporter.service.interface.ts b/sigap-website/src/application/services/crash-reporter.service.interface.ts
index b94f6b7..03f0fc7 100644
--- a/sigap-website/src/application/services/crash-reporter.service.interface.ts
+++ b/sigap-website/src/application/services/crash-reporter.service.interface.ts
@@ -1,3 +1,12 @@
export interface ICrashReporterService {
report(error: any): string;
- }
\ No newline at end of file
+ }
+
+class CrashReporterService implements ICrashReporterService {
+ report(error: any): string {
+ // Implementation of the report method
+ return "Error reported";
+ }
+}
+
+export const ICrashReporterServiceImpl = new CrashReporterService();
\ No newline at end of file
diff --git a/sigap-website/src/application/services/instrumentation.service.interface.ts b/sigap-website/src/application/services/instrumentation.service.interface.ts
index 9c8ff0f..fa994c1 100644
--- a/sigap-website/src/application/services/instrumentation.service.interface.ts
+++ b/sigap-website/src/application/services/instrumentation.service.interface.ts
@@ -8,4 +8,25 @@ export interface IInstrumentationService {
options: Record,
callback: () => T
): Promise;
- }
\ No newline at end of file
+ }
+
+class InstrumentationService implements IInstrumentationService {
+ startSpan(
+ options: { name: string; op?: string; attributes?: Record },
+ callback: () => T
+ ): T {
+ // Implementation of the startSpan method
+ return callback();
+ }
+
+ async instrumentServerAction(
+ name: string,
+ options: Record,
+ callback: () => T
+ ): Promise {
+ // Implementation of the instrumentServerAction method
+ return callback();
+ }
+}
+
+export const IInstrumentationServiceImpl = new InstrumentationService();
\ No newline at end of file
diff --git a/sigap-website/src/entities/models/auth/sign-in.model.ts b/sigap-website/src/entities/models/auth/sign-in.model.ts
index 286f315..b7d96d9 100644
--- a/sigap-website/src/entities/models/auth/sign-in.model.ts
+++ b/sigap-website/src/entities/models/auth/sign-in.model.ts
@@ -1,15 +1,15 @@
import { z } from "zod";
// Define the sign-in form schema using Zod
-export const signInSchema = z.object({
+export const SignInSchema = z.object({
email: z
.string()
.min(1, { message: "Email is required" })
- .email({ message: "Invalid email address" }),
+ .email({ message: "Please enter a valid email address" }),
});
// Export the type derived from the schema
-export type SignInFormData = z.infer;
+export type SignInFormData = z.infer;
// Default values for the form
export const defaultSignInValues: SignInFormData = {
diff --git a/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx
new file mode 100644
index 0000000..58418db
--- /dev/null
+++ b/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { useMutation } from '@tanstack/react-query';
+import { toast } from 'sonner';
+import { SignInFormData } from '@/src/entities/models/auth/sign-in.model';
+import { VerifyOtpFormData } from '@/src/entities/models/auth/verify-otp.model';
+import { useNavigations } from '@/app/_hooks/use-navigations';
+import { AuthenticationError } from '@/src/entities/errors/auth';
+import * as authRepository from '@/src/application/repositories/authentication.repository';
+
+export function useAuthMutation() {
+ const { router } = useNavigations();
+
+ // Sign In Mutation
+ const signInMutation = useMutation({
+ mutationFn: async (data: SignInFormData) => {
+ return await authRepository.signIn(data);
+ },
+ onSuccess: (result) => {
+ toast.success(result.message);
+ if (result.redirectTo && result.success) {
+ router.push(result.redirectTo);
+ }
+ },
+ onError: (error) => {
+ if (error instanceof AuthenticationError) {
+ toast.error(`Authentication failed: ${error.message}`);
+ } else {
+ toast.error('Failed to sign in. Please try again later.');
+ }
+ }
+ });
+
+ // Verify OTP Mutation
+ const verifyOtpMutation = useMutation({
+ mutationFn: async (data: VerifyOtpFormData) => {
+ return await authRepository.verifyOtp(data);
+ },
+ onSuccess: (result) => {
+ toast.success(result.message);
+ if (result.redirectTo) {
+ router.push(result.redirectTo);
+ }
+ },
+ onError: (error) => {
+ if (error instanceof AuthenticationError) {
+ toast.error(`Verification failed: ${error.message}`);
+ } else {
+ toast.error('Failed to verify OTP. Please try again.');
+ }
+ }
+ });
+
+ // Sign Out Mutation
+ const signOutMutation = useMutation({
+ mutationFn: async () => {
+ return await authRepository.signOut();
+ },
+ onSuccess: (result) => {
+ toast.success(result.message);
+ if (result.redirectTo) {
+ router.push(result.redirectTo);
+ }
+ },
+ onError: (error) => {
+ toast.error('Failed to sign out. Please try again.');
+ }
+ });
+
+ // Password Recovery Mutation
+ const passwordRecoveryMutation = useMutation({
+ mutationFn: async (email: string) => {
+ return await authRepository.sendPasswordRecovery(email);
+ },
+ onSuccess: (result) => {
+ toast.success(result.message);
+ },
+ onError: (error) => {
+ toast.error('Failed to send password recovery email. Please try again.');
+ }
+ });
+
+ // Magic Link Mutation
+ const magicLinkMutation = useMutation({
+ mutationFn: async (email: string) => {
+ return await authRepository.sendMagicLink(email);
+ },
+ onSuccess: (result) => {
+ toast.success(result.message);
+ },
+ onError: (error) => {
+ toast.error('Failed to send magic link email. Please try again.');
+ }
+ });
+
+ return {
+ signIn: {
+ mutate: signInMutation.mutateAsync,
+ isPending: signInMutation.isPending,
+ error: signInMutation.error,
+ },
+ verifyOtp: {
+ mutate: verifyOtpMutation.mutateAsync,
+ isPending: verifyOtpMutation.isPending,
+ error: verifyOtpMutation.error,
+ },
+ signOut: {
+ mutate: signOutMutation.mutateAsync,
+ isPending: signOutMutation.isPending,
+ error: signOutMutation.error,
+ },
+ passwordRecovery: {
+ mutate: passwordRecoveryMutation.mutateAsync,
+ isPending: passwordRecoveryMutation.isPending,
+ error: passwordRecoveryMutation.error,
+ },
+ magicLink: {
+ mutate: magicLinkMutation.mutateAsync,
+ isPending: magicLinkMutation.isPending,
+ error: magicLinkMutation.error,
+ }
+ };
+}
\ No newline at end of file
diff --git a/sigap-website/src/interface-adapters/controllers/auth/sign-in-controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/sign-in-controller.tsx
index 85fb85e..886ea12 100644
--- a/sigap-website/src/interface-adapters/controllers/auth/sign-in-controller.tsx
+++ b/sigap-website/src/interface-adapters/controllers/auth/sign-in-controller.tsx
@@ -1,97 +1,194 @@
-// src/controllers/sign-in.controller.tsx
"use client";
-
import { useRouter } from "next/navigation";
import {
defaultSignInValues,
SignInFormData,
- signInSchema,
+ SignInSchema,
} from "@/src/entities/models/auth/sign-in.model";
import { useState, type FormEvent, type ChangeEvent } from "react";
import { toast } from "sonner";
import { z } from "zod";
-import { signIn } from "@/app/(pages)/(auth)/action";
+// import { signIn } from "";
+import { useAuthMutation } from "./auth-controller";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { AuthenticationError } from "@/src/entities/errors/auth";
type SignInFormErrors = Partial>;
-export function useSignInForm() {
- const [formData, setFormData] = useState(defaultSignInValues);
- const [errors, setErrors] = useState({});
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [message, setMessage] = useState(null);
- const router = useRouter();
+// export function useSignInForm() {
+// const [formData, setFormData] = useState(defaultSignInValues);
+// const [errors, setErrors] = useState({});
+// const [isSubmitting, setIsSubmitting] = useState(false);
+// const [message, setMessage] = useState(null);
+// const router = useRouter();
- const validateForm = (): boolean => {
- try {
- signInSchema.parse(formData);
- setErrors({});
- return true;
- } catch (error) {
- if (error instanceof z.ZodError) {
- const formattedErrors: SignInFormErrors = {};
- error.errors.forEach((err) => {
- const path = err.path[0] as keyof SignInFormData;
- formattedErrors[path] = err.message;
- });
- setErrors(formattedErrors);
- }
- return false;
- }
- };
+// const validateForm = (): boolean => {
+// try {
+// SignInSchema.parse(formData);
+// setErrors({});
+// return true;
+// } catch (error) {
+// if (error instanceof z.ZodError) {
+// const formattedErrors: SignInFormErrors = {};
+// error.errors.forEach((err) => {
+// const path = err.path[0] as keyof SignInFormData;
+// formattedErrors[path] = err.message;
+// });
+// setErrors(formattedErrors);
+// }
+// return false;
+// }
+// };
- const handleChange = (e: ChangeEvent) => {
- const { name, value } = e.target;
- setFormData((prev) => ({
- ...prev,
- [name]: value,
- }));
- };
+// const handleChange = (e: ChangeEvent) => {
+// const { name, value } = e.target;
+// setFormData((prev) => ({
+// ...prev,
+// [name]: value,
+// }));
+// };
- const handleSubmit = async (e: FormEvent) => {
- e.preventDefault();
- if (!validateForm()) {
- return;
- }
+// const handleSubmit = async (e: FormEvent) => {
+// e.preventDefault();
+// if (!validateForm()) {
+// return;
+// }
- setIsSubmitting(true);
- setMessage(null);
+// setIsSubmitting(true);
+// setMessage(null);
- try {
- const result = await signIn(formData);
+// try {
+// const result = await signIn(formData);
- if (result.success) {
- setMessage(result.message);
- toast.success(result.message);
+// if (result.success) {
+// setMessage(result.message);
+// toast.success(result.message);
- // Handle client-side navigation
- if (result.redirectTo) {
- router.push(result.redirectTo);
- }
- } else {
- setErrors({
- email: result.message || "Sign in failed. Please try again.",
- });
- toast.error(result.message || "Sign in failed. Please try again.");
- }
+// // Handle client-side navigation
+// if (result.redirectTo) {
+// router.push(result.redirectTo);
+// }
+// } else {
+// setErrors({
+// email: result.message || "Sign in failed. Please try again.",
+// });
+// toast.error(result.message || "Sign in failed. Please try again.");
+// }
+// } catch (error) {
+// console.error("Sign in failed", error);
+// setErrors({
+// email: "An unexpected error occurred. Please try again.",
+// });
+// toast.error("An unexpected error occurred. Please try again.");
+// } finally {
+// setIsSubmitting(false);
+// }
+// };
+
+// return {
+// formData,
+// errors,
+// isSubmitting,
+// message,
+// setFormData,
+// handleChange,
+// handleSubmit,
+// };
+// }
+
+// export function useSignInController() {
+// const [formData, setFormData] = useState(defaultSignInValues);
+// const [errors, setErrors] = useState>({});
+
+// const { signIn } = useAuthMutation();
+
+// const form = useForm({
+// resolver: zodResolver(SignInSchema),
+// defaultValues: defaultSignInValues,
+// });
+
+// // Handle input changes
+// const handleChange = (e: React.ChangeEvent) => {
+// const { name, value } = e.target;
+// setFormData(prev => ({
+// ...prev,
+// [name]: value
+// }));
+
+// // Clear error when user starts typing
+// if (errors[name]) {
+// setErrors(prev => ({
+// ...prev,
+// [name]: ''
+// }));
+// }
+// };
+
+// // Direct handleSubmit handler for the form
+// const handleSubmit = async (e: React.FormEvent) => {
+// e.preventDefault();
+// setErrors({});
+
+// try {
+// // Basic email validation before sending to API
+// if (!formData.email || !formData.email.includes('@')) {
+// setErrors({ email: 'Please enter a valid email address' });
+// return;
+// }
+
+// await signIn.mutate(formData);
+// } catch (error) {
+// // This catch block will likely not be used since errors are handled in the mutation
+// console.error("Form submission error:", error);
+// }
+// };
+
+// // Combine form validation errors with API errors
+// const formErrors = {
+// ...errors,
+// // If there's an API error from the mutation, add it to the appropriate field
+// ...(signIn.error instanceof AuthenticationError ?
+// { email: signIn.error.message } :
+// {})
+// };
+
+// return {
+// formData,
+// handleChange,
+// handleSubmit,
+// isPending: signIn.isPending,
+// error: formErrors
+// };
+// }
+
+export function useSignInController() {
+ const { signIn } = useAuthMutation();
+
+ // Gunakan react-hook-form untuk mengelola form state & error handling
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(SignInSchema),
+ defaultValues: defaultSignInValues,
+ });
+
+ // Handler untuk submit form
+ const onSubmit = handleSubmit(async (data) => {
+ try {
+ signIn.mutate(data);
} catch (error) {
- console.error("Sign in failed", error);
- setErrors({
- email: "An unexpected error occurred. Please try again.",
- });
- toast.error("An unexpected error occurred. Please try again.");
- } finally {
- setIsSubmitting(false);
+ console.error("Sign-in submission error:", error);
}
- };
+ });
return {
- formData,
+ register,
+ handleSubmit: onSubmit,
errors,
- isSubmitting,
- message,
- setFormData,
- handleChange,
- handleSubmit,
+ isPending: signIn.isPending,
};
}
\ No newline at end of file
diff --git a/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx
index 68fab3f..4461956 100644
--- a/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx
+++ b/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx
@@ -1,11 +1,11 @@
// src/hooks/useVerifyOtpForm.ts
"use client";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
-import { verifyOtp } from "@/app/(pages)/(auth)/action";
+// import { verifyOtp } from "";
import {
defaultVerifyOtpValues,
VerifyOtpFormData,
@@ -13,51 +13,104 @@ import {
} from "@/src/entities/models/auth/verify-otp.model";
import { useNavigations } from "@/app/_hooks/use-navigations";
import { toast } from "sonner";
+import { useAuthMutation } from "./auth-controller";
-export function useVerifyOtpForm(initialEmail: string) {
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [message, setMessage] = useState(null);
- const { router } = useNavigations();
+// export function useVerifyOtpForm(email: string) {
+// const [isSubmitting, setIsSubmitting] = useState(false);
+// const [message, setMessage] = useState(null);
+// const { router } = useNavigations();
- const form = useForm({
+// const form = useForm({
+// resolver: zodResolver(verifyOtpSchema),
+// defaultValues: { ...defaultVerifyOtpValues, email: email },
+// });
+
+// const onSubmit = async (data: VerifyOtpFormData) => {
+// setIsSubmitting(true);
+// setMessage(null);
+
+// try {
+// const result = await verifyOtp(data);
+
+// if (result.success) {
+// setMessage(result.message);
+// // Redirect or update UI state as needed
+// toast.success(result.message);
+// if (result.redirectTo) {
+// router.push(result.redirectTo);
+// }
+// } else {
+// toast.error(result.message);
+// form.setError("token", { type: "manual", message: result.message });
+// }
+// } catch (error) {
+// console.error("OTP verification failed", error);
+// toast.error("An unexpected error occurred. Please try again.");
+// form.setError("token", {
+// type: "manual",
+// message: "An unexpected error occurred. Please try again.",
+// });
+// } finally {
+// setIsSubmitting(false);
+// }
+// };
+
+// return {
+// form,
+// isSubmitting,
+// message,
+// onSubmit,
+// };
+// }
+
+export const useVerifyOtpController = (email: string) => {
+ const { verifyOtp } = useAuthMutation()
+
+ const {
+ control,
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isSubmitSuccessful },
+ } = useForm({
resolver: zodResolver(verifyOtpSchema),
- defaultValues: { ...defaultVerifyOtpValues, email: initialEmail },
- });
+ defaultValues: { ...defaultVerifyOtpValues, email: email },
+ })
- const onSubmit = async (data: VerifyOtpFormData) => {
- setIsSubmitting(true);
- setMessage(null);
-
- try {
- const result = await verifyOtp(data);
-
- if (result.success) {
- setMessage(result.message);
- // Redirect or update UI state as needed
- toast.success(result.message);
- if (result.redirectTo) {
- router.push(result.redirectTo);
- }
- } else {
- toast.error(result.message);
- form.setError("token", { type: "manual", message: result.message });
- }
- } catch (error) {
- console.error("OTP verification failed", error);
- toast.error("An unexpected error occurred. Please try again.");
- form.setError("token", {
- type: "manual",
- message: "An unexpected error occurred. Please try again.",
- });
- } finally {
- setIsSubmitting(false);
+ // Clear form after successful submission
+ useEffect(() => {
+ if (isSubmitSuccessful) {
+ reset({ ...defaultVerifyOtpValues, email })
}
- };
+ }, [isSubmitSuccessful, reset, email])
+
+ const onSubmit = handleSubmit(async (data) => {
+ try {
+ await verifyOtp.mutate(data)
+ } catch (error) {
+ console.error("OTP verification failed", error)
+ }
+ })
+
+ // Function to handle auto-submission when all digits are entered
+ const handleOtpChange = (value: string, onChange: (value: string) => void) => {
+ onChange(value)
+
+ // Auto-submit when all 6 digits are entered
+ if (value.length === 6) {
+ setTimeout(() => {
+ onSubmit()
+ }, 300) // Small delay to allow the UI to update
+ }
+ }
return {
- form,
- isSubmitting,
- message,
- onSubmit,
- };
+ control,
+ register,
+ handleSubmit: onSubmit,
+ handleOtpChange,
+ errors,
+ isPending: verifyOtp.isPending,
+ }
}
+