resolve RLS issue for upload avatars images
This commit is contained in:
parent
45daf059d3
commit
aa52dd0ca4
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"files.autoSave": "off"
|
||||
"files.autoSave": "afterDelay"
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export const checkSession = async () => {
|
|||
return {
|
||||
success: true,
|
||||
session,
|
||||
redirectTo: "/protected/dashboard", // or your preferred authenticated route
|
||||
redirectTo: "/dashboard",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 || "";
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,7 +39,6 @@ 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">
|
||||
|
@ -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">
|
||||
<p>
|
||||
Powered by{" "}
|
||||
Powered by{" "
|
||||
<a
|
||||
href=""
|
||||
target="_blank"
|
||||
|
@ -78,7 +77,6 @@ export default function RootLayout({
|
|||
</footer> */}
|
||||
</div>
|
||||
</main>
|
||||
</ReactQueryProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -57,7 +57,7 @@ export class AuthRepository {
|
|||
|
||||
return {
|
||||
data,
|
||||
redirectTo: "/protected/dashboard"
|
||||
redirectTo: "/dashboard",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue