bug rls (not solved)
This commit is contained in:
parent
b2784cc108
commit
bc03c18a05
|
@ -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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="flex h-12 shrink-0 items-center justify-end border-b px-4 mb-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-8"></header>
|
<header className="flex h-12 shrink-0 items-center justify-end border-b px-4 mb-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-8"></header>
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||||
<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">
|
||||||
|
{JSON.stringify(user, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
UserResponse,
|
UserResponse,
|
||||||
} from "@/src/models/users/users.model";
|
} from "@/src/models/users/users.model";
|
||||||
import { createClient } from "@/utils/supabase/server";
|
import { createClient } from "@/utils/supabase/server";
|
||||||
|
import { createClient as createClientSide } from "@/utils/supabase/client";
|
||||||
import { createAdminClient } from "@/utils/supabase/admin";
|
import { createAdminClient } from "@/utils/supabase/admin";
|
||||||
|
|
||||||
// Initialize Supabase client with admin key
|
// 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
|
// Update an existing user
|
||||||
export async function updateUser(
|
export async function updateUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -110,7 +161,8 @@ export async function updateUser(
|
||||||
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||||
email: params.email,
|
email: params.email,
|
||||||
phone: params.phone,
|
phone: params.phone,
|
||||||
password: params.password,
|
password_hash: params.password_hash,
|
||||||
|
ban_duration: params.ban_duration,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -118,9 +170,52 @@ export async function updateUser(
|
||||||
throw new Error(error.message);
|
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 {
|
return {
|
||||||
data: {
|
data: {
|
||||||
user: data.user,
|
user: {
|
||||||
|
...data.user,
|
||||||
|
role: params.role,
|
||||||
|
profile: {
|
||||||
|
user_id: userId,
|
||||||
|
...updateUser.profile,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,11 +27,14 @@ import { Label } from "@/app/_components/ui/label";
|
||||||
import { Separator } from "@/app/_components/ui/separator";
|
import { Separator } from "@/app/_components/ui/separator";
|
||||||
import { Switch } from "@/app/_components/ui/switch";
|
import { Switch } from "@/app/_components/ui/switch";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
|
||||||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
updateUser,
|
||||||
|
uploadAvatar,
|
||||||
|
} from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
||||||
|
|
||||||
const profileFormSchema = z.object({
|
const profileFormSchema = z.object({
|
||||||
preferred_name: z.string().nullable().optional(),
|
username: z.string().nullable().optional(),
|
||||||
avatar: z.string().nullable().optional(),
|
avatar: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -44,46 +47,32 @@ interface ProfileSettingsProps {
|
||||||
export function ProfileSettings({ user }: ProfileSettingsProps) {
|
export function ProfileSettings({ user }: ProfileSettingsProps) {
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const supabase = createClient();
|
|
||||||
|
|
||||||
// Use profile data with fallbacks
|
// Use profile data with fallbacks
|
||||||
const preferredName = user?.profile?.first_name || "";
|
const username = user?.profile?.username || "";
|
||||||
const userEmail = user?.email || "";
|
const userEmail = user?.email || "";
|
||||||
const userAvatar = user?.profile?.avatar || "";
|
const userAvatar = user?.profile?.avatar || "";
|
||||||
|
|
||||||
const form = useForm<ProfileFormValues>({
|
const form = useForm<ProfileFormValues>({
|
||||||
resolver: zodResolver(profileFormSchema),
|
resolver: zodResolver(profileFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
preferred_name: preferredName || "",
|
username: username || "",
|
||||||
avatar: userAvatar || "",
|
avatar: userAvatar || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file || !user?.id) return;
|
|
||||||
|
if (!file || !user?.id || !user?.email) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
|
||||||
// Upload to Supabase Storage
|
// Upload avatar to storage
|
||||||
const fileExt = file.name.split(".").pop();
|
const publicUrl = await uploadAvatar(user.id, user.email, file);
|
||||||
const fileName = `${user.id}-${Date.now()}.${fileExt}`;
|
|
||||||
const filePath = `avatars/${fileName}`;
|
|
||||||
|
|
||||||
const { error: uploadError, data } = await supabase.storage
|
console.log("publicUrl", publicUrl);
|
||||||
.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);
|
|
||||||
|
|
||||||
// Update the form value
|
// Update the form value
|
||||||
form.setValue("avatar", publicUrl);
|
form.setValue("avatar", publicUrl);
|
||||||
|
@ -103,13 +92,12 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
|
||||||
if (!user?.id) return;
|
if (!user?.id) return;
|
||||||
|
|
||||||
// Update profile in database
|
// Update profile in database
|
||||||
const { error } = await supabase
|
const { error } = await updateUser(user.id, {
|
||||||
.from("profiles")
|
profile: {
|
||||||
.update({
|
avatar: data.avatar || undefined,
|
||||||
first_name: data.preferred_name,
|
username: data.username || undefined,
|
||||||
avatar: data.avatar,
|
},
|
||||||
})
|
});
|
||||||
.eq("user_id", user.id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -133,10 +121,10 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
|
||||||
<Avatar className="h-16 w-16">
|
<Avatar className="h-16 w-16">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
src={form.watch("avatar") || ""}
|
src={form.watch("avatar") || ""}
|
||||||
alt={preferredName}
|
alt={username}
|
||||||
/>
|
/>
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{preferredName?.[0]?.toUpperCase() ||
|
{username?.[0]?.toUpperCase() ||
|
||||||
userEmail?.[0]?.toUpperCase()}
|
userEmail?.[0]?.toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
@ -160,7 +148,7 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
|
||||||
<Label>Preferred name</Label>
|
<Label>Preferred name</Label>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="preferred_name"
|
name="username"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|
|
@ -3,10 +3,22 @@ import type { NextConfig } from "next";
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "images.unsplash.com",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "images.pexels.com",
|
hostname: "images.pexels.com",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "images.unsplash.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "cppejroeyonsqxulinaj.supabase.co",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,11 +34,13 @@ const timestampSchema = z.union([z.string(), z.date()]).nullable();
|
||||||
// email: z.string().email(),
|
// email: z.string().email(),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
|
||||||
export const UserSchema = z.object({
|
export const UserSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
role: z.string().optional(),
|
role: z.string().optional(),
|
||||||
email: z.string().email().optional(),
|
email: z.string().email().optional(),
|
||||||
email_confirmed_at: z.union([z.string(), z.date()]).nullable().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(),
|
invited_at: z.union([z.string(), z.date()]).nullable().optional(),
|
||||||
phone: z.string().nullable().optional(),
|
phone: z.string().nullable().optional(),
|
||||||
confirmed_at: z.union([z.string(), z.date()]).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(),
|
banned_until: z.union([z.string(), z.date()]).nullable().optional(),
|
||||||
profile: z
|
profile: z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string().optional(),
|
||||||
user_id: z.string(),
|
user_id: z.string(),
|
||||||
avatar: z.string().nullable().optional(),
|
avatar: z.string().nullable().optional(),
|
||||||
|
username: z.string().nullable().optional(),
|
||||||
first_name: z.string().nullable().optional(),
|
first_name: z.string().nullable().optional(),
|
||||||
last_name: z.string().nullable().optional(),
|
last_name: z.string().nullable().optional(),
|
||||||
bio: z.string().nullable().optional(),
|
bio: z.string().nullable().optional(),
|
||||||
|
@ -69,11 +72,12 @@ export const ProfileSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
user_id: z.string(),
|
user_id: z.string(),
|
||||||
avatar: z.string().optional(),
|
avatar: z.string().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
first_name: z.string().optional(),
|
first_name: z.string().optional(),
|
||||||
last_name: z.string().optional(),
|
last_name: z.string().optional(),
|
||||||
bio: z.string(),
|
bio: z.string(),
|
||||||
address: z.string().optional(),
|
address: z.string().optional(),
|
||||||
birthdate: z.string().optional(),
|
birth_date: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Profile = z.infer<typeof ProfileSchema>;
|
export type Profile = z.infer<typeof ProfileSchema>;
|
||||||
|
@ -91,7 +95,18 @@ export type CreateUserParams = z.infer<typeof CreateUserParamsSchema>;
|
||||||
export const UpdateUserParamsSchema = z.object({
|
export const UpdateUserParamsSchema = z.object({
|
||||||
email: z.string().email().optional(),
|
email: z.string().email().optional(),
|
||||||
phone: z.string().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<typeof UpdateUserParamsSchema>;
|
export type UpdateUserParams = z.infer<typeof UpdateUserParamsSchema>;
|
||||||
|
|
Loading…
Reference in New Issue