diff --git a/sigap-website/.env.example b/sigap-website/.env.example index 0863ce9..803782c 100644 --- a/sigap-website/.env.example +++ b/sigap-website/.env.example @@ -6,9 +6,10 @@ NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_SECRET= NEXT_PUBLIC_SUPABASE_STORAGE_URL= + # Supabase Local URL -NEXT_PUBLIC_SUPABASE_URL= -NEXT_PUBLIC_SUPABASE_ANON_KEY= +# NEXT_PUBLIC_SUPABASE_URL= +# NEXT_PUBLIC_SUPABASE_ANON_KEY= # Supabase Service Role Secret Key SERVICE_ROLE_SECRET= @@ -21,8 +22,6 @@ SEND_EMAIL_HOOK_SECRET= # Connect to Supabase via connection pooling with Supavisor. DATABASE_URL= - - # Direct connection to the database. Used for migrations. DIRECT_URL= diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/action.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/action.ts index 1272c10..51c640a 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/action.ts +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/action.ts @@ -84,7 +84,7 @@ export async function createUser( const { data, error } = await supabase.auth.admin.createUser({ email: params.email, - password: params.encrypted_password, + password: params.password, phone: params.phone, email_confirm: params.email_confirm, }); diff --git a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx b/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx index 5a67c14..93ffb6a 100644 --- a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx +++ b/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx @@ -8,7 +8,7 @@ import { Input } from "../../../_components/ui/input"; import { SubmitButton } from "../../../_components/submit-button"; import Link from "next/link"; import { FormField } from "../../../_components/form-field"; -import { useSignInForm } from "@/src/controller/auth/sign-in-controller"; +import { useSignInForm } from "@/src/interface-adapters/controllers/auth/sign-in-controller"; export function SignInForm({ className, diff --git a/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx b/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx index 5a08624..3096cf9 100644 --- a/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx +++ b/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx @@ -24,7 +24,7 @@ import { CardTitle, } from "@/app/_components/ui/card"; import { cn } from "@/app/_lib/utils"; -import { useVerifyOtpForm } from "@/src/controller/auth/verify-otp.controller"; +import { useVerifyOtpForm } from "@/src/interface-adapters/controllers/auth/verify-otp.controller"; interface VerifyOtpFormProps extends React.HTMLAttributes {} diff --git a/sigap-website/src/entities/models/users/users.model.ts b/sigap-website/src/entities/models/users/users.model.ts index ea1d19a..6dbc3ce 100644 --- a/sigap-website/src/entities/models/users/users.model.ts +++ b/sigap-website/src/entities/models/users/users.model.ts @@ -84,7 +84,7 @@ export type Profile = z.infer; export const CreateUserParamsSchema = z.object({ email: z.string().email(), - encrypted_password: z.string(), + password: z.string(), phone: z.string().optional(), user_metadata: z.record(z.any()).optional(), email_confirm: z.boolean().optional(), diff --git a/sigap-website/src/controller/auth/sign-in-controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/sign-in-controller.tsx similarity index 100% rename from sigap-website/src/controller/auth/sign-in-controller.tsx rename to sigap-website/src/interface-adapters/controllers/auth/sign-in-controller.tsx diff --git a/sigap-website/src/controller/auth/verify-otp.controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx similarity index 100% rename from sigap-website/src/controller/auth/verify-otp.controller.tsx rename to sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx diff --git a/sigap-website/src/interface-adapters/controllers/users/update-user-controller.ts b/sigap-website/src/interface-adapters/controllers/users/update-user-controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/sigap-website/src/repositories/users.repository.ts b/sigap-website/src/repositories/users.repository.ts index e69de29..86408c9 100644 --- a/sigap-website/src/repositories/users.repository.ts +++ b/sigap-website/src/repositories/users.repository.ts @@ -0,0 +1,313 @@ +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, InputParseError, AuthenticationError, UnauthenticatedError, UnauthorizedError } from "@/path/to/custom/errors"; + +export class UsersRepository { + private supabaseAdmin = createAdminClient(); + private supabaseClient = createClient(); + + async fetchUsers(): Promise { + const users = await db.users.findMany({ + include: { + profile: true, + }, + }); + + if (!users) { + throw new NotFoundError("Users not found"); + } + + return users; + } + + async getCurrentUser(): Promise { + const supabase = await this.supabaseClient; + + const { + data: { user }, + error, + } = await supabase.auth.getUser(); + + 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, + }, + }); + + if (!userDetail) { + throw new NotFoundError("User not found"); + } + + return { + data: { + user: userDetail, + }, + error: null, + }; + } + + async createUser(params: CreateUserParams): Promise { + 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, + }); + + 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 (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; + + 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); + } + } +} \ No newline at end of file