Improve SQL tables
This commit is contained in:
parent
7a17c95fec
commit
0c90e31ec2
18 changed files with 257 additions and 382 deletions
|
|
@ -2,9 +2,9 @@
|
|||
"type": "module",
|
||||
"name": "xtablo-api",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"dev": "NODE_ENV=development tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
"start": "NODE_ENV=production node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.14.4",
|
||||
|
|
|
|||
82
api/src/config.ts
Normal file
82
api/src/config.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
export interface AppConfig {
|
||||
NODE_ENV: "development" | "production" | "test";
|
||||
PORT: number;
|
||||
FRONTEND_URL: string;
|
||||
SUPABASE_URL: string;
|
||||
SUPABASE_SERVICE_ROLE_KEY: string;
|
||||
STREAM_CHAT_API_KEY: string;
|
||||
STREAM_CHAT_API_SECRET: string;
|
||||
EMAIL_USER: string;
|
||||
EMAIL_KEY: string;
|
||||
XTABLO_URL: string;
|
||||
CORS_ORIGIN: string[];
|
||||
LOG_LEVEL: "debug" | "info" | "warn" | "error";
|
||||
}
|
||||
|
||||
function validateEnvVar(name: string, value: string | undefined): string {
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function createConfig(): AppConfig {
|
||||
const NODE_ENV = (process.env.NODE_ENV || "development") as
|
||||
| "development"
|
||||
| "production"
|
||||
| "test";
|
||||
|
||||
// Base configuration
|
||||
const baseConfig: AppConfig = {
|
||||
NODE_ENV,
|
||||
PORT: parseInt(process.env.PORT || "8080", 10),
|
||||
FRONTEND_URL: process.env.FRONTEND_URL || "http://localhost:5173",
|
||||
SUPABASE_URL: validateEnvVar("SUPABASE_URL", process.env.SUPABASE_URL),
|
||||
SUPABASE_SERVICE_ROLE_KEY: validateEnvVar(
|
||||
"SUPABASE_SERVICE_ROLE_KEY",
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
),
|
||||
STREAM_CHAT_API_KEY: validateEnvVar(
|
||||
"STREAM_CHAT_API_KEY",
|
||||
process.env.STREAM_CHAT_API_KEY
|
||||
),
|
||||
STREAM_CHAT_API_SECRET: validateEnvVar(
|
||||
"STREAM_CHAT_API_SECRET",
|
||||
process.env.STREAM_CHAT_API_SECRET
|
||||
),
|
||||
EMAIL_USER: validateEnvVar("EMAIL_USER", process.env.EMAIL_USER),
|
||||
EMAIL_KEY: validateEnvVar("EMAIL_KEY", process.env.EMAIL_KEY),
|
||||
CORS_ORIGIN: [],
|
||||
XTABLO_URL: process.env.XTABLO_URL || "https://xtablo.com",
|
||||
LOG_LEVEL: "info",
|
||||
};
|
||||
|
||||
// Environment-specific configurations
|
||||
if (NODE_ENV === "development") {
|
||||
baseConfig.CORS_ORIGIN = [
|
||||
"http://localhost:5173",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://127.0.0.1:3000",
|
||||
];
|
||||
baseConfig.LOG_LEVEL = "debug";
|
||||
} else if (NODE_ENV === "production") {
|
||||
baseConfig.CORS_ORIGIN = [
|
||||
baseConfig.FRONTEND_URL,
|
||||
"https://xtablo.com",
|
||||
"https://develop.xtablo-source.pages.dev",
|
||||
];
|
||||
baseConfig.LOG_LEVEL = "info";
|
||||
} else if (NODE_ENV === "test") {
|
||||
baseConfig.CORS_ORIGIN = ["http://localhost:3000"];
|
||||
baseConfig.LOG_LEVEL = "warn";
|
||||
}
|
||||
|
||||
return baseConfig;
|
||||
}
|
||||
|
||||
export const config = createConfig();
|
||||
|
||||
// Helper functions for common environment checks
|
||||
export const isDevelopment = () => config.NODE_ENV === "development";
|
||||
export const isProduction = () => config.NODE_ENV === "production";
|
||||
|
|
@ -108,29 +108,26 @@ export type Database = {
|
|||
}
|
||||
Relationships: []
|
||||
}
|
||||
tablo_invitations: {
|
||||
tablo_invites: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
id: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
status: string
|
||||
tablo_id: number
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
id?: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
status?: string
|
||||
tablo_id: number
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
id?: number
|
||||
invite_token?: string
|
||||
invited_by?: string
|
||||
invited_email?: string
|
||||
status?: string
|
||||
tablo_id?: number
|
||||
}
|
||||
Relationships: [
|
||||
|
|
@ -151,9 +148,9 @@ export type Database = {
|
|||
id: number
|
||||
image: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position: number
|
||||
status: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
color?: string | null
|
||||
|
|
@ -162,9 +159,9 @@ export type Database = {
|
|||
id?: number
|
||||
image?: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position?: number
|
||||
status?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
color?: string | null
|
||||
|
|
@ -173,9 +170,9 @@ export type Database = {
|
|||
id?: number
|
||||
image?: string | null
|
||||
name?: string
|
||||
owner_id?: string
|
||||
position?: number
|
||||
status?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,5 +40,4 @@ serve(
|
|||
console.log(`Server is running on http://localhost:${info.port}`);
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: Add health check endpoint
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const authMiddleware = async (c: Context, next: Next) => {
|
|||
export const supabaseMiddleware = async (c: Context, next: Next) => {
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL as string,
|
||||
process.env.SUPABASE_ANON_KEY as string
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY as string
|
||||
);
|
||||
c.set("supabase", supabase);
|
||||
await next();
|
||||
|
|
|
|||
9
api/src/token.ts
Normal file
9
api/src/token.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import crypto from "crypto";
|
||||
|
||||
export const generateToken = (): string => {
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(36).padStart(2, "0")).join(
|
||||
""
|
||||
);
|
||||
};
|
||||
|
|
@ -3,6 +3,8 @@ import { authMiddleware, emailMiddleware } from "./middleware.js";
|
|||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import { StreamChat } from "stream-chat";
|
||||
import type { Transporter } from "nodemailer";
|
||||
import { generateToken } from "./token.js";
|
||||
import { config } from "./config.js";
|
||||
|
||||
export const userRouter = new Hono<{
|
||||
Variables: {
|
||||
|
|
@ -36,25 +38,52 @@ userRouter.get("/get-stream-token", async (c) => {
|
|||
|
||||
userRouter.post("/invite", async (c) => {
|
||||
const sender = c.get("user");
|
||||
// const supabase = c.get("supabase");
|
||||
const supabase = c.get("supabase");
|
||||
const transporter = c.get("transporter");
|
||||
const { email: recipientmail, tablo_id } = await c.req.json();
|
||||
|
||||
const token = generateToken();
|
||||
|
||||
const { data: tablo, error: tabloError } = await supabase
|
||||
.from("tablos")
|
||||
.select("*")
|
||||
.eq("id", tablo_id)
|
||||
.single();
|
||||
|
||||
if (tabloError) {
|
||||
return c.json({ error: tabloError.message }, 500);
|
||||
}
|
||||
|
||||
if (!tablo) {
|
||||
return c.json({ error: "Tablo not found" }, 404);
|
||||
}
|
||||
|
||||
if (tablo.user_id !== sender.id) {
|
||||
return c.json(
|
||||
{ error: "You are not allowed to invite users to this tablo" },
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const { error } = await supabase.from("tablo_invites").insert({
|
||||
invited_email: recipientmail,
|
||||
tablo_id: tablo_id,
|
||||
invited_by: sender.id,
|
||||
invite_token: token,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: `${sender.email} via XTablo <noreply@xtablo.com>`,
|
||||
to: recipientmail,
|
||||
subject: "You have been invited to a tablo",
|
||||
html: `<p>You have been invited to a tablo with the following link: <a href="https://xtablo.com/tablo/${tablo_id}">https://xtablo.com/tablo/${tablo_id}</a></p>`,
|
||||
subject: "Vous avez été invité à un tablo",
|
||||
html: `<p>Vous avez été invité à un tablo avec <a href="${config.XTABLO_URL}/tablo/${tablo_id}?token=${token}">ce lien</a></p>`,
|
||||
});
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.inviteUserByEmail(
|
||||
// recipientmail,
|
||||
// {
|
||||
// data: {
|
||||
// tablo_id,
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
|
||||
return c.json({ data: info });
|
||||
return c.json({
|
||||
message: "Invite sent successfully",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
5
justfile
5
justfile
|
|
@ -14,4 +14,7 @@ _api-dev:
|
|||
cd api && npm run dev
|
||||
|
||||
dev:
|
||||
just _api-dev & (just _frontend-dev)
|
||||
just _api-dev & (just _frontend-dev)
|
||||
|
||||
update-types:
|
||||
npx supabase gen types typescript --project-id "mhcafqvzbrrwvahpvvzd" --schema public > ui/src/types/database.types.ts && cp ui/src/types/database.types.ts api/src/database.types.ts
|
||||
|
|
|
|||
|
|
@ -1,294 +0,0 @@
|
|||
-- =====================================================
|
||||
-- TABLOS SYSTEM DATABASE SCHEMA
|
||||
-- =====================================================
|
||||
|
||||
-- 1. TABLOS TABLE
|
||||
-- Main table for storing tablo/workspace information
|
||||
CREATE TABLE tablos (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(50) NOT NULL DEFAULT 'bg-blue-500',
|
||||
owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
is_archived BOOLEAN DEFAULT FALSE,
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
settings JSONB DEFAULT '{}',
|
||||
|
||||
-- Indexes for performance
|
||||
CONSTRAINT tablos_name_not_empty CHECK (LENGTH(TRIM(name)) > 0),
|
||||
CONSTRAINT tablos_color_format CHECK (color ~ '^bg-[a-z]+-[0-9]+$')
|
||||
);
|
||||
|
||||
-- 2. TABLO MEMBERS TABLE
|
||||
-- Junction table for tablo membership and permissions
|
||||
CREATE TABLE tablo_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tablo_id UUID NOT NULL REFERENCES tablos(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'member',
|
||||
permissions JSONB DEFAULT '{"read": true, "write": false, "admin": false}',
|
||||
joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
invited_by UUID REFERENCES auth.users(id),
|
||||
|
||||
-- Ensure unique membership per tablo
|
||||
UNIQUE(tablo_id, user_id),
|
||||
|
||||
-- Valid roles constraint
|
||||
CONSTRAINT valid_member_role CHECK (role IN ('owner', 'admin', 'member', 'viewer'))
|
||||
);
|
||||
|
||||
-- 3. TABLO BOARDS TABLE
|
||||
-- Different boards within a tablo (like Kanban boards, calendars, etc.)
|
||||
CREATE TABLE tablo_boards (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tablo_id UUID NOT NULL REFERENCES tablos(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL DEFAULT 'kanban',
|
||||
description TEXT,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
settings JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES auth.users(id),
|
||||
|
||||
-- Valid board types
|
||||
CONSTRAINT valid_board_type CHECK (type IN ('kanban', 'calendar', 'table', 'timeline', 'chat'))
|
||||
);
|
||||
|
||||
-- 4. TABLO LISTS TABLE
|
||||
-- Lists/columns within boards (for Kanban-style organization)
|
||||
CREATE TABLE tablo_lists (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
board_id UUID NOT NULL REFERENCES tablo_boards(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
color VARCHAR(50),
|
||||
settings JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 5. TABLO CARDS TABLE
|
||||
-- Individual cards/items within lists
|
||||
CREATE TABLE tablo_cards (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
list_id UUID NOT NULL REFERENCES tablo_lists(id) ON DELETE CASCADE,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
due_date TIMESTAMP WITH TIME ZONE,
|
||||
priority VARCHAR(20) DEFAULT 'medium',
|
||||
labels JSONB DEFAULT '[]',
|
||||
assignees JSONB DEFAULT '[]',
|
||||
attachments JSONB DEFAULT '[]',
|
||||
checklist JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES auth.users(id),
|
||||
|
||||
-- Valid priority levels
|
||||
CONSTRAINT valid_priority CHECK (priority IN ('low', 'medium', 'high', 'urgent'))
|
||||
);
|
||||
|
||||
-- 6. TABLO ACTIVITIES TABLE
|
||||
-- Activity log for tracking changes and actions
|
||||
CREATE TABLE tablo_activities (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tablo_id UUID NOT NULL REFERENCES tablos(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id UUID,
|
||||
details JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- Valid entity types
|
||||
CONSTRAINT valid_entity_type CHECK (entity_type IN ('tablo', 'board', 'list', 'card', 'member'))
|
||||
);
|
||||
|
||||
-- 7. TABLO CHAT CHANNELS TABLE
|
||||
-- Chat channels within tablos
|
||||
CREATE TABLE tablo_chat_channels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tablo_id UUID NOT NULL REFERENCES tablos(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL DEFAULT 'public',
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES auth.users(id),
|
||||
|
||||
-- Valid channel types
|
||||
CONSTRAINT valid_channel_type CHECK (type IN ('public', 'private', 'direct'))
|
||||
);
|
||||
|
||||
-- 8. TABLO CHAT MESSAGES TABLE
|
||||
-- Messages within chat channels
|
||||
CREATE TABLE tablo_chat_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES tablo_chat_channels(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
message_type VARCHAR(20) DEFAULT 'text',
|
||||
attachments JSONB DEFAULT '[]',
|
||||
reply_to UUID REFERENCES tablo_chat_messages(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
is_edited BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Valid message types
|
||||
CONSTRAINT valid_message_type CHECK (message_type IN ('text', 'image', 'file', 'system'))
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- INDEXES FOR PERFORMANCE
|
||||
-- =====================================================
|
||||
|
||||
-- Tablos indexes
|
||||
CREATE INDEX idx_tablos_owner_id ON tablos(owner_id);
|
||||
CREATE INDEX idx_tablos_created_at ON tablos(created_at DESC);
|
||||
CREATE INDEX idx_tablos_name_search ON tablos USING gin(to_tsvector('french', name));
|
||||
|
||||
-- Tablo members indexes
|
||||
CREATE INDEX idx_tablo_members_tablo_id ON tablo_members(tablo_id);
|
||||
CREATE INDEX idx_tablo_members_user_id ON tablo_members(user_id);
|
||||
|
||||
-- Boards indexes
|
||||
CREATE INDEX idx_tablo_boards_tablo_id ON tablo_boards(tablo_id);
|
||||
CREATE INDEX idx_tablo_boards_position ON tablo_boards(tablo_id, position);
|
||||
|
||||
-- Lists indexes
|
||||
CREATE INDEX idx_tablo_lists_board_id ON tablo_lists(board_id);
|
||||
CREATE INDEX idx_tablo_lists_position ON tablo_lists(board_id, position);
|
||||
|
||||
-- Cards indexes
|
||||
CREATE INDEX idx_tablo_cards_list_id ON tablo_cards(list_id);
|
||||
CREATE INDEX idx_tablo_cards_position ON tablo_cards(list_id, position);
|
||||
CREATE INDEX idx_tablo_cards_assignees ON tablo_cards USING gin(assignees);
|
||||
CREATE INDEX idx_tablo_cards_due_date ON tablo_cards(due_date) WHERE due_date IS NOT NULL;
|
||||
|
||||
-- Activities indexes
|
||||
CREATE INDEX idx_tablo_activities_tablo_id ON tablo_activities(tablo_id);
|
||||
CREATE INDEX idx_tablo_activities_created_at ON tablo_activities(created_at DESC);
|
||||
CREATE INDEX idx_tablo_activities_user_id ON tablo_activities(user_id);
|
||||
|
||||
-- Chat indexes
|
||||
CREATE INDEX idx_tablo_chat_channels_tablo_id ON tablo_chat_channels(tablo_id);
|
||||
CREATE INDEX idx_tablo_chat_messages_channel_id ON tablo_chat_messages(channel_id);
|
||||
CREATE INDEX idx_tablo_chat_messages_created_at ON tablo_chat_messages(created_at DESC);
|
||||
|
||||
-- =====================================================
|
||||
-- ROW LEVEL SECURITY (RLS) POLICIES
|
||||
-- =====================================================
|
||||
|
||||
-- Enable RLS on all tables
|
||||
ALTER TABLE tablos ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tablo_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tablo_boards ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tablo_lists ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tablo_cards ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tablo_activities ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tablo_chat_channels ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tablo_chat_messages ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Tablos policies
|
||||
CREATE POLICY "Users can view tablos they are members of" ON tablos
|
||||
FOR SELECT USING (
|
||||
id IN (
|
||||
SELECT tablo_id FROM tablo_members
|
||||
WHERE user_id = auth.uid()
|
||||
) OR owner_id = auth.uid() OR is_public = true
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can create their own tablos" ON tablos
|
||||
FOR INSERT WITH CHECK (owner_id = auth.uid());
|
||||
|
||||
CREATE POLICY "Owners and admins can update tablos" ON tablos
|
||||
FOR UPDATE USING (
|
||||
owner_id = auth.uid() OR
|
||||
id IN (
|
||||
SELECT tablo_id FROM tablo_members
|
||||
WHERE user_id = auth.uid() AND role IN ('admin', 'owner')
|
||||
)
|
||||
);
|
||||
|
||||
-- Tablo members policies
|
||||
CREATE POLICY "Users can view members of tablos they belong to" ON tablo_members
|
||||
FOR SELECT USING (
|
||||
tablo_id IN (
|
||||
SELECT tablo_id FROM tablo_members
|
||||
WHERE user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Owners and admins can manage members" ON tablo_members
|
||||
FOR ALL USING (
|
||||
tablo_id IN (
|
||||
SELECT id FROM tablos WHERE owner_id = auth.uid()
|
||||
) OR
|
||||
tablo_id IN (
|
||||
SELECT tablo_id FROM tablo_members
|
||||
WHERE user_id = auth.uid() AND role IN ('admin', 'owner')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- TRIGGERS FOR AUTOMATIC UPDATES
|
||||
-- =====================================================
|
||||
|
||||
-- Function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Apply triggers to relevant tables
|
||||
CREATE TRIGGER update_tablos_updated_at BEFORE UPDATE ON tablos
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_tablo_boards_updated_at BEFORE UPDATE ON tablo_boards
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_tablo_lists_updated_at BEFORE UPDATE ON tablo_lists
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_tablo_cards_updated_at BEFORE UPDATE ON tablo_cards
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Function to automatically add owner as member
|
||||
CREATE OR REPLACE FUNCTION add_owner_as_member()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO tablo_members (tablo_id, user_id, role, permissions)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
NEW.owner_id,
|
||||
'owner',
|
||||
'{"read": true, "write": true, "admin": true}'::jsonb
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER add_owner_as_member_trigger AFTER INSERT ON tablos
|
||||
FOR EACH ROW EXECUTE FUNCTION add_owner_as_member();
|
||||
|
||||
-- =====================================================
|
||||
-- SAMPLE DATA
|
||||
-- =====================================================
|
||||
|
||||
-- Insert sample tablos (assuming user IDs exist)
|
||||
-- Note: Replace with actual user UUIDs from your auth.users table
|
||||
/*
|
||||
INSERT INTO tablos (name, description, color, owner_id) VALUES
|
||||
('Projet Alpha', 'Développement de la nouvelle application', 'bg-blue-500', 'user-uuid-1'),
|
||||
('Marketing Q4', 'Campagnes marketing pour le quatrième trimestre', 'bg-green-500', 'user-uuid-2'),
|
||||
('Équipe Dev', 'Coordination de l''équipe de développement', 'bg-purple-500', 'user-uuid-1'),
|
||||
('Budget 2024', 'Planification budgétaire pour 2024', 'bg-red-500', 'user-uuid-3'),
|
||||
('Roadmap', 'Feuille de route produit', 'bg-yellow-500', 'user-uuid-1'),
|
||||
('Support Client', 'Gestion du support client', 'bg-indigo-500', 'user-uuid-2');
|
||||
*/
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
-- Create tablos table
|
||||
CREATE TABLE IF NOT EXISTS tablos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id UUID NOT NULL,
|
||||
owner_id UUID NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
image TEXT,
|
||||
color VARCHAR(50),
|
||||
|
|
@ -18,13 +18,18 @@ CREATE TABLE IF NOT EXISTS tablos (
|
|||
ALTER TABLE tablos ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policy to allow users to see only their own tablos
|
||||
CREATE POLICY "Users can view own tablos" ON tablos
|
||||
FOR SELECT USING (auth.uid() = user_id);
|
||||
CREATE POLICY "Users can view tablos they have access to" ON tablos
|
||||
FOR SELECT USING (
|
||||
(SELECT auth.uid()) = owner_id
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM tablo_access WHERE tablo_id = tablos.id AND user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Create policy to allow users to insert their own tablos
|
||||
CREATE POLICY "Users can insert own tablos" ON tablos
|
||||
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||
FOR INSERT WITH CHECK (auth.uid() = owner_id);
|
||||
|
||||
-- Create policy to allow users to update their own tablos
|
||||
CREATE POLICY "Users can update own tablos" ON tablos
|
||||
FOR UPDATE USING (auth.uid() = user_id);
|
||||
FOR UPDATE USING (auth.uid() = owner_id);
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
-- Create tablo_invitations table
|
||||
CREATE TABLE IF NOT EXISTS tablo_invitations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tablo_id INTEGER NOT NULL,
|
||||
invited_email VARCHAR(255) NOT NULL,
|
||||
invited_by UUID NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Foreign key constraint to tablos table
|
||||
CONSTRAINT fk_tablo_invitations_tablo_id
|
||||
FOREIGN KEY (tablo_id) REFERENCES tablos(id) ON DELETE CASCADE,
|
||||
|
||||
-- Constraint to ensure status is one of the allowed values
|
||||
CONSTRAINT tablo_invitations_status_check
|
||||
CHECK (status IN ('pending', 'accepted', 'declined')),
|
||||
|
||||
-- Unique constraint to prevent duplicate invitations
|
||||
CONSTRAINT unique_tablo_invitation
|
||||
UNIQUE (tablo_id, invited_email)
|
||||
);
|
||||
|
||||
-- Enable Row Level Security
|
||||
ALTER TABLE tablo_invitations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policy to allow tablo owners to insert invitations
|
||||
CREATE POLICY "Tablo owners can insert invitations" ON tablo_invitations
|
||||
FOR INSERT WITH CHECK (
|
||||
auth.uid() = invited_by AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM tablos
|
||||
WHERE tablos.id = tablo_invitations.tablo_id
|
||||
AND tablos.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Create index for better query performance
|
||||
CREATE INDEX idx_tablo_invitations_tablo_id ON tablo_invitations(tablo_id);
|
||||
CREATE INDEX idx_tablo_invitations_invited_email ON tablo_invitations(invited_email);
|
||||
21
sql/09_create_tablo_invites_table.sql
Normal file
21
sql/09_create_tablo_invites_table.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-- Create tablo_invitations table
|
||||
CREATE TABLE IF NOT EXISTS tablo_invites (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tablo_id INTEGER NOT NULL,
|
||||
invited_email VARCHAR(255) NOT NULL,
|
||||
invited_by UUID NOT NULL,
|
||||
invite_token TEXT NOT NULL,
|
||||
|
||||
-- Foreign key constraint to tablos table
|
||||
CONSTRAINT fk_tablo_invitations_tablo_id
|
||||
FOREIGN KEY (tablo_id) REFERENCES tablos(id) ON DELETE CASCADE,
|
||||
|
||||
-- Unique constraint to prevent duplicate invitations
|
||||
CONSTRAINT unique_tablo_invitation
|
||||
UNIQUE (tablo_id, invited_email)
|
||||
);
|
||||
|
||||
-- Enable Row Level Security
|
||||
ALTER TABLE tablo_invites ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- No policies for now, since we don't want to provide any access to the table
|
||||
32
sql/10_create_tablo_access_table.sql
Normal file
32
sql/10_create_tablo_access_table.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- Create tablo_access table for managing shared access to tablos
|
||||
CREATE TABLE IF NOT EXISTS tablo_access (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tablo_id INTEGER NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
granted_by UUID NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Foreign key constraint to tablos table
|
||||
CONSTRAINT fk_tablo_access_tablo_id
|
||||
FOREIGN KEY (tablo_id) REFERENCES tablos(id) ON DELETE CASCADE,
|
||||
|
||||
-- Unique constraint to prevent duplicate access records
|
||||
CONSTRAINT unique_tablo_access
|
||||
UNIQUE (tablo_id, user_id)
|
||||
);
|
||||
|
||||
-- Create indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_tablo_access_tablo_id ON tablo_access(tablo_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tablo_access_user_id ON tablo_access(user_id);
|
||||
|
||||
-- Enable Row Level Security
|
||||
ALTER TABLE tablo_access ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy to allow users to view tablo_access records where they are the user
|
||||
CREATE POLICY "Users can view their tablo access only if the access is active" ON tablo_access
|
||||
FOR SELECT USING (
|
||||
user_id = (SELECT auth.uid())
|
||||
AND is_active = TRUE
|
||||
);
|
||||
34
sql/11_create_tablo_access_trigger.sql
Normal file
34
sql/11_create_tablo_access_trigger.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
-- Create a trigger function that automatically creates tablo_access when a new tablo is created
|
||||
CREATE OR REPLACE FUNCTION create_tablo_access_for_owner()
|
||||
RETURNS TRIGGER
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Insert a tablo_access record for the tablo owner
|
||||
INSERT INTO tablo_access (
|
||||
tablo_id,
|
||||
user_id,
|
||||
granted_by,
|
||||
is_active,
|
||||
is_admin
|
||||
) VALUES (
|
||||
NEW.id, -- tablo_id: the newly created tablo's id
|
||||
NEW.owner_id, -- user_id: the tablo owner gets access
|
||||
NEW.owner_id, -- granted_by: self-granted by the owner
|
||||
TRUE, -- is_active: access is active
|
||||
TRUE -- is_admin: owner has admin privileges
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create the trigger that fires after INSERT on tablos table
|
||||
CREATE TRIGGER trigger_create_tablo_access
|
||||
AFTER INSERT ON tablos
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_tablo_access_for_owner();
|
||||
|
||||
-- Add a comment to document the trigger
|
||||
COMMENT ON TRIGGER trigger_create_tablo_access ON tablos IS
|
||||
'Automatically creates tablo_access record for the tablo owner when a new tablo is created';
|
||||
|
|
@ -12,7 +12,7 @@ interface CreateTabloModalProps {
|
|||
onCreate: (
|
||||
tabloData: Omit<
|
||||
Tablo,
|
||||
"id" | "user_id" | "created_at" | "deleted_at" | "position"
|
||||
"id" | "owner_id" | "created_at" | "deleted_at" | "position"
|
||||
>
|
||||
) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ export const useCreateTablo = () => {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (tablo: Omit<TabloInsert, "user_id">) => {
|
||||
mutationFn: async (tablo: Omit<TabloInsert, "owner_id">) => {
|
||||
const { error } = await supabase.from("tablos").insert({
|
||||
...tablo,
|
||||
user_id: session?.user.id ?? "",
|
||||
owner_id: session?.user.id ?? "",
|
||||
});
|
||||
if (error) throw error;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const TabloPage = () => {
|
|||
const createNewTablo = async (
|
||||
tabloData: Omit<
|
||||
Tablo,
|
||||
"id" | "user_id" | "created_at" | "deleted_at" | "position"
|
||||
"id" | "owner_id" | "created_at" | "deleted_at" | "position"
|
||||
>
|
||||
) => {
|
||||
try {
|
||||
|
|
@ -152,12 +152,12 @@ export const TabloPage = () => {
|
|||
|
||||
const getUserRole = (tablo: Tablo) => {
|
||||
if (!session?.user) return "Invité";
|
||||
return tablo.user_id === session.user.id ? "Admin" : "Invité";
|
||||
return tablo.owner_id === session.user.id ? "Admin" : "Invité";
|
||||
};
|
||||
|
||||
const getRoleColor = (tablo: Tablo) => {
|
||||
if (!session?.user) return "text-gray-500 dark:text-gray-400";
|
||||
return tablo.user_id === session.user.id
|
||||
return tablo.owner_id === session.user.id
|
||||
? "text-blue-600 dark:text-blue-400"
|
||||
: "text-gray-500 dark:text-gray-400";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -108,29 +108,26 @@ export type Database = {
|
|||
}
|
||||
Relationships: []
|
||||
}
|
||||
tablo_invitations: {
|
||||
tablo_invites: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
id: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
status: string
|
||||
tablo_id: number
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
id?: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
status?: string
|
||||
tablo_id: number
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
id?: number
|
||||
invite_token?: string
|
||||
invited_by?: string
|
||||
invited_email?: string
|
||||
status?: string
|
||||
tablo_id?: number
|
||||
}
|
||||
Relationships: [
|
||||
|
|
@ -151,9 +148,9 @@ export type Database = {
|
|||
id: number
|
||||
image: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position: number
|
||||
status: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
color?: string | null
|
||||
|
|
@ -162,9 +159,9 @@ export type Database = {
|
|||
id?: number
|
||||
image?: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position?: number
|
||||
status?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
color?: string | null
|
||||
|
|
@ -173,9 +170,9 @@ export type Database = {
|
|||
id?: number
|
||||
image?: string | null
|
||||
name?: string
|
||||
owner_id?: string
|
||||
position?: number
|
||||
status?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue