Improve SQL tables

This commit is contained in:
Arthur Belleville 2025-07-03 21:42:49 +02:00
parent 7a17c95fec
commit 0c90e31ec2
No known key found for this signature in database
18 changed files with 257 additions and 382 deletions

View file

@ -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
View 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";

View file

@ -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: []
}

View file

@ -40,5 +40,4 @@ serve(
console.log(`Server is running on http://localhost:${info.port}`);
}
);
// TODO: Add health check endpoint

View file

@ -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
View 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(
""
);
};

View file

@ -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",
});
});

View file

@ -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

View file

@ -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');
*/

View file

@ -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);

View file

@ -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);

View 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

View 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
);

View 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';

View file

@ -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;
}

View file

@ -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;
},

View file

@ -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";
};

View file

@ -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: []
}