diff --git a/sigap-website/app/(protected)/(admin)/dashboard/page.tsx b/sigap-website/app/(protected)/(admin)/dashboard/page.tsx index 3fa23b9..615f75b 100644 --- a/sigap-website/app/(protected)/(admin)/dashboard/page.tsx +++ b/sigap-website/app/(protected)/(admin)/dashboard/page.tsx @@ -1,10 +1,26 @@ -export default function DashboardPage() { +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + +export default async function DashboardPage() { + const supabase = await createClient(); + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + return redirect("/sign-in"); + } return ( <>
-
+
+
+              {JSON.stringify(user, null, 2)}
+            
+
diff --git a/sigap-website/app/(protected)/(admin)/dashboard/user-management/action.ts b/sigap-website/app/(protected)/(admin)/dashboard/user-management/action.ts index 652b31c..aacbbff 100644 --- a/sigap-website/app/(protected)/(admin)/dashboard/user-management/action.ts +++ b/sigap-website/app/(protected)/(admin)/dashboard/user-management/action.ts @@ -9,6 +9,7 @@ import { UserResponse, } from "@/src/models/users/users.model"; import { createClient } from "@/utils/supabase/server"; +import { createClient as createClientSide } from "@/utils/supabase/client"; import { createAdminClient } from "@/utils/supabase/admin"; // Initialize Supabase client with admin key @@ -100,6 +101,56 @@ export async function createUser( }; } +export async function uploadAvatar(userId: string, email: string, file: File) { + try { + const supabase = createClientSide(); + + // Pastikan mendapatkan session untuk autentikasi + const { data: session } = await supabase.auth.getSession(); + if (!session) throw new Error("User is not authenticated"); + + const baseUrl = `${process.env.NEXT_PUBLIC_SUPABASE_STORAGE_URL}/avatars`; + + const fileExt = file.name.split(".").pop(); + const emailName = email.split("@")[0]; + const fileName = `AVR-${emailName}.${fileExt}`; + const filePath = `${baseUrl}/${fileName}`; + + const { error: uploadError } = await supabase.storage + .from("avatars") + .upload(fileName, file, { + upsert: false, + contentType: file.type, + }); + + if (uploadError) { + throw uploadError; + } + + await db.users.update({ + where: { + id: userId, + }, + data: { + profile: { + update: { + avatar: filePath, + }, + }, + }, + }); + + const { + data: { publicUrl }, + } = supabase.storage.from("avatars").getPublicUrl(fileName); + + return publicUrl; + } catch (error) { + console.error("Error uploading avatar:", error); + throw error; + } +} + // Update an existing user export async function updateUser( userId: string, @@ -110,7 +161,8 @@ export async function updateUser( const { data, error } = await supabase.auth.admin.updateUserById(userId, { email: params.email, phone: params.phone, - password: params.password, + password_hash: params.password_hash, + ban_duration: params.ban_duration, }); if (error) { @@ -118,9 +170,52 @@ export async function updateUser( throw new Error(error.message); } + const user = await db.users.findUnique({ + where: { + id: userId, + }, + include: { + profile: true, + }, + }); + + if (!user) { + throw new Error("User not found"); + } + + const updateUser = await db.users.update({ + where: { + id: userId, + }, + data: { + role: params.role, + 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, + birth_date: params.profile.birth_date || user.profile?.birth_date, + }, + }, + }, + include: { + profile: true, + }, + }); + return { data: { - user: data.user, + user: { + ...data.user, + role: params.role, + profile: { + user_id: userId, + ...updateUser.profile, + }, + }, }, error: null, }; diff --git a/sigap-website/app/_components/admin/settings/profile-settings.tsx b/sigap-website/app/_components/admin/settings/profile-settings.tsx index 1552635..bd70f23 100644 --- a/sigap-website/app/_components/admin/settings/profile-settings.tsx +++ b/sigap-website/app/_components/admin/settings/profile-settings.tsx @@ -27,11 +27,14 @@ import { Label } from "@/app/_components/ui/label"; import { Separator } from "@/app/_components/ui/separator"; import { Switch } from "@/app/_components/ui/switch"; import { useRef, useState } from "react"; -import { createClient } from "@/utils/supabase/client"; import { ScrollArea } from "@/app/_components/ui/scroll-area"; +import { + updateUser, + uploadAvatar, +} from "@/app/(protected)/(admin)/dashboard/user-management/action"; const profileFormSchema = z.object({ - preferred_name: z.string().nullable().optional(), + username: z.string().nullable().optional(), avatar: z.string().nullable().optional(), }); @@ -44,46 +47,32 @@ interface ProfileSettingsProps { export function ProfileSettings({ user }: ProfileSettingsProps) { const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); - const supabase = createClient(); // Use profile data with fallbacks - const preferredName = user?.profile?.first_name || ""; + const username = user?.profile?.username || ""; const userEmail = user?.email || ""; const userAvatar = user?.profile?.avatar || ""; const form = useForm({ resolver: zodResolver(profileFormSchema), defaultValues: { - preferred_name: preferredName || "", + username: username || "", avatar: userAvatar || "", }, }); const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; - if (!file || !user?.id) return; + + if (!file || !user?.id || !user?.email) return; try { setIsUploading(true); - // Upload to Supabase Storage - const fileExt = file.name.split(".").pop(); - const fileName = `${user.id}-${Date.now()}.${fileExt}`; - const filePath = `avatars/${fileName}`; + // Upload avatar to storage + const publicUrl = await uploadAvatar(user.id, user.email, file); - const { error: uploadError, data } = await supabase.storage - .from("profiles") - .upload(filePath, file, { - upsert: true, - contentType: file.type, - }); - - if (uploadError) throw uploadError; - - // Get the public URL - const { - data: { publicUrl }, - } = supabase.storage.from("profiles").getPublicUrl(filePath); + console.log("publicUrl", publicUrl); // Update the form value form.setValue("avatar", publicUrl); @@ -103,13 +92,12 @@ export function ProfileSettings({ user }: ProfileSettingsProps) { if (!user?.id) return; // Update profile in database - const { error } = await supabase - .from("profiles") - .update({ - first_name: data.preferred_name, - avatar: data.avatar, - }) - .eq("user_id", user.id); + const { error } = await updateUser(user.id, { + profile: { + avatar: data.avatar || undefined, + username: data.username || undefined, + }, + }); if (error) throw error; } catch (error) { @@ -133,10 +121,10 @@ export function ProfileSettings({ user }: ProfileSettingsProps) { - {preferredName?.[0]?.toUpperCase() || + {username?.[0]?.toUpperCase() || userEmail?.[0]?.toUpperCase()}
@@ -160,7 +148,7 @@ export function ProfileSettings({ user }: ProfileSettingsProps) { ( diff --git a/sigap-website/app/_components/admin/navigations/profile-form.tsx b/sigap-website/app/_components/admin/users/profile-form.tsx similarity index 100% rename from sigap-website/app/_components/admin/navigations/profile-form.tsx rename to sigap-website/app/_components/admin/users/profile-form.tsx diff --git a/sigap-website/next.config.ts b/sigap-website/next.config.ts index 42663ed..92933bd 100644 --- a/sigap-website/next.config.ts +++ b/sigap-website/next.config.ts @@ -3,10 +3,22 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { images: { remotePatterns: [ + { + protocol: "https", + hostname: "images.unsplash.com", + }, { protocol: "https", hostname: "images.pexels.com", }, + { + protocol: "https", + hostname: "images.unsplash.com", + }, + { + protocol: "https", + hostname: "cppejroeyonsqxulinaj.supabase.co", + }, ], }, }; diff --git a/sigap-website/src/models/users/users.model.ts b/sigap-website/src/models/users/users.model.ts index d9655d0..edaafda 100644 --- a/sigap-website/src/models/users/users.model.ts +++ b/sigap-website/src/models/users/users.model.ts @@ -34,11 +34,13 @@ const timestampSchema = z.union([z.string(), z.date()]).nullable(); // email: z.string().email(), // }); + export const UserSchema = z.object({ id: z.string(), role: z.string().optional(), email: z.string().email().optional(), email_confirmed_at: z.union([z.string(), z.date()]).nullable().optional(), + password_hash: z.string().nullable().optional(), invited_at: z.union([z.string(), z.date()]).nullable().optional(), phone: z.string().nullable().optional(), confirmed_at: z.union([z.string(), z.date()]).nullable().optional(), @@ -50,9 +52,10 @@ export const UserSchema = z.object({ banned_until: z.union([z.string(), z.date()]).nullable().optional(), profile: z .object({ - id: z.string(), + id: z.string().optional(), user_id: z.string(), avatar: z.string().nullable().optional(), + username: z.string().nullable().optional(), first_name: z.string().nullable().optional(), last_name: z.string().nullable().optional(), bio: z.string().nullable().optional(), @@ -69,11 +72,12 @@ export const ProfileSchema = z.object({ id: z.string(), user_id: z.string(), avatar: z.string().optional(), + username: z.string().optional(), first_name: z.string().optional(), last_name: z.string().optional(), bio: z.string(), address: z.string().optional(), - birthdate: z.string().optional(), + birth_date: z.string().optional(), }); export type Profile = z.infer; @@ -91,7 +95,18 @@ export type CreateUserParams = z.infer; export const UpdateUserParamsSchema = z.object({ email: z.string().email().optional(), phone: z.string().optional(), - password: z.string().optional(), + password_hash: z.string().optional(), + ban_duration: z.string().optional(), + role: z.enum(["user", "staff", "admin"]).optional(), + profile: z.object({ + avatar: z.string().optional(), + username: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + bio: z.string().optional(), + address: z.string().optional(), + birth_date: z.string().optional(), + }), }); export type UpdateUserParams = z.infer;