Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
4ca9d41165
commit
1fdd309ca8
Generated
+8328
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lovable.dev/cloud-auth-js": "^1.1.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// This file is auto-generated by Lovable. Do not modify it.
|
||||
|
||||
import { createLovableAuth } from "@lovable.dev/cloud-auth-js";
|
||||
import { supabase } from "../supabase/client";
|
||||
const lovableAuth = createLovableAuth();
|
||||
|
||||
type SignInOptions = {
|
||||
redirect_uri?: string;
|
||||
extraParams?: Record<string, string>;
|
||||
};
|
||||
|
||||
export const lovable = {
|
||||
auth: {
|
||||
signInWithOAuth: async (provider: "google" | "apple" | "microsoft" | "lovable", opts?: SignInOptions) => {
|
||||
const result = await lovableAuth.signInWithOAuth(provider, {
|
||||
redirect_uri: opts?.redirect_uri,
|
||||
extraParams: {
|
||||
...opts?.extraParams,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.redirected) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
await supabase.auth.setSession(result.tokens);
|
||||
} catch (e) {
|
||||
return { error: e instanceof Error ? e : new Error(String(e)) };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -14,16 +14,526 @@ export type Database = {
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
[_ in never]: never
|
||||
attendance: {
|
||||
Row: {
|
||||
created_at: string
|
||||
date: string
|
||||
id: string
|
||||
note: string | null
|
||||
recorded_by: string | null
|
||||
status: Database["public"]["Enums"]["attendance_status"]
|
||||
student_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
date: string
|
||||
id?: string
|
||||
note?: string | null
|
||||
recorded_by?: string | null
|
||||
status?: Database["public"]["Enums"]["attendance_status"]
|
||||
student_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
date?: string
|
||||
id?: string
|
||||
note?: string | null
|
||||
recorded_by?: string | null
|
||||
status?: Database["public"]["Enums"]["attendance_status"]
|
||||
student_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "attendance_student_id_fkey"
|
||||
columns: ["student_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "students"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
authorized_pickups: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
name: string
|
||||
phone: string | null
|
||||
relationship: string | null
|
||||
student_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
name: string
|
||||
phone?: string | null
|
||||
relationship?: string | null
|
||||
student_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
name?: string
|
||||
phone?: string | null
|
||||
relationship?: string | null
|
||||
student_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "authorized_pickups_student_id_fkey"
|
||||
columns: ["student_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "students"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
calendar_events: {
|
||||
Row: {
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
date: string
|
||||
description: string | null
|
||||
end_date: string | null
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
date: string
|
||||
description?: string | null
|
||||
end_date?: string | null
|
||||
id?: string
|
||||
title: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
date?: string
|
||||
description?: string | null
|
||||
end_date?: string | null
|
||||
id?: string
|
||||
title?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
classes: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
name: string
|
||||
teacher_id: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
name: string
|
||||
teacher_id?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
name?: string
|
||||
teacher_id?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
contracts: {
|
||||
Row: {
|
||||
created_at: string
|
||||
file_path: string
|
||||
id: string
|
||||
student_id: string
|
||||
title: string | null
|
||||
uploaded_by: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
file_path: string
|
||||
id?: string
|
||||
student_id: string
|
||||
title?: string | null
|
||||
uploaded_by?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
file_path?: string
|
||||
id?: string
|
||||
student_id?: string
|
||||
title?: string | null
|
||||
uploaded_by?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "contracts_student_id_fkey"
|
||||
columns: ["student_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "students"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
form_responses: {
|
||||
Row: {
|
||||
data_json: Json
|
||||
form_id: string
|
||||
id: string
|
||||
student_id: string | null
|
||||
submitted_at: string
|
||||
submitted_by: string
|
||||
}
|
||||
Insert: {
|
||||
data_json?: Json
|
||||
form_id: string
|
||||
id?: string
|
||||
student_id?: string | null
|
||||
submitted_at?: string
|
||||
submitted_by: string
|
||||
}
|
||||
Update: {
|
||||
data_json?: Json
|
||||
form_id?: string
|
||||
id?: string
|
||||
student_id?: string | null
|
||||
submitted_at?: string
|
||||
submitted_by?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "form_responses_form_id_fkey"
|
||||
columns: ["form_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "forms"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "form_responses_student_id_fkey"
|
||||
columns: ["student_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "students"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
forms: {
|
||||
Row: {
|
||||
active: boolean
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
description: string | null
|
||||
id: string
|
||||
schema_json: Json
|
||||
title: string
|
||||
}
|
||||
Insert: {
|
||||
active?: boolean
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
description?: string | null
|
||||
id?: string
|
||||
schema_json?: Json
|
||||
title: string
|
||||
}
|
||||
Update: {
|
||||
active?: boolean
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
description?: string | null
|
||||
id?: string
|
||||
schema_json?: Json
|
||||
title?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
ledger_entries: {
|
||||
Row: {
|
||||
amount_cents: number
|
||||
category: Database["public"]["Enums"]["ledger_category"]
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
date: string
|
||||
id: string
|
||||
kind: Database["public"]["Enums"]["ledger_kind"]
|
||||
note: string | null
|
||||
student_id: string
|
||||
}
|
||||
Insert: {
|
||||
amount_cents: number
|
||||
category?: Database["public"]["Enums"]["ledger_category"]
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
date?: string
|
||||
id?: string
|
||||
kind: Database["public"]["Enums"]["ledger_kind"]
|
||||
note?: string | null
|
||||
student_id: string
|
||||
}
|
||||
Update: {
|
||||
amount_cents?: number
|
||||
category?: Database["public"]["Enums"]["ledger_category"]
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
date?: string
|
||||
id?: string
|
||||
kind?: Database["public"]["Enums"]["ledger_kind"]
|
||||
note?: string | null
|
||||
student_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "ledger_entries_student_id_fkey"
|
||||
columns: ["student_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "students"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
message_threads: {
|
||||
Row: {
|
||||
created_at: string
|
||||
created_by: string
|
||||
id: string
|
||||
student_id: string | null
|
||||
subject: string | null
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
created_by: string
|
||||
id?: string
|
||||
student_id?: string | null
|
||||
subject?: string | null
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
created_by?: string
|
||||
id?: string
|
||||
student_id?: string | null
|
||||
subject?: string | null
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "message_threads_student_id_fkey"
|
||||
columns: ["student_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "students"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
messages: {
|
||||
Row: {
|
||||
body: string
|
||||
created_at: string
|
||||
id: string
|
||||
sender_id: string
|
||||
thread_id: string
|
||||
}
|
||||
Insert: {
|
||||
body: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
sender_id: string
|
||||
thread_id: string
|
||||
}
|
||||
Update: {
|
||||
body?: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
sender_id?: string
|
||||
thread_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "messages_thread_id_fkey"
|
||||
columns: ["thread_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "message_threads"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
parent_students: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
parent_id: string
|
||||
relationship: string | null
|
||||
student_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
parent_id: string
|
||||
relationship?: string | null
|
||||
student_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
parent_id?: string
|
||||
relationship?: string | null
|
||||
student_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "parent_students_student_id_fkey"
|
||||
columns: ["student_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "students"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
profiles: {
|
||||
Row: {
|
||||
created_at: string
|
||||
email: string | null
|
||||
full_name: string | null
|
||||
id: string
|
||||
phone: string | null
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
email?: string | null
|
||||
full_name?: string | null
|
||||
id: string
|
||||
phone?: string | null
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
email?: string | null
|
||||
full_name?: string | null
|
||||
id?: string
|
||||
phone?: string | null
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
students: {
|
||||
Row: {
|
||||
allergies: string | null
|
||||
class_id: string | null
|
||||
created_at: string
|
||||
dob: string | null
|
||||
first_name: string
|
||||
id: string
|
||||
last_name: string
|
||||
notes: string | null
|
||||
photo_release: boolean
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
allergies?: string | null
|
||||
class_id?: string | null
|
||||
created_at?: string
|
||||
dob?: string | null
|
||||
first_name: string
|
||||
id?: string
|
||||
last_name: string
|
||||
notes?: string | null
|
||||
photo_release?: boolean
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
allergies?: string | null
|
||||
class_id?: string | null
|
||||
created_at?: string
|
||||
dob?: string | null
|
||||
first_name?: string
|
||||
id?: string
|
||||
last_name?: string
|
||||
notes?: string | null
|
||||
photo_release?: boolean
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "students_class_id_fkey"
|
||||
columns: ["class_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "classes"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
thread_participants: {
|
||||
Row: {
|
||||
id: string
|
||||
thread_id: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
thread_id: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
thread_id?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "thread_participants_thread_id_fkey"
|
||||
columns: ["thread_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "message_threads"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
user_roles: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
role: Database["public"]["Enums"]["app_role"]
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
role: Database["public"]["Enums"]["app_role"]
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
role?: Database["public"]["Enums"]["app_role"]
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
current_user_has_role: {
|
||||
Args: { _role: Database["public"]["Enums"]["app_role"] }
|
||||
Returns: boolean
|
||||
}
|
||||
has_role: {
|
||||
Args: {
|
||||
_role: Database["public"]["Enums"]["app_role"]
|
||||
_user_id: string
|
||||
}
|
||||
Returns: boolean
|
||||
}
|
||||
is_parent_of: { Args: { _student: string }; Returns: boolean }
|
||||
is_thread_participant: { Args: { _thread: string }; Returns: boolean }
|
||||
teaches_student: { Args: { _student: string }; Returns: boolean }
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
app_role: "admin" | "teacher" | "parent"
|
||||
attendance_status: "present" | "absent" | "late" | "excused"
|
||||
ledger_category: "tuition" | "late_pickup" | "activity" | "other"
|
||||
ledger_kind: "charge" | "payment"
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
@@ -150,6 +660,11 @@ export type CompositeTypes<
|
||||
|
||||
export const Constants = {
|
||||
public: {
|
||||
Enums: {},
|
||||
Enums: {
|
||||
app_role: ["admin", "teacher", "parent"],
|
||||
attendance_status: ["present", "absent", "late", "excused"],
|
||||
ledger_category: ["tuition", "late_pickup", "activity", "other"],
|
||||
ledger_kind: ["charge", "payment"],
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
|
||||
-- ============ ENUMS ============
|
||||
CREATE TYPE public.app_role AS ENUM ('admin', 'teacher', 'parent');
|
||||
CREATE TYPE public.attendance_status AS ENUM ('present', 'absent', 'late', 'excused');
|
||||
CREATE TYPE public.ledger_kind AS ENUM ('charge', 'payment');
|
||||
CREATE TYPE public.ledger_category AS ENUM ('tuition', 'late_pickup', 'activity', 'other');
|
||||
|
||||
-- ============ PROFILES ============
|
||||
CREATE TABLE public.profiles (
|
||||
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
full_name TEXT,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE ON public.profiles TO authenticated;
|
||||
GRANT ALL ON public.profiles TO service_role;
|
||||
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ USER ROLES ============
|
||||
CREATE TABLE public.user_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
role app_role NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (user_id, role)
|
||||
);
|
||||
GRANT SELECT ON public.user_roles TO authenticated;
|
||||
GRANT ALL ON public.user_roles TO service_role;
|
||||
ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.has_role(_user_id UUID, _role app_role)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT EXISTS (SELECT 1 FROM public.user_roles WHERE user_id = _user_id AND role = _role)
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.current_user_has_role(_role app_role)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT EXISTS (SELECT 1 FROM public.user_roles WHERE user_id = auth.uid() AND role = _role)
|
||||
$$;
|
||||
|
||||
-- ============ CLASSES ============
|
||||
CREATE TABLE public.classes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
teacher_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.classes TO authenticated;
|
||||
GRANT ALL ON public.classes TO service_role;
|
||||
ALTER TABLE public.classes ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ STUDENTS ============
|
||||
CREATE TABLE public.students (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
dob DATE,
|
||||
class_id UUID REFERENCES public.classes(id) ON DELETE SET NULL,
|
||||
allergies TEXT,
|
||||
photo_release BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.students TO authenticated;
|
||||
GRANT ALL ON public.students TO service_role;
|
||||
ALTER TABLE public.students ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ PARENT-STUDENT LINKS ============
|
||||
CREATE TABLE public.parent_students (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
parent_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
||||
relationship TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (parent_id, student_id)
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.parent_students TO authenticated;
|
||||
GRANT ALL ON public.parent_students TO service_role;
|
||||
ALTER TABLE public.parent_students ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.is_parent_of(_student UUID)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT EXISTS (SELECT 1 FROM public.parent_students WHERE parent_id = auth.uid() AND student_id = _student)
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.teaches_student(_student UUID)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.students s
|
||||
JOIN public.classes c ON c.id = s.class_id
|
||||
WHERE s.id = _student AND c.teacher_id = auth.uid()
|
||||
)
|
||||
$$;
|
||||
|
||||
-- ============ AUTHORIZED PICKUPS ============
|
||||
CREATE TABLE public.authorized_pickups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
relationship TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.authorized_pickups TO authenticated;
|
||||
GRANT ALL ON public.authorized_pickups TO service_role;
|
||||
ALTER TABLE public.authorized_pickups ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ CONTRACTS (admin only) ============
|
||||
CREATE TABLE public.contracts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
||||
file_path TEXT NOT NULL,
|
||||
title TEXT,
|
||||
uploaded_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.contracts TO authenticated;
|
||||
GRANT ALL ON public.contracts TO service_role;
|
||||
ALTER TABLE public.contracts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ ATTENDANCE ============
|
||||
CREATE TABLE public.attendance (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL,
|
||||
status attendance_status NOT NULL DEFAULT 'present',
|
||||
recorded_by UUID REFERENCES auth.users(id),
|
||||
note TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (student_id, date)
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.attendance TO authenticated;
|
||||
GRANT ALL ON public.attendance TO service_role;
|
||||
ALTER TABLE public.attendance ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ LEDGER ============
|
||||
CREATE TABLE public.ledger_entries (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
kind ledger_kind NOT NULL,
|
||||
category ledger_category NOT NULL DEFAULT 'tuition',
|
||||
amount_cents INTEGER NOT NULL,
|
||||
note TEXT,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.ledger_entries TO authenticated;
|
||||
GRANT ALL ON public.ledger_entries TO service_role;
|
||||
ALTER TABLE public.ledger_entries ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ MESSAGING ============
|
||||
CREATE TABLE public.message_threads (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
subject TEXT,
|
||||
student_id UUID REFERENCES public.students(id) ON DELETE SET NULL,
|
||||
created_by UUID NOT NULL REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.message_threads TO authenticated;
|
||||
GRANT ALL ON public.message_threads TO service_role;
|
||||
ALTER TABLE public.message_threads ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE public.thread_participants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
thread_id UUID NOT NULL REFERENCES public.message_threads(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
UNIQUE (thread_id, user_id)
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.thread_participants TO authenticated;
|
||||
GRANT ALL ON public.thread_participants TO service_role;
|
||||
ALTER TABLE public.thread_participants ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.is_thread_participant(_thread UUID)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT EXISTS (SELECT 1 FROM public.thread_participants WHERE thread_id = _thread AND user_id = auth.uid())
|
||||
$$;
|
||||
|
||||
CREATE TABLE public.messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
thread_id UUID NOT NULL REFERENCES public.message_threads(id) ON DELETE CASCADE,
|
||||
sender_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
body TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT ON public.messages TO authenticated;
|
||||
GRANT ALL ON public.messages TO service_role;
|
||||
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ FORMS ============
|
||||
CREATE TABLE public.forms (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
schema_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.forms TO authenticated;
|
||||
GRANT ALL ON public.forms TO service_role;
|
||||
ALTER TABLE public.forms ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE public.form_responses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
form_id UUID NOT NULL REFERENCES public.forms(id) ON DELETE CASCADE,
|
||||
student_id UUID REFERENCES public.students(id) ON DELETE SET NULL,
|
||||
submitted_by UUID NOT NULL REFERENCES auth.users(id),
|
||||
data_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
submitted_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT ON public.form_responses TO authenticated;
|
||||
GRANT ALL ON public.form_responses TO service_role;
|
||||
ALTER TABLE public.form_responses ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ CALENDAR ============
|
||||
CREATE TABLE public.calendar_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
date DATE NOT NULL,
|
||||
end_date DATE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.calendar_events TO authenticated;
|
||||
GRANT ALL ON public.calendar_events TO service_role;
|
||||
ALTER TABLE public.calendar_events ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============ POLICIES ============
|
||||
|
||||
-- profiles: each user sees their own; admins see all
|
||||
CREATE POLICY "own profile read" ON public.profiles FOR SELECT TO authenticated
|
||||
USING (id = auth.uid() OR public.current_user_has_role('admin'));
|
||||
CREATE POLICY "own profile update" ON public.profiles FOR UPDATE TO authenticated
|
||||
USING (id = auth.uid() OR public.current_user_has_role('admin'));
|
||||
CREATE POLICY "own profile insert" ON public.profiles FOR INSERT TO authenticated
|
||||
WITH CHECK (id = auth.uid());
|
||||
|
||||
-- user_roles: users see their own, admins manage all
|
||||
CREATE POLICY "see own roles" ON public.user_roles FOR SELECT TO authenticated
|
||||
USING (user_id = auth.uid() OR public.current_user_has_role('admin'));
|
||||
CREATE POLICY "admins manage roles" ON public.user_roles FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
||||
|
||||
-- classes
|
||||
CREATE POLICY "classes read" ON public.classes FOR SELECT TO authenticated
|
||||
USING (public.current_user_has_role('admin') OR teacher_id = auth.uid() OR public.current_user_has_role('parent'));
|
||||
CREATE POLICY "admins manage classes" ON public.classes FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
||||
|
||||
-- students
|
||||
CREATE POLICY "students read" ON public.students FOR SELECT TO authenticated
|
||||
USING (
|
||||
public.current_user_has_role('admin')
|
||||
OR public.is_parent_of(id)
|
||||
OR (class_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.classes c WHERE c.id = class_id AND c.teacher_id = auth.uid()))
|
||||
);
|
||||
CREATE POLICY "admins manage students" ON public.students FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
||||
|
||||
-- parent_students
|
||||
CREATE POLICY "parent_students read" ON public.parent_students FOR SELECT TO authenticated
|
||||
USING (
|
||||
public.current_user_has_role('admin')
|
||||
OR parent_id = auth.uid()
|
||||
OR public.teaches_student(student_id)
|
||||
);
|
||||
CREATE POLICY "admins manage parent_students" ON public.parent_students FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
||||
|
||||
-- authorized_pickups
|
||||
CREATE POLICY "pickups read" ON public.authorized_pickups FOR SELECT TO authenticated
|
||||
USING (
|
||||
public.current_user_has_role('admin')
|
||||
OR public.is_parent_of(student_id)
|
||||
OR public.teaches_student(student_id)
|
||||
);
|
||||
CREATE POLICY "pickups write parent or admin" ON public.authorized_pickups FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin') OR public.is_parent_of(student_id))
|
||||
WITH CHECK (public.current_user_has_role('admin') OR public.is_parent_of(student_id));
|
||||
|
||||
-- contracts: ADMIN ONLY
|
||||
CREATE POLICY "contracts admin only" ON public.contracts FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
||||
|
||||
-- attendance
|
||||
CREATE POLICY "attendance read" ON public.attendance FOR SELECT TO authenticated
|
||||
USING (
|
||||
public.current_user_has_role('admin')
|
||||
OR public.is_parent_of(student_id)
|
||||
OR public.teaches_student(student_id)
|
||||
);
|
||||
CREATE POLICY "attendance write teacher or admin" ON public.attendance FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin') OR public.teaches_student(student_id))
|
||||
WITH CHECK (public.current_user_has_role('admin') OR public.teaches_student(student_id));
|
||||
|
||||
-- ledger
|
||||
CREATE POLICY "ledger read" ON public.ledger_entries FOR SELECT TO authenticated
|
||||
USING (public.current_user_has_role('admin') OR public.is_parent_of(student_id));
|
||||
CREATE POLICY "ledger admin manage" ON public.ledger_entries FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
||||
|
||||
-- threads
|
||||
CREATE POLICY "threads participants read" ON public.message_threads FOR SELECT TO authenticated
|
||||
USING (public.current_user_has_role('admin') OR public.is_thread_participant(id));
|
||||
CREATE POLICY "threads create" ON public.message_threads FOR INSERT TO authenticated
|
||||
WITH CHECK (created_by = auth.uid());
|
||||
CREATE POLICY "threads admin manage" ON public.message_threads FOR UPDATE TO authenticated
|
||||
USING (public.current_user_has_role('admin'));
|
||||
|
||||
CREATE POLICY "tp read" ON public.thread_participants FOR SELECT TO authenticated
|
||||
USING (public.current_user_has_role('admin') OR user_id = auth.uid() OR public.is_thread_participant(thread_id));
|
||||
CREATE POLICY "tp insert" ON public.thread_participants FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
public.current_user_has_role('admin')
|
||||
OR EXISTS (SELECT 1 FROM public.message_threads t WHERE t.id = thread_id AND t.created_by = auth.uid())
|
||||
);
|
||||
|
||||
CREATE POLICY "msg read" ON public.messages FOR SELECT TO authenticated
|
||||
USING (public.current_user_has_role('admin') OR public.is_thread_participant(thread_id));
|
||||
CREATE POLICY "msg send" ON public.messages FOR INSERT TO authenticated
|
||||
WITH CHECK (sender_id = auth.uid() AND (public.current_user_has_role('admin') OR public.is_thread_participant(thread_id)));
|
||||
|
||||
-- forms
|
||||
CREATE POLICY "forms read all" ON public.forms FOR SELECT TO authenticated USING (TRUE);
|
||||
CREATE POLICY "forms admin manage" ON public.forms FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
||||
|
||||
CREATE POLICY "form_responses read" ON public.form_responses FOR SELECT TO authenticated
|
||||
USING (public.current_user_has_role('admin') OR submitted_by = auth.uid());
|
||||
CREATE POLICY "form_responses submit" ON public.form_responses FOR INSERT TO authenticated
|
||||
WITH CHECK (submitted_by = auth.uid());
|
||||
|
||||
-- calendar
|
||||
CREATE POLICY "calendar read" ON public.calendar_events FOR SELECT TO authenticated USING (TRUE);
|
||||
CREATE POLICY "calendar admin manage" ON public.calendar_events FOR ALL TO authenticated
|
||||
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
||||
|
||||
-- ============ STORAGE POLICIES (contracts bucket: admin only) ============
|
||||
CREATE POLICY "contracts bucket admin read" ON storage.objects FOR SELECT TO authenticated
|
||||
USING (bucket_id = 'contracts' AND public.current_user_has_role('admin'));
|
||||
CREATE POLICY "contracts bucket admin write" ON storage.objects FOR INSERT TO authenticated
|
||||
WITH CHECK (bucket_id = 'contracts' AND public.current_user_has_role('admin'));
|
||||
CREATE POLICY "contracts bucket admin update" ON storage.objects FOR UPDATE TO authenticated
|
||||
USING (bucket_id = 'contracts' AND public.current_user_has_role('admin'));
|
||||
CREATE POLICY "contracts bucket admin delete" ON storage.objects FOR DELETE TO authenticated
|
||||
USING (bucket_id = 'contracts' AND public.current_user_has_role('admin'));
|
||||
|
||||
-- ============ AUTO PROFILE + FIRST-USER-ADMIN TRIGGER ============
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
user_count INTEGER;
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (id, full_name, email)
|
||||
VALUES (NEW.id, COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.email), NEW.email)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
SELECT COUNT(*) INTO user_count FROM public.user_roles;
|
||||
IF user_count = 0 THEN
|
||||
INSERT INTO public.user_roles (user_id, role) VALUES (NEW.id, 'admin');
|
||||
ELSE
|
||||
INSERT INTO public.user_roles (user_id, role) VALUES (NEW.id, 'parent')
|
||||
ON CONFLICT (user_id, role) DO NOTHING;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||
|
||||
-- updated_at trigger function
|
||||
CREATE OR REPLACE FUNCTION public.set_updated_at()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||
BEGIN NEW.updated_at = now(); RETURN NEW; END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_profiles_upd BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
CREATE TRIGGER trg_students_upd BEFORE UPDATE ON public.students FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
CREATE TRIGGER trg_threads_upd BEFORE UPDATE ON public.message_threads FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
Reference in New Issue
Block a user