resolve RLS issue for upload avatars images

This commit is contained in:
vergiLgood1 2025-03-07 00:22:44 +07:00
parent 45daf059d3
commit aa52dd0ca4
19 changed files with 119 additions and 80 deletions

View File

@ -1,3 +1,3 @@
{ {
"files.autoSave": "off" "files.autoSave": "afterDelay"
} }

View File

@ -20,7 +20,7 @@ export const checkSession = async () => {
return { return {
success: true, success: true,
session, session,
redirectTo: "/protected/dashboard", // or your preferred authenticated route redirectTo: "/dashboard",
}; };
} }

View File

@ -12,14 +12,16 @@ export const signInAction = async (formData: FormData) => {
try { try {
// First, check for existing session // First, check for existing session
const { session, error: sessionError } = await checkSession(); const {
data: { session },
} = await supabase.auth.getSession();
// If there's an active session and the email matches // If there's an active session and the email matches
if (session && session.user.email === email) { if (session?.user?.email === email) {
return { return {
success: true, success: true,
message: "Already logged in", message: "You are already signed in",
redirectTo: "/protected/dashboard", // or wherever you want to redirect logged in users redirectTo: "/dashboard",
}; };
} }

View File

@ -26,5 +26,5 @@ export const verifyOtpAction = async (formData: FormData) => {
return redirect(`/verify-otp?error=${encodeURIComponent(error.message)}`); return redirect(`/verify-otp?error=${encodeURIComponent(error.message)}`);
} }
return redirect("/protected/dashboard?message=OTP verified successfully"); return redirect("/dashboard?message=OTP verified successfully");
}; };

View File

@ -1,17 +1,21 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { checkSession } from "./_actions/session"; import { checkSession } from "./_actions/session";
import { createClient } from "@/utils/supabase/client";
export default async function Layout({ export default async function Layout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const sessionResult = await checkSession(); // const supabase = createClient();
// If there's an active session, redirect to dashboard // const {
if (sessionResult.success && sessionResult.redirectTo) { // data: { session },
redirect(sessionResult.redirectTo); // } = await supabase.auth.getSession();
}
// if (!session) {
// return redirect("/sign-in");
// }
return <div className="max-w-full gap-12 items-start">{children}</div>; return <div className="max-w-full gap-12 items-start">{children}</div>;
} }

View File

