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;