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 {
success: true,
session,
redirectTo: "/protected/dashboard", // or your preferred authenticated route
redirectTo: "/dashboard",
};
}

View File

@ -12,14 +12,16 @@ export const signInAction = async (formData: FormData) => {
try {
// 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 (session && session.user.email === email) {
if (session?.user?.email === email) {
return {
success: true,
message: "Already logged in",
redirectTo: "/protected/dashboard", // or wherever you want to redirect logged in users
message: "You are already signed in",
redirectTo: "/dashboard",
};
}

View File

@ -26,5 +26,5 @@ export const verifyOtpAction = async (formData: FormData) => {
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 { checkSession } from "./_actions/session";
import { createClient } from "@/utils/supabase/client";
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const sessionResult = await checkSession();
// const supabase = createClient();
// If there's an active session, redirect to dashboard
if (sessionResult.success && sessionResult.redirectTo) {
redirect(sessionResult.redirectTo);
}
// const {
// data: { session },
// } = await supabase.auth.getSession();
// if (!session) {
// return redirect("/sign-in");
// }
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) {
return redirect("/sign-in");
}
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>

View File

@ -9,7 +9,6 @@ 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
@ -103,30 +102,34 @@ 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 supabase = await createClient();
const fileExt = file.name.split(".").pop();
const emailName = email.split("@")[0];
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
.from("avatars")
.upload(fileName, file, {
upsert: false,
.upload(filePath, file, {
upsert: true,
contentType: file.type,
});
if (uploadError) {
console.error("Error uploading avatar:", 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({
where: {
id: userId,
@ -134,16 +137,12 @@ export async function uploadAvatar(userId: string, email: string, file: File) {
data: {
profile: {
update: {
avatar: filePath,
avatar: publicUrl,
},
},
},
});
const {
data: { publicUrl },
} = supabase.storage.from("avatars").getPublicUrl(fileName);
return publicUrl;
} catch (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
export async function updateUser(
userId: string,

View File

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

View File

@ -19,6 +19,7 @@ import {
import type * as TablerIcons from "@tabler/icons-react";
import { useNavigations } from "@/app/_hooks/use-navigations";
import { formatUrl } from "@/utils/utils";
interface SubSubItem {
title: string;
@ -40,22 +41,6 @@ interface NavItem {
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 }) {
const router = useNavigations();
const formattedUrl = formatUrl(item.url);

View File

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

View File

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

View File

@ -5,6 +5,9 @@ import { Card, CardContent } from "@/app/_components/ui/card";
import { Users, UserCheck, UserX } from "lucide-react";
import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
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[]) {
const totalUsers = users.length;
@ -25,10 +28,23 @@ function calculateUserStats(users: User[]) {
}
export function UserStats() {
const { data: users = [], isLoading } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
});
const { isLoading, setIsLoading } = useNavigations();
const [users, setUsers] = useState<User[]>([]);
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);

View File

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

View File

@ -56,9 +56,9 @@ const ThemeSwitcherComponent = ({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
variant="outline"
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`}
>
<currentTheme.icon

View File

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

View File

@ -16,8 +16,8 @@ const defaultUrl = process.env.VERCEL_URL
export const metadata = {
metadataBase: new URL(defaultUrl),
title: "Next.js and Supabase Starter Kit",
description: "The fastest way to build apps with Next.js and Supabase",
title: "Sigap | Jember ",
description: "Sigap is a platform for managing your crime data.",
};
const geistSans = Geist({
@ -39,10 +39,9 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<ReactQueryProvider>
<main className="min-h-screen flex flex-col 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">
<main className="min-h-screen flex flex-col 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">
<div className="w-full max-w-5xl flex justify-between items-center p-3 px-5 text-sm">
<div className="flex gap-5 items-center font-semibold">
<Link href={"/"}>
@ -58,14 +57,14 @@ export default function RootLayout({
</div>
</div>
</nav> */}
<div className="flex flex-col max-w-full p-0">
{children}
<Toaster theme="system" richColors position="top-right" />
</div>
<div className="flex flex-col max-w-full p-0">
{children}
<Toaster theme="system" richColors position="top-right" />
</div>
{/* <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>
Powered by{" "}
Powered by{" "
<a
href=""
target="_blank"
@ -76,9 +75,8 @@ export default function RootLayout({
</a>
</p>
</footer> */}
</div>
</main>
</ReactQueryProvider>
</div>
</main>
</ThemeProvider>
</body>
</html>

View File

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

View File

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

View File

@ -14,3 +14,24 @@ export function encodedRedirect(
) {
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;
}