chore: remove unused profile lock migration and admin_profile_service
This commit is contained in:
@@ -1,13 +1,29 @@
|
||||
import { serve } from "https://deno.land/std@0.203.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
// CORS: allow browser-based web clients (adjust origin in production)
|
||||
// Permit headers the supabase-js client sends (e.g. `apikey`, `x-client-info`) so
|
||||
// browser preflight requests succeed. Keep origin wide for dev; in production
|
||||
// prefer echoing the request origin and enabling credentials only when needed.
|
||||
const CORS_HEADERS: Record<string, string> = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, x-client-info, x-requested-with, Origin, Accept",
|
||||
"Access-Control-Max-Age": "3600",
|
||||
};
|
||||
|
||||
const jsonResponse = (body: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
|
||||
});
|
||||
|
||||
serve(async (req) => {
|
||||
// Handle CORS preflight quickly so browsers can call this function from localhost/dev
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (req.method != "POST") {
|
||||
return jsonResponse({ error: "Method not allowed" }, 405);
|
||||
}
|
||||
@@ -54,8 +70,51 @@ serve(async (req) => {
|
||||
|
||||
const action = payload.action as string | undefined;
|
||||
const userId = payload.userId as string | undefined;
|
||||
if (!action || !userId) {
|
||||
return jsonResponse({ error: "Missing action or userId" }, 400);
|
||||
if (!action) {
|
||||
return jsonResponse({ error: "Missing action" }, 400);
|
||||
}
|
||||
// `list_users` does not require a target userId; other actions do.
|
||||
if (action !== "list_users" && !userId) {
|
||||
return jsonResponse({ error: "Missing userId" }, 400);
|
||||
}
|
||||
|
||||
if (action === "list_users") {
|
||||
const offset = Number(payload.offset ?? 0);
|
||||
const limit = Number(payload.limit ?? 50);
|
||||
const searchQuery = (payload.searchQuery ?? "").toString().trim();
|
||||
|
||||
// Fetch paginated profiles first (enforce server-side pagination)
|
||||
let profilesQuery = adminClient
|
||||
.from("profiles")
|
||||
.select("id, full_name, role")
|
||||
.order("id", { ascending: true })
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
if (searchQuery.length > 0) {
|
||||
profilesQuery = adminClient
|
||||
.from("profiles")
|
||||
.select("id, full_name, role")
|
||||
.ilike("full_name", `%${searchQuery}%`)
|
||||
.order("id", { ascending: true })
|
||||
.range(offset, offset + limit - 1);
|
||||
}
|
||||
|
||||
const { data: profiles, error: profilesError } = await profilesQuery;
|
||||
if (profilesError) return jsonResponse({ error: profilesError.message }, 500);
|
||||
|
||||
const users = [];
|
||||
for (const p of (profiles ?? [])) {
|
||||
const { data: userResp } = await adminClient.auth.admin.getUserById(p.id);
|
||||
users.push({
|
||||
id: p.id,
|
||||
full_name: p.full_name ?? null,
|
||||
role: p.role ?? null,
|
||||
email: userResp?.user?.email ?? null,
|
||||
bannedUntil: userResp?.user?.banned_until ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse({ users });
|
||||
}
|
||||
|
||||
if (action == "get_user") {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { serve } from "https://deno.land/std@0.203.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
// CORS: allow browser-based web clients (adjust origin in production)
|
||||
// Permit headers the supabase-js client sends (e.g. `apikey`, `x-client-info`) so
|
||||
// browser preflight requests succeed. Keep origin wide for dev; in production
|
||||
// prefer echoing the request origin and enabling credentials only when needed.
|
||||
const CORS_HEADERS: Record<string, string> = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, x-client-info, x-requested-with, Origin, Accept",
|
||||
"Access-Control-Max-Age": "3600",
|
||||
};
|
||||
|
||||
const jsonResponse = (body: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
|
||||
});
|
||||
|
||||
serve(async (req) => {
|
||||
// Handle CORS preflight quickly so browsers can call this function from localhost/dev
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
||||
}
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? "";
|
||||
const anonKey = Deno.env.get("SUPABASE_ANON_KEY") ?? "";
|
||||
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
||||
if (!supabaseUrl || !anonKey || !serviceKey) {
|
||||
return jsonResponse({ error: "Missing env configuration" }, 500);
|
||||
}
|
||||
|
||||
// Authenticate caller (must be admin)
|
||||
const authHeader = req.headers.get("Authorization") ?? "";
|
||||
const token = authHeader.replace("Bearer ", "").trim();
|
||||
if (!token) return jsonResponse({ error: "Missing access token" }, 401);
|
||||
|
||||
const authClient = createClient(supabaseUrl, anonKey, {
|
||||
global: { headers: { Authorization: `Bearer ${token}` } },
|
||||
});
|
||||
|
||||
const { data: authData, error: authError } = await authClient.auth.getUser();
|
||||
if (authError || !authData?.user) return jsonResponse({ error: "Unauthorized" }, 401);
|
||||
|
||||
// Use service role client for privileged DB ops
|
||||
const adminClient = createClient(supabaseUrl, serviceKey);
|
||||
|
||||
// Ensure caller is admin in profiles table
|
||||
const { data: profile, error: profileError } = await adminClient
|
||||
.from("profiles")
|
||||
.select("role")
|
||||
.eq("id", authData.user.id)
|
||||
.maybeSingle();
|
||||
const role = (profile?.role ?? "").toString().toLowerCase();
|
||||
if (profileError || role !== "admin") return jsonResponse({ error: "Forbidden" }, 403);
|
||||
|
||||
// Route request
|
||||
const url = new URL(req.url);
|
||||
const id = url.pathname.split("/").pop();
|
||||
|
||||
try {
|
||||
if (req.method === "GET") {
|
||||
if (id && id !== "teams") {
|
||||
const { data, error } = await adminClient
|
||||
.from("teams")
|
||||
.select('*, team_members(*)')
|
||||
.eq('id', id)
|
||||
.maybeSingle();
|
||||
if (error) return jsonResponse({ error: error.message }, 400);
|
||||
return jsonResponse({ team: data });
|
||||
}
|
||||
|
||||
const { data, error } = await adminClient
|
||||
.from("teams")
|
||||
.select('*, team_members(*)')
|
||||
.order('name');
|
||||
if (error) return jsonResponse({ error: error.message }, 400);
|
||||
return jsonResponse({ teams: data });
|
||||
}
|
||||
|
||||
if (req.method === "POST") {
|
||||
// Support both: standard POST-create and POST with an 'action' parameter
|
||||
const body = await req.json();
|
||||
const action = typeof body.action === 'string' ? body.action.toLowerCase() : 'create';
|
||||
|
||||
// Create (default POST)
|
||||
if (action === 'create') {
|
||||
const name = typeof body.name === 'string' ? body.name : undefined;
|
||||
const leaderId = typeof body.leader_id === 'string' ? body.leader_id : undefined;
|
||||
const officeIds = Array.isArray(body.office_ids) ? (body.office_ids as string[]) : [];
|
||||
const members = Array.isArray(body.members) ? (body.members as string[]) : [];
|
||||
|
||||
if (!name || !leaderId || officeIds.length === 0) {
|
||||
return jsonResponse({ error: 'Missing required fields' }, 400);
|
||||
}
|
||||
|
||||
const { data: insertedTeam, error: insertError } = await adminClient
|
||||
.from('teams')
|
||||
.insert({ name, leader_id: leaderId, office_ids: officeIds })
|
||||
.select()
|
||||
.single();
|
||||
if (insertError) return jsonResponse({ error: insertError.message }, 400);
|
||||
|
||||
if (members.length > 0) {
|
||||
const rows = members.map((u) => ({ team_id: (insertedTeam as any).id, user_id: u }));
|
||||
const { error } = await adminClient.from('team_members').insert(rows);
|
||||
if (error) return jsonResponse({ error: error.message }, 400);
|
||||
}
|
||||
|
||||
return jsonResponse({ team: insertedTeam }, 201);
|
||||
}
|
||||
|
||||
// Update (POST with action='update')
|
||||
if (action === 'update') {
|
||||
const teamId = typeof body.id === 'string' ? body.id : undefined;
|
||||
if (!teamId) return jsonResponse({ error: 'Missing team id' }, 400);
|
||||
const name = typeof body.name === 'string' ? body.name : undefined;
|
||||
const leaderId = typeof body.leader_id === 'string' ? body.leader_id : undefined;
|
||||
const officeIds = Array.isArray(body.office_ids) ? (body.office_ids as string[]) : [];
|
||||
const members = Array.isArray(body.members) ? (body.members as string[]) : [];
|
||||
|
||||
const { error: upErr } = await adminClient
|
||||
.from('teams')
|
||||
.update({ name, leader_id: leaderId, office_ids: officeIds })
|
||||
.eq('id', teamId);
|
||||
if (upErr) return jsonResponse({ error: upErr.message }, 400);
|
||||
|
||||
await adminClient.from('team_members').delete().eq('team_id', teamId);
|
||||
if (members.length > 0) {
|
||||
const rows = members.map((u) => ({ team_id: teamId, user_id: u }));
|
||||
const { error } = await adminClient.from('team_members').insert(rows);
|
||||
if (error) return jsonResponse({ error: error.message }, 400);
|
||||
}
|
||||
|
||||
const { data: updated, error: fetchErr } = await adminClient.from('teams').select().eq('id', teamId).maybeSingle();
|
||||
if (fetchErr) return jsonResponse({ error: fetchErr.message }, 400);
|
||||
return jsonResponse({ team: updated });
|
||||
}
|
||||
|
||||
// Delete (POST with action='delete')
|
||||
if (action === 'delete') {
|
||||
const teamId = typeof body.id === 'string' ? body.id : undefined;
|
||||
if (!teamId) return jsonResponse({ error: 'Missing team id' }, 400);
|
||||
await adminClient.from('team_members').delete().eq('team_id', teamId);
|
||||
const { error } = await adminClient.from('teams').delete().eq('id', teamId);
|
||||
if (error) return jsonResponse({ error: error.message }, 400);
|
||||
return jsonResponse({ ok: true });
|
||||
}
|
||||
|
||||
return jsonResponse({ error: 'Unknown POST action' }, 400);
|
||||
}
|
||||
|
||||
if (req.method === "PUT") {
|
||||
if (!id || id === "teams") return jsonResponse({ error: "Missing team id" }, 400);
|
||||
const body = await req.json();
|
||||
const name = body.name as string | undefined;
|
||||
const leaderId = body.leader_id as string | undefined;
|
||||
const officeIds = (body.office_ids as string[] | undefined) ?? [];
|
||||
const members = (body.members as string[] | undefined) ?? [];
|
||||
|
||||
const { error: upErr } = await adminClient.from('teams').update({ name, leader_id: leaderId, office_ids: officeIds }).eq('id', id);
|
||||
if (upErr) return jsonResponse({ error: upErr.message }, 400);
|
||||
|
||||
await adminClient.from('team_members').delete().eq('team_id', id);
|
||||
if (members.length > 0) {
|
||||
const rows = members.map((u) => ({ team_id: id, user_id: u }));
|
||||
const { error } = await adminClient.from('team_members').insert(rows);
|
||||
if (error) return jsonResponse({ error: error.message }, 400);
|
||||
}
|
||||
|
||||
const { data: updated, error: fetchErr } = await adminClient.from('teams').select().eq('id', id).maybeSingle();
|
||||
if (fetchErr) return jsonResponse({ error: fetchErr.message }, 400);
|
||||
return jsonResponse({ team: updated });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE") {
|
||||
if (!id || id === "teams") return jsonResponse({ error: "Missing team id" }, 400);
|
||||
await adminClient.from('team_members').delete().eq('team_id', id);
|
||||
const { error } = await adminClient.from('teams').delete().eq('id', id);
|
||||
if (error) return jsonResponse({ error: error.message }, 400);
|
||||
return jsonResponse({ ok: true });
|
||||
}
|
||||
|
||||
return jsonResponse({ error: "Method not allowed" }, 405);
|
||||
} catch (err) {
|
||||
return jsonResponse({ error: (err as Error).message }, 500);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table public.duty_schedules
|
||||
add column if not exists reliever_ids uuid[] default '{}'::uuid[];
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Enforce that team leaders and team members must be profiles with role = 'it_staff'
|
||||
|
||||
-- Function: validate_team_leader_is_it_staff
|
||||
create or replace function public.validate_team_leader_is_it_staff()
|
||||
returns trigger language plpgsql as $$
|
||||
begin
|
||||
if new.leader_id is not null then
|
||||
perform 1 from public.profiles where id = new.leader_id and role = 'it_staff';
|
||||
if not found then
|
||||
raise exception 'team leader must have role "it_staff"';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Trigger on teams
|
||||
drop trigger if exists trig_validate_team_leader on public.teams;
|
||||
create trigger trig_validate_team_leader
|
||||
before insert or update on public.teams
|
||||
for each row execute function public.validate_team_leader_is_it_staff();
|
||||
|
||||
|
||||
-- Function: validate_team_member_is_it_staff
|
||||
create or replace function public.validate_team_member_is_it_staff()
|
||||
returns trigger language plpgsql as $$
|
||||
begin
|
||||
if new.user_id is not null then
|
||||
perform 1 from public.profiles where id = new.user_id and role = 'it_staff';
|
||||
if not found then
|
||||
raise exception 'team member must have role "it_staff"';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Trigger on team_members
|
||||
drop trigger if exists trig_validate_team_member on public.team_members;
|
||||
create trigger trig_validate_team_member
|
||||
before insert or update on public.team_members
|
||||
for each row execute function public.validate_team_member_is_it_staff();
|
||||
@@ -0,0 +1,49 @@
|
||||
-- Row-level security for teams and team_members
|
||||
|
||||
-- Enable RLS on teams and team_members
|
||||
ALTER TABLE public.teams ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.team_members ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Allow only profiles with role = 'admin' to select/manage teams
|
||||
CREATE POLICY "Admins can manage teams (select)" ON public.teams
|
||||
FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Admins can manage teams (write)" ON public.teams
|
||||
FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role = 'admin'
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- Policies for team_members (admin-only management)
|
||||
CREATE POLICY "Admins can manage team_members (select)" ON public.team_members
|
||||
FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Admins can manage team_members (write)" ON public.team_members
|
||||
FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role = 'admin'
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role = 'admin'
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user