mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Update
This commit is contained in:
1 parent
b10eae1bf3
commit
a15089f607
20 files changed
+3566
-3654
No files matched your search
@@ -1,2 +1,5 @@
|
||||
VITE_SUPABASE_URL=your_supabase_url
|
||||
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
|
||||
VITE_OPENAI_API_KEY=your_openai_api_key
|
||||
VITE_OPENROUTER_API_KEY=your_openrouter_api_key
|
||||
VITE_AI_MODEL=openai/gpt-4o-mini
|
||||
@@ -8,6 +8,9 @@ services:
|
||||
environment:
|
||||
- VITE_SUPABASE_URL=${VITE_SUPABASE_URL}
|
||||
- VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY}
|
||||
- VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}
|
||||
- VITE_OPENROUTER_API_KEY=${VITE_OPENROUTER_API_KEY}
|
||||
- VITE_AI_MODEL=${VITE_AI_MODEL}
|
||||
volumes:
|
||||
- ./src:/app/apps/admin/src
|
||||
- ./index.html:/app/apps/admin/index.html
|
||||
|
||||
+26
-5
@@ -2,24 +2,45 @@
|
||||
"name": "@sauci/admin",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.39.0",
|
||||
"ra-supabase": "^1.0.0",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sauci/shared": "*",
|
||||
"@supabase/supabase-js": "^2.39.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.2.0",
|
||||
"lucide-react": "^0.309.0",
|
||||
"openai": "^4.24.0",
|
||||
"react": "^18.2.0",
|
||||
"react-admin": "^4.16.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.0",
|
||||
"recharts": "^3.6.0",
|
||||
"sonner": "^1.3.1",
|
||||
"tailwind-merge": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
-71
@@ -1,81 +1,93 @@
|
||||
import { Admin, Resource } from "react-admin";
|
||||
import { auditDataProvider } from "./providers/auditDataProvider";
|
||||
import { PackList, PackEdit, PackCreate } from "./resources/packs";
|
||||
import { QuestionList, QuestionEdit, QuestionCreate } from "./resources/questions";
|
||||
import { ProfileList, ProfileShow, ProfileEdit, ResponseList } from "./resources/users";
|
||||
import { CoupleList } from "./resources/couples";
|
||||
import { MatchList, MatchShow } from "./resources/matches";
|
||||
import { AuditLogList } from "./resources/auditLogs";
|
||||
import { authProvider, AdminPermissions } from "./providers/authProvider";
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider } from '@/contexts/AuthContext';
|
||||
import { ProtectedRoute } from '@/components/ProtectedRoute';
|
||||
import { AppLayout } from '@/components/layout/AppLayout';
|
||||
import { LoginPage } from '@/pages/LoginPage';
|
||||
import { DashboardPage } from '@/pages/DashboardPage';
|
||||
import { CategoriesPage } from '@/pages/content/CategoriesPage';
|
||||
import { PacksPage } from '@/pages/content/PacksPage';
|
||||
import { QuestionsPage } from '@/pages/content/QuestionsPage';
|
||||
import { UsersPage } from '@/pages/users/UsersPage';
|
||||
import { UserDetailPage } from '@/pages/users/UserDetailPage';
|
||||
import { MatchChatPage } from '@/pages/users/MatchChatPage';
|
||||
import { AuditLogsPage } from '@/pages/AuditLogsPage';
|
||||
import { AdminsPage } from '@/pages/admins/AdminsPage';
|
||||
|
||||
const App = () => (
|
||||
<Admin
|
||||
dataProvider={auditDataProvider}
|
||||
authProvider={authProvider}
|
||||
requireAuth
|
||||
>
|
||||
{(permissions: AdminPermissions) => (
|
||||
<>
|
||||
{/* Question Packs - accessible to all admins */}
|
||||
<Resource
|
||||
name="question_packs"
|
||||
list={PackList}
|
||||
edit={PackEdit}
|
||||
create={PackCreate}
|
||||
recordRepresentation="name"
|
||||
options={{ label: 'Question Packs' }}
|
||||
/>
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
{/* Questions - accessible to all admins */}
|
||||
<Resource
|
||||
name="questions"
|
||||
list={QuestionList}
|
||||
edit={QuestionEdit}
|
||||
create={QuestionCreate}
|
||||
options={{ label: 'Questions' }}
|
||||
/>
|
||||
{/* Protected routes */}
|
||||
<Route
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AppLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
{/* Dashboard */}
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
|
||||
{/* Super admin only resources */}
|
||||
{permissions?.role === 'super_admin' && (
|
||||
<>
|
||||
<Resource
|
||||
name="profiles"
|
||||
list={ProfileList}
|
||||
show={ProfileShow}
|
||||
edit={ProfileEdit}
|
||||
recordRepresentation="name"
|
||||
options={{ label: 'Users' }}
|
||||
{/* Content management */}
|
||||
<Route path="/categories" element={<CategoriesPage />} />
|
||||
<Route path="/categories/:categoryId/packs" element={<PacksPage />} />
|
||||
<Route path="/packs/:packId/questions" element={<QuestionsPage />} />
|
||||
|
||||
{/* User management (super_admin only) */}
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
<ProtectedRoute requireSuperAdmin>
|
||||
<UsersPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="couples"
|
||||
list={CoupleList}
|
||||
options={{ label: 'Relationships' }}
|
||||
<Route
|
||||
path="/users/:userId"
|
||||
element={
|
||||
<ProtectedRoute requireSuperAdmin>
|
||||
<UserDetailPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="matches"
|
||||
list={MatchList}
|
||||
show={MatchShow}
|
||||
options={{ label: 'Matches & Chats' }}
|
||||
<Route
|
||||
path="/users/:userId/matches/:matchId"
|
||||
element={
|
||||
<ProtectedRoute requireSuperAdmin>
|
||||
<MatchChatPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="responses"
|
||||
list={ResponseList}
|
||||
options={{ label: 'Responses' }}
|
||||
|
||||
{/* System (super_admin only) */}
|
||||
<Route
|
||||
path="/admins"
|
||||
element={
|
||||
<ProtectedRoute requireSuperAdmin>
|
||||
<AdminsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="messages"
|
||||
options={{ label: 'Messages' }}
|
||||
<Route
|
||||
path="/audit-logs"
|
||||
element={
|
||||
<ProtectedRoute requireSuperAdmin>
|
||||
<AuditLogsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="audit_logs"
|
||||
list={AuditLogList}
|
||||
options={{ label: 'Audit Logs' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Admin>
|
||||
);
|
||||
</Route>
|
||||
|
||||
{/* Catch all - redirect to dashboard */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -1,136 +0,0 @@
|
||||
import { AuthProvider } from 'react-admin';
|
||||
import { supabase } from '../config';
|
||||
|
||||
// Define admin roles
|
||||
export type AdminRole = 'pack_creator' | 'super_admin';
|
||||
|
||||
// Permission structure
|
||||
export interface AdminPermissions {
|
||||
role: AdminRole;
|
||||
permissions: Array<{ action: string; resource: string }>;
|
||||
}
|
||||
|
||||
// Define permissions by role
|
||||
const ROLE_PERMISSIONS: Record<AdminRole, Array<{ action: string; resource: string }>> = {
|
||||
pack_creator: [
|
||||
{ action: 'list', resource: 'question_packs' },
|
||||
{ action: 'create', resource: 'question_packs' },
|
||||
{ action: 'edit', resource: 'question_packs' },
|
||||
{ action: 'delete', resource: 'question_packs' },
|
||||
{ action: 'show', resource: 'question_packs' },
|
||||
{ action: 'list', resource: 'questions' },
|
||||
{ action: 'create', resource: 'questions' },
|
||||
{ action: 'edit', resource: 'questions' },
|
||||
{ action: 'delete', resource: 'questions' },
|
||||
{ action: 'show', resource: 'questions' },
|
||||
],
|
||||
super_admin: [
|
||||
// Full access to all resources
|
||||
{ action: '*', resource: '*' },
|
||||
],
|
||||
};
|
||||
|
||||
const PERMISSIONS_KEY = 'admin_permissions';
|
||||
|
||||
export const authProvider: AuthProvider = {
|
||||
login: async ({ username, password }) => {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email: username,
|
||||
password,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
// Fetch admin role
|
||||
const { data: adminData, error: adminError } = await supabase
|
||||
.from('admin_users')
|
||||
.select('role')
|
||||
.eq('user_id', data.user.id)
|
||||
.single();
|
||||
|
||||
if (adminError || !adminData) {
|
||||
await supabase.auth.signOut();
|
||||
throw new Error('Access denied. You are not an administrator.');
|
||||
}
|
||||
|
||||
// Store permissions
|
||||
const permissions: AdminPermissions = {
|
||||
role: adminData.role as AdminRole,
|
||||
permissions: ROLE_PERMISSIONS[adminData.role as AdminRole],
|
||||
};
|
||||
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify(permissions));
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
localStorage.removeItem(PERMISSIONS_KEY);
|
||||
await supabase.auth.signOut();
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
checkError: async (error) => {
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
localStorage.removeItem(PERMISSIONS_KEY);
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
const session = await supabase.auth.getSession();
|
||||
const permissions = localStorage.getItem(PERMISSIONS_KEY);
|
||||
|
||||
if (!session.data.session || !permissions) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
getPermissions: async () => {
|
||||
const permissions = localStorage.getItem(PERMISSIONS_KEY);
|
||||
if (!permissions) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return JSON.parse(permissions) as AdminPermissions;
|
||||
},
|
||||
|
||||
canAccess: async ({ resource, action }) => {
|
||||
const permissionsStr = localStorage.getItem(PERMISSIONS_KEY);
|
||||
if (!permissionsStr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { permissions } = JSON.parse(permissionsStr) as AdminPermissions;
|
||||
|
||||
// Check for wildcard (super_admin)
|
||||
if (permissions.some(p => p.action === '*' && p.resource === '*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check specific permission
|
||||
return permissions.some(
|
||||
p => p.resource === resource && (p.action === action || p.action === '*')
|
||||
);
|
||||
},
|
||||
|
||||
getIdentity: async () => {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (!data.user) {
|
||||
throw new Error('No user');
|
||||
}
|
||||
|
||||
const permissionsStr = localStorage.getItem(PERMISSIONS_KEY);
|
||||
const role = permissionsStr
|
||||
? (JSON.parse(permissionsStr) as AdminPermissions).role
|
||||
: 'unknown';
|
||||
|
||||
return {
|
||||
id: data.user.id,
|
||||
fullName: data.user.email,
|
||||
avatar: undefined,
|
||||
role,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -2,7 +2,11 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
@@ -14,7 +18,15 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: 3001,
|
||||
|
||||
Reference in new issue
Block a user