@ -11,6 +11,7 @@ export default async function DashboardPage() {
if (!user) { if (!user) {
return redirect("/sign-in"); 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>

View File

@ -9,7 +9,6 @@ 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
@ -103,30 +102,34 @@ export async function createUser(
export async function uploadAvatar(userId: string, email: string, file: File) { export async function uploadAvatar(userId: string, email: string, file: File) {
try { try {
const supabase = createClientSide(); const supabase = await createClient();
// 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 fileExt = file.name.split(".").pop();
const emailName = email.split("@")[0]; const emailName = email.split("@")[0];
const fileName = `AVR-${emailName}.${fileExt}`; const fileName = `AVR-${emailName}.${fileExt}`;
const filePath = `${baseUrl}/${fileName}`;
// Change this line - store directly in the user's folder
const filePath = `${userId}/${fileName}`;
// Upload the avatar to Supabase storage
const { error: uploadError } = await supabase.storage const { error: uploadError } = await supabase.storage
.from("avatars") .from("avatars")
.upload(fileName, file, { .upload(filePath, file, {
upsert: false, upsert: true,
contentType: file.type, contentType: file.type,
}); });
if (uploadError) { if (uploadError) {
console.error("Error uploading avatar:", uploadError);
throw uploadError; throw uploadError;
} }
// Get the public URL
const {
data: { publicUrl },
} = supabase.storage.from("avatars").getPublicUrl(filePath);
// Update user profile with the new avatar URL
await db.users.update({ await db.users.update({
where: { where: {
id: userId, id: userId,
@ -134,16 +137,12 @@ export async function uploadAvatar(userId: string, email: string, file: File) {
data: { data: {
profile: { profile: {
update: { update: {
avatar: filePath, avatar: publicUrl,
}, },
}, },
}, },
}); });
const {
data: { publicUrl },
} = supabase.storage.from("avatars").getPublicUrl(fileName);
return publicUrl; return publicUrl;
} catch (error) { } catch (error) {
console.error("Error uploading avatar:", error); console.error("Error uploading avatar:", error);
@ -151,6 +150,7 @@ export async function uploadAvatar(userId: string, email: string, file: File) {
} }
} }
// Update an existing user // Update an existing user
export async function updateUser( export async function updateUser(
userId: string, userId: string,

View File

@ -27,12 +27,23 @@ import { Separator } from "@/app/_components/ui/separator";
import { InboxDrawer } from "@/app/_components/inbox-drawer"; import { InboxDrawer } from "@/app/_components/inbox-drawer";
import FloatingActionSearchBar from "@/app/_components/floating-action-search-bar"; import FloatingActionSearchBar from "@/app/_components/floating-action-search-bar";
import { AppSidebar } from "@/app/_components/admin/app-sidebar"; import { AppSidebar } from "@/app/_components/admin/app-sidebar";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export default async function Layout({ export default async function Layout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const supabase = await createClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return redirect("/sign-in");
}
return ( return (
<> <>
<SidebarProvider> <SidebarProvider>

View File

@ -19,6 +19,7 @@ import {
import type * as TablerIcons from "@tabler/icons-react"; import type * as TablerIcons from "@tabler/icons-react";
import { useNavigations } from "@/app/_hooks/use-navigations"; import { useNavigations } from "@/app/_hooks/use-navigations";
import { formatUrl } from "@/utils/utils";
interface SubSubItem { interface SubSubItem {
title: string; title: string;
@ -40,22 +41,6 @@ interface NavItem {
subItems?: SubItem[]; subItems?: SubItem[];
} }
// Helper function to ensure URLs are properly formatted
function formatUrl(url: string): string {
// If URL starts with a slash, it's already absolute
if (url.startsWith("/")) {
return url;
}
// Otherwise, ensure it's properly formatted relative to root
// Remove any potential duplicated '/dashboard' prefixes
if (url.startsWith("dashboard/")) {
return "/" + url;
}
return "/" + url;
}
function SubSubItemComponent({ item }: { item: SubSubItem }) { function SubSubItemComponent({ item }: { item: SubSubItem }) {
const router = useNavigations(); const router = useNavigations();
const formattedUrl = formatUrl(item.url); const formattedUrl = formatUrl(item.url);

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
Dialog, Dialog,
@ -27,6 +27,7 @@ import {
import type { User } from "@/src/models/users/users.model"; import type { User } from "@/src/models/users/users.model";
import { ProfileSettings } from "./profile-settings"; import { ProfileSettings } from "./profile-settings";
import { DialogTitle } from "@radix-ui/react-dialog"; import { DialogTitle } from "@radix-ui/react-dialog";
import { useState } from "react";
interface SettingsDialogProps { interface SettingsDialogProps {
user: User | null; user: User | null;
@ -55,7 +56,7 @@ export function SettingsDialog({
open, open,
onOpenChange, onOpenChange,
}: SettingsDialogProps) { }: SettingsDialogProps) {
const [selectedTab, setSelectedTab] = React.useState(defaultTab); const [selectedTab, setSelectedTab] = useState(defaultTab);
// Get user display name // Get user display name
const preferredName = user?.profile?.first_name || ""; const preferredName = user?.profile?.first_name || "";

View File

@ -40,6 +40,7 @@ import { AddUserDialog } from "./add-user-dialog";
import { UserDetailsSheet } from "./sheet"; import { UserDetailsSheet } from "./sheet";
import { Avatar } from "@radix-ui/react-avatar"; import { Avatar } from "@radix-ui/react-avatar";
import Image from "next/image"; import Image from "next/image";
import { useNavigations } from "@/app/_hooks/use-navigations";
export default function UserManagement() { export default function UserManagement() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@ -65,11 +66,12 @@ export default function UserManagement() {
// Use React Query to fetch users // Use React Query to fetch users
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true); const { isLoading, setIsLoading } = useNavigations();
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
setIsLoading(true);
const fetchedUsers = await fetchUsers(); const fetchedUsers = await fetchUsers();
setUsers(fetchedUsers); setUsers(fetchedUsers);
} catch (error) { } catch (error) {

View File

@ -5,6 +5,9 @@ import { Card, CardContent } from "@/app/_components/ui/card";
import { Users, UserCheck, UserX } from "lucide-react"; import { Users, UserCheck, UserX } from "lucide-react";
import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action"; import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
import { User } from "@/src/models/users/users.model"; import { User } from "@/src/models/users/users.model";
import { useNavigations } from "@/app/_hooks/use-navigations";
import { useEffect, useState } from "react";
import { toast } from "sonner";
function calculateUserStats(users: User[]) { function calculateUserStats(users: User[]) {
const totalUsers = users.length; const totalUsers = users.length;
@ -25,10 +28,23 @@ function calculateUserStats(users: User[]) {
} }
export function UserStats() { export function UserStats() {
const { data: users = [], isLoading } = useQuery({ const { isLoading, setIsLoading } = useNavigations();
queryKey: ["users"], const [users, setUsers] = useState<User[]>([]);
queryFn: fetchUsers,
}); useEffect(() => {
const fetchUserData = async () => {
try {
setIsLoading(true);
const fetchedUsers = await fetchUsers();
setUsers(fetchedUsers);
} catch (error) {
toast.error("Failed to fetch users");
} finally {
setIsLoading(false);
}
};
fetchUserData();
}, [setIsLoading]);
const stats = calculateUserStats(users); const stats = calculateUserStats(users);

View File

@ -145,9 +145,9 @@ const InboxDrawerComponent: React.FC<InboxDrawerProps> = ({
<Sheet open={isOpen} onOpenChange={setIsOpen}> <Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <Button
variant="ghost" variant="outline"
size={showTitle ? "sm" : "icon"} size={showTitle ? "sm" : "icon"}
className={`relative ${showTitle ? "flex items-center" : ""}`} className={`relative border-2 ${showTitle ? "flex items-center" : ""}`}
aria-label="Open inbox" aria-label="Open inbox"
> >
<Inbox <Inbox
@ -159,10 +159,8 @@ const InboxDrawerComponent: React.FC<InboxDrawerProps> = ({
{unreadCount > 0 && ( {unreadCount > 0 && (
<Badge <Badge
variant="destructive" variant="destructive"
className="absolute -top-2 -right-2 px-1.5 py-0.5 text-xs" className="absolute -top-1.5 -right-1.5 text-[10px] h-3 w-3 rounded-full p-0 flex items-center justify-center"
> ></Badge>
{unreadCount}
</Badge>
)} )}
</Button> </Button>
</SheetTrigger> </SheetTrigger>
@ -171,7 +169,7 @@ const InboxDrawerComponent: React.FC<InboxDrawerProps> = ({
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<SheetHeader className="p-4 border-b"> <SheetHeader className="p-4 border-b">
<Button <Button
variant="ghost" variant="outline"
onClick={handleBackToList} onClick={handleBackToList}
className="w-fit px-2" className="w-fit px-2"
> >

View File

@ -56,9 +56,9 @@ const ThemeSwitcherComponent = ({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" variant="outline"
size={showTitle ? "sm" : "icon"} size={showTitle ? "sm" : "icon"}
className={showTitle ? "flex justify-center items-center" : ""} className={`border-2 ${showTitle ? "flex justify-center items-center" : ""}`}
aria-label={`Current theme: theme`} aria-label={`Current theme: theme`}
> >
<currentTheme.icon <currentTheme.icon

View File

@ -7,7 +7,7 @@ import {
import { useState } from "react"; import { useState } from "react";
export const useNavigations = () => { export const useNavigations = () => {
const [loading, setLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [active, setActive] = useState<string>(""); const [active, setActive] = useState<string>("");
const router = useRouter(); const router = useRouter();
@ -16,8 +16,8 @@ export const useNavigations = () => {
const pathname = usePathname(); const pathname = usePathname();
return { return {
loading, isLoading,
setLoading, setIsLoading,
open, open,
setOpen, setOpen,
active, active,

View File

@ -16,8 +16,8 @@ const defaultUrl = process.env.VERCEL_URL
export const metadata = { export const metadata = {
metadataBase: new URL(defaultUrl), metadataBase: new URL(defaultUrl),
title: "Next.js and Supabase Starter Kit", title: "Sigap | Jember ",
description: "The fastest way to build apps with Next.js and Supabase", description: "Sigap is a platform for managing your crime data.",
}; };
const geistSans = Geist({ const geistSans = Geist({
@ -39,7 +39,6 @@ export default function RootLayout({
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >
<ReactQueryProvider>
<main className="min-h-screen flex flex-col items-center"> <main className="min-h-screen flex flex-col items-center">
<div className="flex-1 w-full gap-20 items-center"> <div className="flex-1 w-full gap-20 items-center">
{/* <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16"> {/* <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
@ -65,7 +64,7 @@ export default function RootLayout({
{/* <footer className="w-full flex items-center justify-center border-t mx-auto text-center text-xs gap-8 py-16 mt-auto"> {/* <footer className="w-full flex items-center justify-center border-t mx-auto text-center text-xs gap-8 py-16 mt-auto">
<p> <p>
Powered by{" "} Powered by{" "
<a <a
href="" href=""
target="_blank" target="_blank"
@ -78,7 +77,6 @@ export default function RootLayout({
</footer> */} </footer> */}
</div> </div>
</main> </main>
</ReactQueryProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@ -132,7 +132,7 @@ model geographics {
model profiles { model profiles {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user_id String @unique @db.Uuid user_id String @unique @db.Uuid
avatar String? @db.VarChar(255) avatar String? @db.VarChar(355)
username String? @unique @db.VarChar(255) username String? @unique @db.VarChar(255)
first_name String? @db.VarChar(255) first_name String? @db.VarChar(255)
last_name String? @db.VarChar(255) last_name String? @db.VarChar(255)

View File

@ -57,7 +57,7 @@ export class AuthRepository {
return { return {
data, data,
redirectTo: "/protected/dashboard" redirectTo: "/dashboard",
}; };
} }
} }

View File

@ -14,3 +14,24 @@ export function encodedRedirect(
) { ) {
return redirect(`${path}?${type}=${encodeURIComponent(message)}`); return redirect(`${path}?${type}=${encodeURIComponent(message)}`);
} }
/**
* Formats a URL by removing any trailing slashes.
* @param {string} url - The URL to format.
* @returns {string} The formatted URL.
*/
// Helper function to ensure URLs are properly formatted
export function formatUrl(url: string): string {
// If URL starts with a slash, it's already absolute
if (url.startsWith("/")) {
return url;
}
// Otherwise, ensure it's properly formatted relative to root
// Remove any potential duplicated '/dashboard' prefixes
if (url.startsWith("dashboard/")) {
return "/" + url;
}
return "/" + url;
}