Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ next-env.d.ts

## supabase
/supabase/
.qodo
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,27 @@ pnpm db:regenerate-migrations
If this worked, you should see 5 triggers in Supabase under [Database > Triggers > auth](https://supabase.com/dashboard/project/pboorlggoqpyiucxmneq/database/triggers?schema=auth).

Also, make sure you delete your Supabase users under Authentication, as those are no longer duplicated in the `UserProfile` table.

## Dynamic Database Credentials with Vercel

This project supports dynamic database credentials, making it easy to change the database user without modifying the code.

### Local Setup
1. Set `POSTGRES_USER` and `POSTGRES_PASSWORD` in your `.env` file
2. Run migrations with `npm run db:migrate:prod`

### Vercel Setup
1. Add the following environment variables in your Vercel project:
- `POSTGRES_USER`: Your database user (e.g., "prisma" or any custom user)
- `POSTGRES_PASSWORD`: Your database password

2. Update the build command in your Vercel project to use the migration script:
```
npm run build && npm run db:migrate:prod
```

3. When you need to change database credentials:
- Update the `POSTGRES_USER` and `POSTGRES_PASSWORD` environment variables in Vercel
- Redeploy your application

This approach allows you to change database credentials without modifying the codebase, migration files, or prisma schema.
6 changes: 3 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ export default function Login() {
try {
setIsLoading(true);

const { url } = await loginMutation.mutateAsync({ authProviderType: "GOOGLE" });
const { data } = await loginMutation.mutateAsync({ authProviderType: "GOOGLE" });

if (url) {
if (data) {
// Redirect to Google's OAuth page
window.location.href = url
window.location.href = data
} else {
console.error("No URL returned from authenticate")
setIsLoading(false);
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
"db:migrate": "prisma migrate dev",
"seed": "node -r ts-node/register --env-file=.env prisma/seed.ts",
"postinstall": "npx prisma generate",
"db:postinstall": "prisma generate && prisma migrate deploy",
"db:postinstall": "prisma generate && ./scripts/migrate-db.sh",
"db:regenerate-migrations": "./scripts/regenerate-prisma-migrations.sh",
"db:migrate:prod": "./scripts/migrate-db.sh",
"sdk:dev": "tsup --watch",
"sdk:build": "tsup"
},
Expand Down
35 changes: 16 additions & 19 deletions prisma/migrations/20250221160426_user_trigger/migration.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,24 @@

-- inserts a row into public.UserProfiles

create function public.handle_new_user()
create or replace function public.handle_new_user()
returns trigger as $$
begin
-- Set the user ID as the current user for RLS policies
PERFORM set_config('app.current_user_id', NEW.id::text, true);

insert into public."UserProfiles" ("supId", "supEmail", "supPhone", "name", "email", "phone", "picture", "updatedAt")
values (new.id, new.email, new.phone, new.raw_user_meta_data->>'full_name', new.email, new.phone, coalesce(new.raw_user_meta_data->>'avatar_url', new.raw_user_meta_data->>'picture'), now());
return new;
end;
$$ language plpgsql SECURITY DEFINER set search_path = public;

-- trigger the function above every time an auth.user is created

-- $$ language plpgsql security definer;
-- create trigger on_auth_user_created
-- after insert on auth.users
-- for each row execute procedure public.handle_new_user();

DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN
EXECUTE 'CREATE TRIGGER on_auth_user_created
EXECUTE 'CREATE OR REPLACE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();';
END IF;
Expand All @@ -39,25 +38,24 @@ END $$;
create or replace function public.handle_update_user_email_n_phone()
returns trigger as $$
begin
-- Set the user ID as the current user for RLS policies
PERFORM set_config('app.current_user_id', NEW.id::text, true);

update public."UserProfiles"
set
"supEmail" = coalesce(new.email, "supEmail"),
"supPhone" = coalesce(new.phone, "supPhone")
where "supId" = new.id;
return new;
end;
$$ language plpgsql SECURITY DEFINER set search_path = public;

-- trigger the function above every time an auth.user's email or phone are updated

-- $$ language plpgsql security definer set search_path = public;
-- create trigger on_auth_user_updated
-- after update of email, phone on auth.users
-- for each row execute procedure public.handle_update_user_email_n_phone();

DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN
EXECUTE 'CREATE TRIGGER on_auth_user_updated
EXECUTE 'CREATE OR REPLACE TRIGGER on_auth_user_updated
AFTER UPDATE OF email, phone ON auth.users
FOR EACH ROW EXECUTE PROCEDURE public.handle_update_user_email_n_phone();';
END IF;
Expand All @@ -69,21 +67,20 @@ END $$;
create or replace function public.handle_delete_user()
returns trigger as $$
begin
-- Set the user ID as the current user for RLS policies
PERFORM set_config('app.current_user_id', OLD.id::text, true);

delete from public."UserProfiles" where "supId" = old.id;
return old;
end;
$$ language plpgsql SECURITY DEFINER set search_path = public;

-- Trigger the function above every time an auth.user is deleted

-- $$ language plpgsql security definer set search_path = public;
-- create trigger on_auth_user_deleted
-- after delete on auth.users
-- for each row execute procedure public.handle_delete_user();

DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN
EXECUTE 'CREATE TRIGGER on_auth_user_deleted
EXECUTE 'CREATE OR REPLACE TRIGGER on_auth_user_deleted
AFTER DELETE ON auth.users
FOR EACH ROW EXECUTE PROCEDURE public.handle_delete_user();';
END IF;
Expand Down
180 changes: 172 additions & 8 deletions prisma/migrations/20250224065512_session_trigger/migration.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,149 @@
-- These triggers automatically create and update a Session when a new session is created via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.

-- inserts a row into public.Sessions
-- Utility function to ensure a user profile exists
DROP FUNCTION IF EXISTS public.ensure_user_profile;
CREATE OR REPLACE FUNCTION public.ensure_user_profile(user_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
user_exists BOOLEAN;
BEGIN
-- Check if user already exists
SELECT EXISTS (
SELECT 1 FROM "UserProfiles" WHERE "supId" = user_id
) INTO user_exists;

-- If not, try to create it
IF NOT user_exists THEN
BEGIN
-- Try to create from auth.users first
INSERT INTO "UserProfiles" ("supId", "supEmail", "supPhone", "name", "email", "phone", "picture", "updatedAt")
SELECT
u.id,
u.email,
COALESCE(u.raw_user_meta_data->>'full_name', u.raw_user_meta_data->>'name'),
u.email,
NOW()
FROM auth.users u
WHERE u.id = user_id
ON CONFLICT ("supId") DO NOTHING;

-- If that fails, create a minimal profile
SELECT EXISTS (
SELECT 1 FROM "UserProfiles" WHERE "supId" = user_id
) INTO user_exists;

IF NOT user_exists THEN
INSERT INTO "UserProfiles" ("supId", "updatedAt")
VALUES (user_id, NOW())
ON CONFLICT ("supId") DO NOTHING;
END IF;
EXCEPTION
WHEN OTHERS THEN
-- Last attempt with minimal data
BEGIN
INSERT INTO "UserProfiles" ("supId", "updatedAt")
VALUES (user_id, NOW())
ON CONFLICT ("supId") DO NOTHING;
EXCEPTION
WHEN OTHERS THEN
RETURN FALSE;
END;
END;
END IF;

RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;

-- Improved session creation function with better error handling
DROP FUNCTION IF EXISTS public.upsert_session;
CREATE OR REPLACE FUNCTION public.upsert_session(
session_id UUID,
user_id UUID,
device_nonce TEXT,
ip_address TEXT,
user_agent TEXT
) RETURNS BOOLEAN AS $$
BEGIN
-- First ensure the user exists
IF NOT public.ensure_user_profile(user_id) THEN
RAISE NOTICE 'Failed to ensure user profile exists for %', user_id;
RETURN FALSE;
END IF;

-- Now create/update the session
BEGIN
-- Temporarily set the current_user_id for RLS
PERFORM set_config('app.current_user_id', user_id::text, true);

INSERT INTO "Sessions" ("id", "userId", "createdAt", "updatedAt", "deviceNonce", "ip", "userAgent")
VALUES (
session_id,
user_id,
NOW(),
NOW(),
device_nonce,
ip_address::inet,
user_agent
)
ON CONFLICT ("id")
DO UPDATE SET
"updatedAt" = NOW(),
"deviceNonce" = device_nonce,
"ip" = ip_address::inet,
"userAgent" = user_agent;

RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Error upserting session: %', SQLERRM;
RETURN FALSE;
END;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;

-- Improved trigger for session creation
DROP FUNCTION IF EXISTS public.handle_new_session;
CREATE OR REPLACE FUNCTION public.handle_new_session()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
country_code VARCHAR(2);
BEGIN
INSERT INTO "Sessions" (id, "createdAt", "updatedAt", ip, "userAgent", "userId", "deviceNonce")
SELECT NEW.id, NEW.created_at, NEW.updated_at, NEW.ip, NEW.user_agent, NEW.user_id, gen_random_uuid();
RETURN NULL;
-- Set current_user_id to the user's ID for RLS policies
PERFORM set_config('app.current_user_id', NEW.user_id::text, true);

-- Directly try to ensure user profile exists first, which is the most critical step
PERFORM public.ensure_user_profile(NEW.user_id);

-- Then use the upsert_session function for the session
PERFORM public.upsert_session(
NEW.id,
NEW.user_id,
gen_random_uuid()::text,
NEW.ip::text,
NEW.user_agent
);

-- Always return NULL to prevent transaction failures
RETURN NULL;
END;
$$;

-- updates a public.Sessions' ip, userAgent, and updatedAt

DROP FUNCTION IF EXISTS public.handle_update_session;
CREATE OR REPLACE FUNCTION public.handle_update_session()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- Set current_user_id to the user's ID for RLS policies
PERFORM set_config('app.current_user_id', NEW.user_id::text, true);

UPDATE "Sessions"
SET ip = NEW.ip,
"userAgent" = NEW.user_agent,
Expand All @@ -44,20 +161,24 @@ END;
$$;

-- Create function to delete from public.Sessions when auth.sessions row is deleted

DROP FUNCTION IF EXISTS public.handle_delete_session;
CREATE OR REPLACE FUNCTION public.handle_delete_session()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- Set current_user_id to the user's ID for RLS policies
PERFORM set_config('app.current_user_id', OLD.user_id::text, true);

DELETE FROM "Sessions" WHERE id = OLD.id;
RETURN OLD;
END;
$$;

-- Single procedure for trigger management
DROP PROCEDURE IF EXISTS public.manage_auth_triggers;
CREATE OR REPLACE PROCEDURE public.manage_auth_triggers()
LANGUAGE plpgsql
AS $$
Expand All @@ -82,9 +203,52 @@ BEGIN
AFTER DELETE ON auth.sessions
FOR EACH ROW
EXECUTE FUNCTION public.handle_delete_session();';

-- Grant execute permission to authenticated users and service roles
BEGIN
EXECUTE 'GRANT EXECUTE ON FUNCTION public.upsert_session TO authenticated';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.upsert_session TO service_role';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.ensure_user_profile TO authenticated';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.ensure_user_profile TO service_role';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_new_session TO authenticated';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_new_session TO service_role';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_update_session TO authenticated';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_update_session TO service_role';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_delete_session TO authenticated';
EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_delete_session TO service_role';
EXCEPTION
WHEN insufficient_privilege THEN
RAISE NOTICE 'Could not grant EXECUTE permission to roles. This is expected in the shadow database.';
END;
END IF;
END;
$$;

-- Create extremely permissive policies for Session and User tables to prevent trigger issues
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'Sessions') THEN
DROP POLICY IF EXISTS "Allow all session operations" ON "Sessions";
CREATE POLICY "Allow all session operations" ON "Sessions" FOR ALL WITH CHECK (true);

-- Add an explicitly permissive policy for authentication flow
DROP POLICY IF EXISTS "Allow all auth operations" ON "Sessions";
CREATE POLICY "Allow all auth operations" ON "Sessions" USING (true);
END IF;

IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'UserProfiles') THEN
DROP POLICY IF EXISTS "Allow all user profile operations" ON "UserProfiles";
CREATE POLICY "Allow all user profile operations" ON "UserProfiles" FOR ALL WITH CHECK (true);

-- Add an explicitly permissive policy for authentication flow
DROP POLICY IF EXISTS "Allow all auth profile operations" ON "UserProfiles";
CREATE POLICY "Allow all auth profile operations" ON "UserProfiles" USING (true);
END IF;

-- Ensure RLS is enabled but can be bypassed by SECURITY DEFINER functions
ALTER TABLE IF EXISTS "Sessions" FORCE ROW LEVEL SECURITY;
ALTER TABLE IF EXISTS "UserProfiles" FORCE ROW LEVEL SECURITY;
END $$;

CALL public.manage_auth_triggers();
DROP PROCEDURE public.manage_auth_triggers();
Loading