parent
cfec43f7ef
commit
6ec6b0753f
14 changed files with 1849 additions and 330 deletions
@ -1,6 +0,0 @@ |
|||||||
PUBLIC_SUPABASE_URL=https://your-project.supabase.co |
|
||||||
PUBLIC_SUPABASE_ANON_KEY=your-anon-key |
|
||||||
|
|
||||||
# Google Calendar Integration (optional) |
|
||||||
VITE_GOOGLE_CLIENT_ID=your-google-client-id |
|
||||||
VITE_GOOGLE_CLIENT_SECRET=your-google-client-secret |
|
||||||
@ -0,0 +1,61 @@ |
|||||||
|
import { redirect } from '@sveltejs/kit'; |
||||||
|
import type { PageServerLoad } from './$types'; |
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ parent, locals }) => { |
||||||
|
const { org, userRole } = await parent(); |
||||||
|
|
||||||
|
// Only admins and owners can access settings
|
||||||
|
if (userRole !== 'owner' && userRole !== 'admin') { |
||||||
|
redirect(303, `/${(org as any).slug}`); |
||||||
|
} |
||||||
|
|
||||||
|
const orgId = (org as any).id; |
||||||
|
|
||||||
|
// Get org members with profiles
|
||||||
|
const { data: members } = await locals.supabase |
||||||
|
.from('org_members') |
||||||
|
.select(` |
||||||
|
id, |
||||||
|
user_id, |
||||||
|
role, |
||||||
|
role_id, |
||||||
|
created_at, |
||||||
|
profiles:user_id ( |
||||||
|
id, |
||||||
|
email, |
||||||
|
full_name, |
||||||
|
avatar_url |
||||||
|
) |
||||||
|
`)
|
||||||
|
.eq('org_id', orgId); |
||||||
|
|
||||||
|
// Get org roles
|
||||||
|
const { data: roles } = await locals.supabase |
||||||
|
.from('org_roles') |
||||||
|
.select('*') |
||||||
|
.eq('org_id', orgId) |
||||||
|
.order('position'); |
||||||
|
|
||||||
|
// Get pending invites
|
||||||
|
const { data: invites } = await locals.supabase |
||||||
|
.from('org_invites') |
||||||
|
.select('*') |
||||||
|
.eq('org_id', orgId) |
||||||
|
.is('accepted_at', null) |
||||||
|
.gt('expires_at', new Date().toISOString()); |
||||||
|
|
||||||
|
// Get org Google Calendar connection
|
||||||
|
const { data: orgCalendar } = await locals.supabase |
||||||
|
.from('org_google_calendars') |
||||||
|
.select('*') |
||||||
|
.eq('org_id', orgId) |
||||||
|
.single(); |
||||||
|
|
||||||
|
return { |
||||||
|
members: members ?? [], |
||||||
|
roles: roles ?? [], |
||||||
|
invites: invites ?? [], |
||||||
|
orgCalendar, |
||||||
|
userRole |
||||||
|
}; |
||||||
|
}; |
||||||
File diff suppressed because it is too large
Load Diff
@ -1,43 +0,0 @@ |
|||||||
import { redirect } from '@sveltejs/kit'; |
|
||||||
import type { RequestHandler } from './$types'; |
|
||||||
import { exchangeCodeForTokens } from '$lib/api/google-calendar'; |
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ url, locals }) => { |
|
||||||
const code = url.searchParams.get('code'); |
|
||||||
const stateParam = url.searchParams.get('state'); |
|
||||||
const error = url.searchParams.get('error'); |
|
||||||
|
|
||||||
if (error || !code || !stateParam) { |
|
||||||
redirect(303, '/?error=google_auth_failed'); |
|
||||||
} |
|
||||||
|
|
||||||
let state: { orgSlug: string; userId: string }; |
|
||||||
try { |
|
||||||
state = JSON.parse(decodeURIComponent(stateParam)); |
|
||||||
} catch { |
|
||||||
redirect(303, '/?error=invalid_state'); |
|
||||||
} |
|
||||||
|
|
||||||
const redirectUri = `${url.origin}/api/google-calendar/callback`; |
|
||||||
|
|
||||||
try { |
|
||||||
const tokens = await exchangeCodeForTokens(code, redirectUri); |
|
||||||
const expiresAt = new Date(Date.now() + tokens.expires_in * 1000); |
|
||||||
|
|
||||||
// Store tokens in database
|
|
||||||
await locals.supabase |
|
||||||
.from('google_calendar_connections') |
|
||||||
.upsert({ |
|
||||||
user_id: state.userId, |
|
||||||
access_token: tokens.access_token, |
|
||||||
refresh_token: tokens.refresh_token, |
|
||||||
token_expires_at: expiresAt.toISOString(), |
|
||||||
updated_at: new Date().toISOString() |
|
||||||
}, { onConflict: 'user_id' }); |
|
||||||
|
|
||||||
redirect(303, `/${state.orgSlug}/calendar?connected=true`); |
|
||||||
} catch (err) { |
|
||||||
console.error('Google Calendar OAuth error:', err); |
|
||||||
redirect(303, `/${state.orgSlug}/calendar?error=token_exchange_failed`); |
|
||||||
} |
|
||||||
}; |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
import { redirect } from '@sveltejs/kit'; |
|
||||||
import type { RequestHandler } from './$types'; |
|
||||||
import { getGoogleAuthUrl } from '$lib/api/google-calendar'; |
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ url, locals }) => { |
|
||||||
const { session } = await locals.safeGetSession(); |
|
||||||
|
|
||||||
if (!session) { |
|
||||||
redirect(303, '/login'); |
|
||||||
} |
|
||||||
|
|
||||||
const orgSlug = url.searchParams.get('org'); |
|
||||||
if (!orgSlug) { |
|
||||||
redirect(303, '/'); |
|
||||||
} |
|
||||||
|
|
||||||
const redirectUri = `${url.origin}/api/google-calendar/callback`; |
|
||||||
const state = JSON.stringify({ orgSlug, userId: session.user.id }); |
|
||||||
const authUrl = getGoogleAuthUrl(redirectUri, encodeURIComponent(state)); |
|
||||||
|
|
||||||
redirect(303, authUrl); |
|
||||||
}; |
|
||||||
@ -0,0 +1,60 @@ |
|||||||
|
import { json } from '@sveltejs/kit'; |
||||||
|
import type { RequestHandler } from './$types'; |
||||||
|
import { GOOGLE_API_KEY } from '$env/static/private'; |
||||||
|
import { fetchPublicCalendarEvents } from '$lib/api/google-calendar'; |
||||||
|
|
||||||
|
// Fetch events from a public Google Calendar
|
||||||
|
export const GET: RequestHandler = async ({ url, locals }) => { |
||||||
|
const orgId = url.searchParams.get('org_id'); |
||||||
|
|
||||||
|
if (!orgId) { |
||||||
|
return json({ error: 'org_id required' }, { status: 400 }); |
||||||
|
} |
||||||
|
|
||||||
|
if (!GOOGLE_API_KEY) { |
||||||
|
return json({ error: 'Google API key not configured' }, { status: 500 }); |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
// Get the org's calendar ID from database
|
||||||
|
const { data: orgCal, error: dbError } = await locals.supabase |
||||||
|
.from('org_google_calendars') |
||||||
|
.select('calendar_id, calendar_name') |
||||||
|
.eq('org_id', orgId) |
||||||
|
.single(); |
||||||
|
|
||||||
|
if (dbError) { |
||||||
|
console.error('DB error fetching calendar:', dbError); |
||||||
|
return json({ error: 'No calendar connected', events: [] }, { status: 404 }); |
||||||
|
} |
||||||
|
|
||||||
|
if (!orgCal) { |
||||||
|
return json({ error: 'No calendar connected', events: [] }, { status: 404 }); |
||||||
|
} |
||||||
|
|
||||||
|
console.log('Fetching events for calendar:', (orgCal as any).calendar_id); |
||||||
|
|
||||||
|
// Fetch events for the next 3 months
|
||||||
|
const now = new Date(); |
||||||
|
const timeMin = new Date(now.getFullYear(), now.getMonth() - 1, 1); |
||||||
|
const timeMax = new Date(now.getFullYear(), now.getMonth() + 3, 0); |
||||||
|
|
||||||
|
const events = await fetchPublicCalendarEvents( |
||||||
|
(orgCal as any).calendar_id, |
||||||
|
GOOGLE_API_KEY, |
||||||
|
timeMin, |
||||||
|
timeMax |
||||||
|
); |
||||||
|
|
||||||
|
console.log('Fetched', events.length, 'events'); |
||||||
|
|
||||||
|
return json({ |
||||||
|
events, |
||||||
|
calendar_id: (orgCal as any).calendar_id, |
||||||
|
calendar_name: (orgCal as any).calendar_name |
||||||
|
}); |
||||||
|
} catch (err) { |
||||||
|
console.error('Failed to fetch calendar events:', err); |
||||||
|
return json({ error: 'Failed to fetch events. Make sure the calendar is public.', events: [] }, { status: 500 }); |
||||||
|
} |
||||||
|
}; |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
-- Organization-level Google Calendar (shared across all members) |
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS org_google_calendars ( |
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE UNIQUE, |
||||||
|
calendar_id TEXT NOT NULL, -- Google Calendar ID (e.g., "abc123@group.calendar.google.com") |
||||||
|
calendar_name TEXT, |
||||||
|
connected_by UUID REFERENCES auth.users(id), |
||||||
|
access_token TEXT NOT NULL, |
||||||
|
refresh_token TEXT NOT NULL, |
||||||
|
token_expires_at TIMESTAMPTZ NOT NULL, |
||||||
|
created_at TIMESTAMPTZ DEFAULT now(), |
||||||
|
updated_at TIMESTAMPTZ DEFAULT now() |
||||||
|
); |
||||||
|
|
||||||
|
-- Index |
||||||
|
CREATE INDEX IF NOT EXISTS idx_org_google_calendars_org ON org_google_calendars(org_id); |
||||||
|
|
||||||
|
-- RLS |
||||||
|
ALTER TABLE org_google_calendars ENABLE ROW LEVEL SECURITY; |
||||||
|
|
||||||
|
-- All org members can view the org calendar connection |
||||||
|
CREATE POLICY "Org members can view org calendar" ON org_google_calendars |
||||||
|
FOR SELECT USING (EXISTS ( |
||||||
|
SELECT 1 FROM org_members om |
||||||
|
WHERE om.org_id = org_google_calendars.org_id |
||||||
|
AND om.user_id = auth.uid() |
||||||
|
)); |
||||||
|
|
||||||
|
-- Only admins/owners can manage org calendar |
||||||
|
CREATE POLICY "Admins can manage org calendar" ON org_google_calendars |
||||||
|
FOR ALL USING (EXISTS ( |
||||||
|
SELECT 1 FROM org_members om |
||||||
|
WHERE om.org_id = org_google_calendars.org_id |
||||||
|
AND om.user_id = auth.uid() |
||||||
|
AND om.role IN ('owner', 'admin') |
||||||
|
)); |
||||||
@ -0,0 +1,177 @@ |
|||||||
|
-- Custom Roles and Invite System |
||||||
|
|
||||||
|
-- Permission types (similar to Google Drive) |
||||||
|
-- viewer: Can view content |
||||||
|
-- commenter: Can view and comment |
||||||
|
-- editor: Can view, comment, and edit content |
||||||
|
-- admin: Can manage members and settings |
||||||
|
-- owner: Full control |
||||||
|
|
||||||
|
-- Custom roles table (allows vanity roles with custom permissions) |
||||||
|
CREATE TABLE IF NOT EXISTS org_roles ( |
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE, |
||||||
|
name TEXT NOT NULL, |
||||||
|
color TEXT DEFAULT '#6366f1', -- For vanity display |
||||||
|
permissions JSONB NOT NULL DEFAULT '[]'::jsonb, |
||||||
|
is_default BOOLEAN DEFAULT false, -- If this is a default role for new members |
||||||
|
is_system BOOLEAN DEFAULT false, -- System roles can't be deleted |
||||||
|
position INTEGER DEFAULT 0, -- For ordering |
||||||
|
created_at TIMESTAMPTZ DEFAULT now(), |
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(), |
||||||
|
UNIQUE(org_id, name) |
||||||
|
); |
||||||
|
|
||||||
|
-- Available permissions |
||||||
|
COMMENT ON COLUMN org_roles.permissions IS 'Array of permission strings: |
||||||
|
documents.view, documents.create, documents.edit, documents.delete, |
||||||
|
kanban.view, kanban.create, kanban.edit, kanban.delete, |
||||||
|
calendar.view, calendar.create, calendar.edit, calendar.delete, |
||||||
|
members.view, members.invite, members.manage, members.remove, |
||||||
|
roles.view, roles.create, roles.edit, roles.delete, |
||||||
|
settings.view, settings.edit, |
||||||
|
org.delete'; |
||||||
|
|
||||||
|
-- Update org_members to reference custom roles |
||||||
|
ALTER TABLE org_members ADD COLUMN IF NOT EXISTS role_id UUID REFERENCES org_roles(id); |
||||||
|
|
||||||
|
-- Organization invites |
||||||
|
CREATE TABLE IF NOT EXISTS org_invites ( |
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE, |
||||||
|
email TEXT NOT NULL, |
||||||
|
role_id UUID REFERENCES org_roles(id), |
||||||
|
role TEXT DEFAULT 'viewer', -- Fallback if no custom role |
||||||
|
invited_by UUID REFERENCES auth.users(id), |
||||||
|
token TEXT UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'), |
||||||
|
expires_at TIMESTAMPTZ DEFAULT (now() + interval '7 days'), |
||||||
|
accepted_at TIMESTAMPTZ, |
||||||
|
created_at TIMESTAMPTZ DEFAULT now(), |
||||||
|
UNIQUE(org_id, email) |
||||||
|
); |
||||||
|
|
||||||
|
-- Indexes |
||||||
|
CREATE INDEX IF NOT EXISTS idx_org_roles_org ON org_roles(org_id); |
||||||
|
CREATE INDEX IF NOT EXISTS idx_org_invites_org ON org_invites(org_id); |
||||||
|
CREATE INDEX IF NOT EXISTS idx_org_invites_token ON org_invites(token); |
||||||
|
CREATE INDEX IF NOT EXISTS idx_org_invites_email ON org_invites(email); |
||||||
|
|
||||||
|
-- RLS for org_roles |
||||||
|
ALTER TABLE org_roles ENABLE ROW LEVEL SECURITY; |
||||||
|
|
||||||
|
CREATE POLICY "Org members can view roles" ON org_roles FOR SELECT |
||||||
|
USING (EXISTS ( |
||||||
|
SELECT 1 FROM org_members om |
||||||
|
WHERE om.org_id = org_roles.org_id AND om.user_id = auth.uid() |
||||||
|
)); |
||||||
|
|
||||||
|
CREATE POLICY "Admins can manage roles" ON org_roles FOR ALL |
||||||
|
USING (EXISTS ( |
||||||
|
SELECT 1 FROM org_members om |
||||||
|
WHERE om.org_id = org_roles.org_id |
||||||
|
AND om.user_id = auth.uid() |
||||||
|
AND om.role IN ('owner', 'admin') |
||||||
|
)); |
||||||
|
|
||||||
|
-- RLS for org_invites |
||||||
|
ALTER TABLE org_invites ENABLE ROW LEVEL SECURITY; |
||||||
|
|
||||||
|
CREATE POLICY "Org members can view invites" ON org_invites FOR SELECT |
||||||
|
USING (EXISTS ( |
||||||
|
SELECT 1 FROM org_members om |
||||||
|
WHERE om.org_id = org_invites.org_id AND om.user_id = auth.uid() |
||||||
|
)); |
||||||
|
|
||||||
|
CREATE POLICY "Admins can manage invites" ON org_invites FOR ALL |
||||||
|
USING (EXISTS ( |
||||||
|
SELECT 1 FROM org_members om |
||||||
|
WHERE om.org_id = org_invites.org_id |
||||||
|
AND om.user_id = auth.uid() |
||||||
|
AND om.role IN ('owner', 'admin') |
||||||
|
)); |
||||||
|
|
||||||
|
-- Anyone can view invite by token (for accepting) |
||||||
|
CREATE POLICY "Anyone can view invite by token" ON org_invites FOR SELECT |
||||||
|
USING (true); |
||||||
|
|
||||||
|
-- Function to create default roles for new org |
||||||
|
CREATE OR REPLACE FUNCTION create_default_org_roles() |
||||||
|
RETURNS TRIGGER AS $$ |
||||||
|
BEGIN |
||||||
|
-- Owner role (full permissions) |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, position) VALUES |
||||||
|
(NEW.id, 'Owner', '#ef4444', '["*"]'::jsonb, true, 0); |
||||||
|
|
||||||
|
-- Admin role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, position) VALUES |
||||||
|
(NEW.id, 'Admin', '#f59e0b', '[ |
||||||
|
"documents.view", "documents.create", "documents.edit", "documents.delete", |
||||||
|
"kanban.view", "kanban.create", "kanban.edit", "kanban.delete", |
||||||
|
"calendar.view", "calendar.create", "calendar.edit", "calendar.delete", |
||||||
|
"members.view", "members.invite", "members.manage", |
||||||
|
"roles.view", "settings.view", "settings.edit" |
||||||
|
]'::jsonb, true, 1); |
||||||
|
|
||||||
|
-- Editor role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, is_default, position) VALUES |
||||||
|
(NEW.id, 'Editor', '#10b981', '[ |
||||||
|
"documents.view", "documents.create", "documents.edit", |
||||||
|
"kanban.view", "kanban.create", "kanban.edit", |
||||||
|
"calendar.view", "calendar.create", "calendar.edit", |
||||||
|
"members.view" |
||||||
|
]'::jsonb, true, true, 2); |
||||||
|
|
||||||
|
-- Commenter role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, position) VALUES |
||||||
|
(NEW.id, 'Commenter', '#6366f1', '[ |
||||||
|
"documents.view", |
||||||
|
"kanban.view", |
||||||
|
"calendar.view", |
||||||
|
"members.view" |
||||||
|
]'::jsonb, true, 3); |
||||||
|
|
||||||
|
-- Viewer role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, position) VALUES |
||||||
|
(NEW.id, 'Viewer', '#8b5cf6', '[ |
||||||
|
"documents.view", |
||||||
|
"kanban.view", |
||||||
|
"calendar.view", |
||||||
|
"members.view" |
||||||
|
]'::jsonb, true, 4); |
||||||
|
|
||||||
|
RETURN NEW; |
||||||
|
END; |
||||||
|
$$ LANGUAGE plpgsql; |
||||||
|
|
||||||
|
-- Trigger to create default roles |
||||||
|
DROP TRIGGER IF EXISTS on_org_created_create_roles ON organizations; |
||||||
|
CREATE TRIGGER on_org_created_create_roles |
||||||
|
AFTER INSERT ON organizations |
||||||
|
FOR EACH ROW EXECUTE FUNCTION create_default_org_roles(); |
||||||
|
|
||||||
|
-- Insert default roles for existing organizations |
||||||
|
DO $$ |
||||||
|
DECLARE |
||||||
|
org RECORD; |
||||||
|
BEGIN |
||||||
|
FOR org IN SELECT id FROM organizations LOOP |
||||||
|
-- Only insert if no roles exist |
||||||
|
IF NOT EXISTS (SELECT 1 FROM org_roles WHERE org_id = org.id) THEN |
||||||
|
-- Owner role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, position) VALUES |
||||||
|
(org.id, 'Owner', '#ef4444', '["*"]'::jsonb, true, 0); |
||||||
|
-- Admin role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, position) VALUES |
||||||
|
(org.id, 'Admin', '#f59e0b', '["documents.view", "documents.create", "documents.edit", "documents.delete", "kanban.view", "kanban.create", "kanban.edit", "kanban.delete", "calendar.view", "calendar.create", "calendar.edit", "calendar.delete", "members.view", "members.invite", "members.manage", "roles.view", "settings.view", "settings.edit"]'::jsonb, true, 1); |
||||||
|
-- Editor role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, is_default, position) VALUES |
||||||
|
(org.id, 'Editor', '#10b981', '["documents.view", "documents.create", "documents.edit", "kanban.view", "kanban.create", "kanban.edit", "calendar.view", "calendar.create", "calendar.edit", "members.view"]'::jsonb, true, true, 2); |
||||||
|
-- Commenter role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, position) VALUES |
||||||
|
(org.id, 'Commenter', '#6366f1', '["documents.view", "kanban.view", "calendar.view", "members.view"]'::jsonb, true, 3); |
||||||
|
-- Viewer role |
||||||
|
INSERT INTO org_roles (org_id, name, color, permissions, is_system, position) VALUES |
||||||
|
(org.id, 'Viewer', '#8b5cf6', '["documents.view", "kanban.view", "calendar.view", "members.view"]'::jsonb, true, 4); |
||||||
|
END IF; |
||||||
|
END LOOP; |
||||||
|
END $$; |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
-- Simplify Google Calendar integration to use public calendars only |
||||||
|
-- No OAuth needed - just store calendar ID and fetch with API key |
||||||
|
|
||||||
|
-- Drop the old OAuth-based table |
||||||
|
DROP TABLE IF EXISTS org_google_calendars CASCADE; |
||||||
|
DROP TABLE IF EXISTS google_calendar_connections CASCADE; |
||||||
|
|
||||||
|
-- Create simplified org calendar table |
||||||
|
CREATE TABLE IF NOT EXISTS org_google_calendars ( |
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE UNIQUE, |
||||||
|
calendar_id TEXT NOT NULL, -- The public calendar ID (email format) |
||||||
|
calendar_name TEXT, -- Display name |
||||||
|
connected_by UUID REFERENCES auth.users(id), |
||||||
|
created_at TIMESTAMPTZ DEFAULT now(), |
||||||
|
updated_at TIMESTAMPTZ DEFAULT now() |
||||||
|
); |
||||||
|
|
||||||
|
-- RLS policies |
||||||
|
ALTER TABLE org_google_calendars ENABLE ROW LEVEL SECURITY; |
||||||
|
|
||||||
|
-- Members can view org calendar |
||||||
|
CREATE POLICY "Members can view org calendar" ON org_google_calendars |
||||||
|
FOR SELECT USING ( |
||||||
|
EXISTS ( |
||||||
|
SELECT 1 FROM org_members |
||||||
|
WHERE org_members.org_id = org_google_calendars.org_id |
||||||
|
AND org_members.user_id = auth.uid() |
||||||
|
) |
||||||
|
); |
||||||
|
|
||||||
|
-- Admins/owners can manage org calendar |
||||||
|
CREATE POLICY "Admins can manage org calendar" ON org_google_calendars |
||||||
|
FOR ALL USING ( |
||||||
|
EXISTS ( |
||||||
|
SELECT 1 FROM org_members |
||||||
|
WHERE org_members.org_id = org_google_calendars.org_id |
||||||
|
AND org_members.user_id = auth.uid() |
||||||
|
AND org_members.role IN ('admin', 'owner') |
||||||
|
) |
||||||
|
); |
||||||
|
|
||||||
|
-- Enable realtime |
||||||
|
ALTER PUBLICATION supabase_realtime ADD TABLE org_google_calendars; |
||||||
Loading…
Reference in new issue