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