Skip to content

Commit d477a90

Browse files
authored
Add: Dashboard db migrations & tooling (#6)
2 parents 588cfbf + aacdaf3 commit d477a90

File tree

9 files changed

+156
-92
lines changed

9 files changed

+156
-92
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ NEXT_PUBLIC_DEFAULT_API_DOMAIN=e2b.dev
1919
NEXT_PUBLIC_EXPOSE_STORYBOOK=0
2020
NEXT_PUBLIC_SCAN=0
2121
NEXT_PUBLIC_MOCK_DATA=0
22+
23+
# For applying migrations
24+
# POSTGRES_CONNECTION_STRING=

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,24 @@ vercel storage add
7575
3. Copy the `anon key` and `service_role key`
7676
4. Copy the project URL
7777

78-
#### c. Supabase Storage Setup
78+
#### c. Database Setup
79+
1. Retrieve the `POSTGRES_CONNECTION_STRING` from the Supabase project settings
80+
2. Run the migrations by running the following command:
81+
```bash
82+
bun run db:migrations:apply
83+
```
84+
85+
#### d. Supabase Storage Setup
7986
1. Go to Storage > Buckets
8087
2. Create a new **public** bucket named `profile-pictures`
81-
3. Apply storage access policies by running the SQL from [supabase/policies/buckets.sql](supabase/policies/buckets.sql) in the Supabase SQL Editor:
82-
- These policies ensure only Supabase admin (service role) can write to and list files in the bucket
83-
- Public URLs are accessible for downloading files if the exact path is known
84-
- Regular users cannot browse, upload, update, or delete files in the bucket
8588

86-
#### d. Environment Variables
89+
#### e. Environment Variables
8790
```bash
8891
# Copy the example env file
8992
cp .env.example .env.local
9093
```
9194

92-
#### e. Cookie Encryption
95+
#### f. Cookie Encryption
9396
The dashboard uses encrypted cookies for secure data storage. You'll need to set up a `COOKIE_ENCRYPTION_KEY`:
9497

9598
```bash
@@ -178,4 +181,4 @@ If you need help or have questions:
178181
## License
179182
This project is licensed under the Apache License, Version 2.0 - see the [LICENSE](LICENSE) file for details.
180183

181-
Copyright 2025 FoundryLabs, Inc.
184+
Copyright 2025 FoundryLabs, Inc.

bun.lock

Lines changed: 7 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

migrations/20250205180205.sql

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
This migration adds team slugs and profile pictures to support user-friendly URLs and team branding.
3+
4+
It performs the following steps:
5+
6+
1. Adds two new columns to the teams table:
7+
- slug: A URL-friendly version of the team name (e.g. "acme-inc")
8+
- profile_picture_url: URL to the team's profile picture
9+
10+
2. Creates a slug generation function that:
11+
- Takes a team name and converts it to a URL-friendly format
12+
- Removes special characters, accents, and spaces
13+
- Handles email addresses by only using the part before @
14+
- Converts to lowercase and replaces spaces with hyphens
15+
16+
3. Installs the unaccent PostgreSQL extension for proper accent handling
17+
18+
4. Generates initial slugs for all existing teams:
19+
- Uses the team name as base for the slug
20+
- If multiple teams would have the same slug, appends part of the team ID
21+
to ensure uniqueness
22+
23+
5. Sets up automatic slug generation for new teams:
24+
- Creates a trigger that runs before team insertion
25+
- Generates a unique slug using random suffixes if needed
26+
- Only generates a slug if one isn't explicitly provided
27+
28+
6. Enforces slug uniqueness with a database constraint
29+
*/
30+
31+
ALTER TABLE teams
32+
ADD COLUMN slug TEXT,
33+
ADD COLUMN profile_picture_url TEXT;
34+
35+
CREATE OR REPLACE FUNCTION generate_team_slug(name TEXT)
36+
RETURNS TEXT AS $$
37+
DECLARE
38+
base_name TEXT;
39+
BEGIN
40+
base_name := SPLIT_PART(name, '@', 1);
41+
42+
RETURN LOWER(
43+
REGEXP_REPLACE(
44+
REGEXP_REPLACE(
45+
UNACCENT(TRIM(base_name)),
46+
'[^a-zA-Z0-9\s-]',
47+
'',
48+
'g'
49+
),
50+
'\s+',
51+
'-',
52+
'g'
53+
)
54+
);
55+
END;
56+
$$ LANGUAGE plpgsql;
57+
58+
CREATE EXTENSION IF NOT EXISTS unaccent;
59+
60+
WITH numbered_teams AS (
61+
SELECT
62+
id,
63+
name,
64+
generate_team_slug(name) as base_slug,
65+
ROW_NUMBER() OVER (PARTITION BY generate_team_slug(name) ORDER BY created_at) as slug_count
66+
FROM teams
67+
WHERE slug IS NULL
68+
)
69+
UPDATE teams
70+
SET slug =
71+
CASE
72+
WHEN t.slug_count = 1 THEN t.base_slug
73+
ELSE t.base_slug || '-' || SUBSTRING(teams.id::text, 1, 4)
74+
END
75+
FROM numbered_teams t
76+
WHERE teams.id = t.id;
77+
78+
CREATE OR REPLACE FUNCTION generate_team_slug_trigger()
79+
RETURNS TRIGGER AS $$
80+
DECLARE
81+
base_slug TEXT;
82+
test_slug TEXT;
83+
suffix TEXT;
84+
BEGIN
85+
IF NEW.slug IS NULL THEN
86+
base_slug := generate_team_slug(NEW.name);
87+
test_slug := base_slug;
88+
89+
WHILE EXISTS (SELECT 1 FROM teams WHERE slug = test_slug) LOOP
90+
suffix := SUBSTRING(gen_random_uuid()::text, 1, 4);
91+
test_slug := base_slug || '-' || suffix;
92+
END LOOP;
93+
94+
NEW.slug := test_slug;
95+
END IF;
96+
RETURN NEW;
97+
END;
98+
$$ LANGUAGE plpgsql;
99+
100+
CREATE TRIGGER team_slug_trigger
101+
BEFORE INSERT ON teams
102+
FOR EACH ROW
103+
EXECUTE FUNCTION generate_team_slug_trigger();
104+
105+
ALTER TABLE teams
106+
ADD CONSTRAINT teams_slug_unique UNIQUE (slug);
107+
108+
ALTER TABLE teams
109+
ALTER COLUMN slug SET NOT NULL;

migrations/20250311144556.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE OR REPLACE VIEW public.auth_users AS
2+
SELECT
3+
id,
4+
email
5+
FROM auth.users;
6+
7+
-- Revoke all permissions to ensure no public access
8+
REVOKE ALL ON public.auth_users FROM PUBLIC;
9+
REVOKE ALL ON public.auth_users FROM anon;
10+
REVOKE ALL ON public.auth_users FROM authenticated;

package.json

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,24 @@
1010
"preview": "bun run build && bun start",
1111
"lint": "next lint",
1212
"lint:fix": "next lint --fix",
13-
1413
"<<<<<< Development Tools": "",
1514
"dev:scan": "bun dev & bun scan",
1615
"start:scan": "bun start & bun scan",
17-
1816
"<<<<<< Database": "",
1917
"db:types": "bunx supabase@latest gen types typescript --schema public > src/types/database.types.ts --project-id $SUPABASE_PROJECT_ID",
20-
"db:migration": "bun scripts/create-migration.ts",
21-
18+
"db:migrations:create": "bun run scripts:create-migration",
19+
"db:migrations:apply": "bun run scripts:apply-migrations",
2220
"<<<<<< Scripts": "",
2321
"scripts:check-app-env": "bun scripts/check-app-env.ts",
2422
"scripts:build-storybook": "bun scripts/build-storybook.ts",
2523
"scripts:check-e2e-env": "bun scripts/check-e2e-env.ts",
2624
"scripts:check-all-env": "bun scripts:check-app-env && bun scripts:check-e2e-env",
27-
25+
"scripts:create-migration": "bun scripts/create-migration.ts",
2826
"<<<<<< Development": "",
2927
"storybook": "storybook dev -p 6006",
3028
"shad": "bunx shadcn@canary",
3129
"prebuild": "bun scripts:build-storybook",
3230
"postinstall": "fumadocs-mdx",
33-
3431
"<<<<<< Testing": "",
3532
"test:run": "bun scripts:check-all-env && vitest run",
3633
"test:integration": "bun scripts:check-app-env && vitest run src/__test__/integration/",
@@ -88,7 +85,7 @@
8885
"next": "^15.2.2-canary.6",
8986
"next-logger": "^5.0.1",
9087
"next-themes": "^0.4.4",
91-
"pg": "^8.13.1",
88+
"pg": "^8.14.0",
9289
"pino": "^9.6.0",
9390
"pino-pretty": "^13.0.0",
9491
"postgres": "^3.4.5",
@@ -128,7 +125,7 @@
128125
"@tailwindcss/postcss": "^4.0.6",
129126
"@testing-library/jest-dom": "^6.6.3",
130127
"@testing-library/react": "^16.2.0",
131-
"@types/bun": "latest",
128+
"@types/bun": "^1.2.5",
132129
"@types/node": "22.10.10",
133130
"@types/pg": "^8.11.11",
134131
"@types/react": "^19.0.8",

src/middleware.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,17 @@ export async function middleware(request: NextRequest) {
4040
}
4141
)
4242

43-
// 2. Refresh session and handle auth redirects
43+
// 2. Handle URL rewrites first (early return for non-dashboard routes)
44+
const rewriteResponse = await handleUrlRewrites(request, {
45+
landingPage: LANDING_PAGE_DOMAIN,
46+
landingPageFramer: LANDING_PAGE_FRAMER_DOMAIN,
47+
blogFramer: BLOG_FRAMER_DOMAIN,
48+
docsNext: DOCS_NEXT_DOMAIN,
49+
})
50+
51+
if (rewriteResponse) return rewriteResponse
52+
53+
// 3. Refresh session and handle auth redirects
4454
const { error, data } = await getUserSession(supabase)
4555

4656
// Handle authentication redirects
@@ -52,16 +62,6 @@ export async function middleware(request: NextRequest) {
5262
return response
5363
}
5464

55-
// 3. Handle URL rewrites first (early return for non-dashboard routes)
56-
const rewriteResponse = await handleUrlRewrites(request, {
57-
landingPage: LANDING_PAGE_DOMAIN,
58-
landingPageFramer: LANDING_PAGE_FRAMER_DOMAIN,
59-
blogFramer: BLOG_FRAMER_DOMAIN,
60-
docsNext: DOCS_NEXT_DOMAIN,
61-
})
62-
63-
if (rewriteResponse) return rewriteResponse
64-
6565
// 4. Handle team resolution for all dashboard routes
6666
const teamResult = await resolveTeamForDashboard(request, data.user.id)
6767

src/styles/globals.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
@import 'tailwindcss';
22

3-
@custom-variant dark (&:is(.dark *));
43
@import 'fumadocs-ui/css/preset.css';
54

65
/* path of `fumadocs-ui` relative to the CSS file */

supabase/policies/buckets.sql

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)