Fix ship physics init order and Supabase RLS user sync
All checks were successful
Build / build (push) Successful in 1m47s
All checks were successful
Build / build (push) Successful in 1m47s
- Pass initial position to ship.initialize() to set position BEFORE creating physics body, preventing collision race condition on reload - Use get_or_create_user_id RPC (security definer) to bypass RLS for user profile sync in both authService and cloudLeaderboardService - Sync user to Supabase on Auth0 login to ensure profile exists - Add Supabase schema.sql and policies.sql for documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d3d6175360
commit
c87b85de40
@ -353,12 +353,10 @@ export class Level1 implements Level {
|
|||||||
log.error('Initialize called twice');
|
log.error('Initialize called twice');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this._ship.initialize();
|
// Get ship config BEFORE initialize to pass position (avoids physics race condition)
|
||||||
setLoadingMessage("Loading level from configuration...");
|
|
||||||
|
|
||||||
// Apply ship configuration from level config
|
|
||||||
const shipConfig = this._deserializer.getShipConfig();
|
const shipConfig = this._deserializer.getShipConfig();
|
||||||
this._ship.position = new Vector3(...shipConfig.position);
|
await this._ship.initialize(new Vector3(...shipConfig.position));
|
||||||
|
setLoadingMessage("Loading level from configuration...");
|
||||||
|
|
||||||
if (shipConfig.linearVelocity) {
|
if (shipConfig.linearVelocity) {
|
||||||
this._ship.setLinearVelocity(new Vector3(...shipConfig.linearVelocity));
|
this._ship.setLinearVelocity(new Vector3(...shipConfig.linearVelocity));
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { createAuth0Client, Auth0Client, User } from '@auth0/auth0-spa-js';
|
import { createAuth0Client, Auth0Client, User } from '@auth0/auth0-spa-js';
|
||||||
import log from '../core/logger';
|
import log from '../core/logger';
|
||||||
|
import { SupabaseService } from './supabaseService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton service for managing Auth0 authentication
|
* Singleton service for managing Auth0 authentication
|
||||||
@ -89,6 +90,9 @@ export class AuthService {
|
|||||||
email: this._user?.email,
|
email: this._user?.email,
|
||||||
sub: this._user?.sub
|
sub: this._user?.sub
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync user to Supabase (fire and forget - don't block init)
|
||||||
|
this.syncUserToSupabase();
|
||||||
} else {
|
} else {
|
||||||
log.info('[AuthService] User not authenticated');
|
log.info('[AuthService] User not authenticated');
|
||||||
}
|
}
|
||||||
@ -156,6 +160,32 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync user to Supabase users table
|
||||||
|
* Called after successful authentication
|
||||||
|
* Uses RPC to bypass RLS via security definer function
|
||||||
|
*/
|
||||||
|
private async syncUserToSupabase(): Promise<void> {
|
||||||
|
if (!this._user?.sub) return;
|
||||||
|
|
||||||
|
const supabase = SupabaseService.getInstance();
|
||||||
|
if (!supabase.isConfigured()) return;
|
||||||
|
|
||||||
|
const client = await supabase.getAuthenticatedClient();
|
||||||
|
if (!client) return;
|
||||||
|
|
||||||
|
// Use security definer function to create/get user (bypasses RLS)
|
||||||
|
const { data, error } = await client.rpc('get_or_create_user_id', {
|
||||||
|
p_auth0_id: this._user.sub
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
log.warn('[AuthService] Failed to sync user to Supabase:', error);
|
||||||
|
} else {
|
||||||
|
log.info('[AuthService] User synced to Supabase, UUID:', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user logged in via Facebook
|
* Check if user logged in via Facebook
|
||||||
* Auth0 stores the identity provider in the user's sub claim
|
* Auth0 stores the identity provider in the user's sub claim
|
||||||
|
|||||||
@ -62,35 +62,32 @@ export class CloudLeaderboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure user exists in the users table with current display name
|
* Ensure user exists in the users table
|
||||||
* Called before submitting scores
|
* Called before submitting scores
|
||||||
|
* Uses RPC to bypass RLS via security definer function
|
||||||
|
* @returns The internal UUID of the user, or null on failure
|
||||||
*/
|
*/
|
||||||
private async ensureUserProfile(userId: string, displayName: string): Promise<boolean> {
|
private async ensureUserProfile(auth0Id: string): Promise<string | null> {
|
||||||
const supabase = SupabaseService.getInstance();
|
const supabase = SupabaseService.getInstance();
|
||||||
const client = await supabase.getAuthenticatedClient();
|
const client = await supabase.getAuthenticatedClient();
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
log.warn('[CloudLeaderboardService] Not authenticated - cannot sync user');
|
log.warn('[CloudLeaderboardService] Not authenticated - cannot sync user');
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert the user (insert or update if exists)
|
// Use security definer function to create/get user (bypasses RLS)
|
||||||
const { error } = await client
|
const { data, error } = await client.rpc('get_or_create_user_id', {
|
||||||
.from('users')
|
p_auth0_id: auth0Id
|
||||||
.upsert({
|
|
||||||
user_id: userId,
|
|
||||||
display_name: displayName
|
|
||||||
}, {
|
|
||||||
onConflict: 'user_id'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
log.error('[CloudLeaderboardService] Failed to sync user:', error);
|
log.error('[CloudLeaderboardService] Failed to sync user:', error);
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('[CloudLeaderboardService] User synced:', userId);
|
log.info('[CloudLeaderboardService] User synced:', auth0Id, '-> UUID:', data);
|
||||||
return true;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,11 +120,15 @@ export class CloudLeaderboardService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user profile exists with current display name
|
// Ensure user profile exists and get the internal UUID
|
||||||
await this.ensureUserProfile(user.sub, result.playerName);
|
const internalUserId = await this.ensureUserProfile(user.sub);
|
||||||
|
if (!internalUserId) {
|
||||||
|
log.warn('[CloudLeaderboardService] Failed to get/create user profile');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const entry = {
|
const entry = {
|
||||||
user_id: user.sub,
|
user_id: internalUserId,
|
||||||
level_id: result.levelId,
|
level_id: result.levelId,
|
||||||
level_name: result.levelName,
|
level_name: result.levelName,
|
||||||
completed: result.completed,
|
completed: result.completed,
|
||||||
|
|||||||
@ -148,16 +148,18 @@ export class Ship {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initialize() {
|
public async initialize(initialPosition?: Vector3) {
|
||||||
this._scoreboard = new Scoreboard();
|
this._scoreboard = new Scoreboard();
|
||||||
this._scoreboard.setShip(this); // Pass ship reference for velocity reading
|
this._scoreboard.setShip(this); // Pass ship reference for velocity reading
|
||||||
this._gameStats = new GameStats();
|
this._gameStats = new GameStats();
|
||||||
this._ship = new TransformNode("shipBase", DefaultScene.MainScene);
|
this._ship = new TransformNode("shipBase", DefaultScene.MainScene);
|
||||||
const data = await loadAsset("ship.glb");
|
const data = await loadAsset("ship.glb");
|
||||||
this._ship = data.container.transformNodes[0];
|
this._ship = data.container.transformNodes[0];
|
||||||
//this._ship.rotation = new Vector3(0, Math.PI, 0);
|
|
||||||
// this._ship.id = "Ship"; // Set ID so mission brief can find it
|
// Set position BEFORE creating physics body to avoid collision race condition
|
||||||
// Position is now set from level config in Level1.initialize()
|
if (initialPosition) {
|
||||||
|
this._ship.position.copyFrom(initialPosition);
|
||||||
|
}
|
||||||
|
|
||||||
// Create physics if enabled
|
// Create physics if enabled
|
||||||
const config = GameConfig.getInstance();
|
const config = GameConfig.getInstance();
|
||||||
|
|||||||
310
supabase/policies.sql
Normal file
310
supabase/policies.sql
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
create policy "Anyone can read leaderboard" on public.leaderboard
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
using true;
|
||||||
|
|
||||||
|
create policy "Allow all inserts" on public.leaderboard
|
||||||
|
as permissive
|
||||||
|
for insert
|
||||||
|
with check true;
|
||||||
|
|
||||||
|
create function public.is_admin() returns boolean
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
RETURN EXISTS (
|
||||||
|
SELECT 1 FROM admins
|
||||||
|
WHERE user_id = auth.uid()::text
|
||||||
|
AND is_active = true
|
||||||
|
AND (expires_at IS NULL OR expires_at > NOW())
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.is_admin() owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.is_admin() to anon;
|
||||||
|
|
||||||
|
grant execute on function public.is_admin() to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.is_admin() to service_role;
|
||||||
|
|
||||||
|
create function public.has_admin_permission(permission text) returns boolean
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
admin_record admins%ROWTYPE;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO admin_record FROM admins
|
||||||
|
WHERE user_id = auth.uid()::text
|
||||||
|
AND is_active = true
|
||||||
|
AND (expires_at IS NULL OR expires_at > NOW());
|
||||||
|
|
||||||
|
IF admin_record IS NULL THEN
|
||||||
|
RETURN false;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
CASE permission
|
||||||
|
WHEN 'review_levels' THEN RETURN admin_record.can_review_levels;
|
||||||
|
WHEN 'manage_admins' THEN RETURN admin_record.can_manage_admins;
|
||||||
|
WHEN 'manage_official' THEN RETURN admin_record.can_manage_official;
|
||||||
|
WHEN 'view_analytics' THEN RETURN admin_record.can_view_analytics;
|
||||||
|
ELSE RETURN false;
|
||||||
|
END CASE;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.has_admin_permission(text) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.has_admin_permission(text) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.has_admin_permission(text) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.has_admin_permission(text) to service_role;
|
||||||
|
|
||||||
|
create function public.validate_slug(slug text) returns boolean
|
||||||
|
immutable
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
-- Allow NULL slugs (optional)
|
||||||
|
IF slug IS NULL THEN
|
||||||
|
RETURN true;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Must be 3-50 chars, lowercase alphanumeric with hyphens, no leading/trailing hyphens
|
||||||
|
RETURN slug ~ '^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.validate_slug(text) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.validate_slug(text) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.validate_slug(text) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.validate_slug(text) to service_role;
|
||||||
|
|
||||||
|
create function public.is_slug_available(check_slug text, exclude_level_id uuid DEFAULT NULL::uuid) returns boolean
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF check_slug IS NULL THEN
|
||||||
|
RETURN true;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NOT EXISTS (
|
||||||
|
SELECT 1 FROM levels
|
||||||
|
WHERE slug = check_slug
|
||||||
|
AND (exclude_level_id IS NULL OR id != exclude_level_id)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.is_slug_available(text, uuid) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.is_slug_available(text, uuid) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.is_slug_available(text, uuid) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.is_slug_available(text, uuid) to service_role;
|
||||||
|
|
||||||
|
create function public.submit_level_for_review(level_id uuid) returns void
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
UPDATE levels
|
||||||
|
SET level_type = 'pending_review',
|
||||||
|
submitted_at = NOW()
|
||||||
|
WHERE id = level_id
|
||||||
|
AND user_id = auth_user_id()
|
||||||
|
AND level_type IN ('private', 'rejected');
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.submit_level_for_review(uuid) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.submit_level_for_review(uuid) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.submit_level_for_review(uuid) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.submit_level_for_review(uuid) to service_role;
|
||||||
|
|
||||||
|
create function public.approve_level(p_level_id uuid, p_notes text DEFAULT NULL::text) returns void
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF NOT has_admin_permission('review_levels') THEN
|
||||||
|
RAISE EXCEPTION 'Permission denied';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE levels
|
||||||
|
SET
|
||||||
|
level_type = 'published',
|
||||||
|
reviewed_at = NOW(),
|
||||||
|
reviewed_by = auth.uid()::text,
|
||||||
|
review_notes = p_notes,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_level_id
|
||||||
|
AND level_type = 'pending_review';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.approve_level(uuid, text) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.approve_level(uuid, text) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.approve_level(uuid, text) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.approve_level(uuid, text) to service_role;
|
||||||
|
|
||||||
|
create function public.reject_level(p_level_id uuid, p_notes text) returns void
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF NOT has_admin_permission('review_levels') THEN
|
||||||
|
RAISE EXCEPTION 'Permission denied';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE levels
|
||||||
|
SET
|
||||||
|
level_type = 'rejected',
|
||||||
|
reviewed_at = NOW(),
|
||||||
|
reviewed_by = auth.uid()::text,
|
||||||
|
review_notes = p_notes,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_level_id
|
||||||
|
AND level_type = 'pending_review';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.reject_level(uuid, text) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.reject_level(uuid, text) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.reject_level(uuid, text) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.reject_level(uuid, text) to service_role;
|
||||||
|
|
||||||
|
create function public.update_updated_at() returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.update_updated_at() owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.update_updated_at() to anon;
|
||||||
|
|
||||||
|
grant execute on function public.update_updated_at() to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.update_updated_at() to service_role;
|
||||||
|
|
||||||
|
create function public.increment_play_count(p_level_id uuid) returns void
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
UPDATE levels
|
||||||
|
SET play_count = play_count + 1
|
||||||
|
WHERE id = p_level_id;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.increment_play_count(uuid) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.increment_play_count(uuid) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.increment_play_count(uuid) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.increment_play_count(uuid) to service_role;
|
||||||
|
|
||||||
|
create function public.increment_completion_count(p_level_id uuid) returns void
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
UPDATE levels
|
||||||
|
SET completion_count = completion_count + 1
|
||||||
|
WHERE id = p_level_id;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.increment_completion_count(uuid) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.increment_completion_count(uuid) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.increment_completion_count(uuid) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.increment_completion_count(uuid) to service_role;
|
||||||
|
|
||||||
|
create function public.get_or_create_user_id(p_auth0_id text) returns uuid
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
BEGIN
|
||||||
|
-- Try to find existing user
|
||||||
|
SELECT id INTO v_user_id FROM users WHERE auth0_id = p_auth0_id;
|
||||||
|
|
||||||
|
-- Create if not found
|
||||||
|
IF v_user_id IS NULL THEN
|
||||||
|
INSERT INTO users (auth0_id) VALUES (p_auth0_id)
|
||||||
|
RETURNING id INTO v_user_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN v_user_id;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.get_or_create_user_id(text) owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.get_or_create_user_id(text) to anon;
|
||||||
|
|
||||||
|
grant execute on function public.get_or_create_user_id(text) to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.get_or_create_user_id(text) to service_role;
|
||||||
|
|
||||||
|
create function public.auth_user_id() returns uuid
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
language plpgsql
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
RETURN (
|
||||||
|
SELECT id FROM users
|
||||||
|
WHERE auth0_id = auth.jwt() ->> 'sub'
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.auth_user_id() owner to postgres;
|
||||||
|
|
||||||
|
grant execute on function public.auth_user_id() to anon;
|
||||||
|
|
||||||
|
grant execute on function public.auth_user_id() to authenticated;
|
||||||
|
|
||||||
|
grant execute on function public.auth_user_id() to service_role;
|
||||||
|
|
||||||
228
supabase/schema.sql
Normal file
228
supabase/schema.sql
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
create table public._migrations
|
||||||
|
(
|
||||||
|
id serial
|
||||||
|
primary key,
|
||||||
|
name text not null
|
||||||
|
unique,
|
||||||
|
executed_at timestamp with time zone default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table public._migrations
|
||||||
|
owner to postgres;
|
||||||
|
|
||||||
|
grant select, update, usage on sequence public._migrations_id_seq to anon;
|
||||||
|
|
||||||
|
grant select, update, usage on sequence public._migrations_id_seq to authenticated;
|
||||||
|
|
||||||
|
grant select, update, usage on sequence public._migrations_id_seq to service_role;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public._migrations to anon;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public._migrations to authenticated;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public._migrations to service_role;
|
||||||
|
|
||||||
|
create table public.users
|
||||||
|
(
|
||||||
|
id uuid default gen_random_uuid() not null
|
||||||
|
primary key,
|
||||||
|
auth0_id text not null
|
||||||
|
unique,
|
||||||
|
display_name text,
|
||||||
|
email text,
|
||||||
|
avatar_url text,
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
last_login_at timestamp with time zone default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table public.users
|
||||||
|
owner to postgres;
|
||||||
|
|
||||||
|
create table public.leaderboard
|
||||||
|
(
|
||||||
|
id uuid default gen_random_uuid() not null
|
||||||
|
primary key,
|
||||||
|
level_id text not null,
|
||||||
|
level_name text not null,
|
||||||
|
completed boolean not null,
|
||||||
|
end_reason text not null,
|
||||||
|
game_time_seconds numeric not null,
|
||||||
|
asteroids_destroyed integer not null,
|
||||||
|
total_asteroids integer not null,
|
||||||
|
accuracy numeric not null,
|
||||||
|
hull_damage_taken numeric not null,
|
||||||
|
fuel_consumed numeric not null,
|
||||||
|
final_score integer not null,
|
||||||
|
star_rating integer not null,
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
is_test_data boolean default false not null,
|
||||||
|
user_id uuid
|
||||||
|
constraint leaderboard_internal_user_id_fkey
|
||||||
|
references public.users
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table public.leaderboard
|
||||||
|
owner to postgres;
|
||||||
|
|
||||||
|
create index idx_leaderboard_score
|
||||||
|
on public.leaderboard (final_score desc);
|
||||||
|
|
||||||
|
create index idx_leaderboard_level
|
||||||
|
on public.leaderboard (level_id);
|
||||||
|
|
||||||
|
create index idx_leaderboard_test_data
|
||||||
|
on public.leaderboard (is_test_data)
|
||||||
|
where (is_test_data = true);
|
||||||
|
|
||||||
|
create index idx_leaderboard_user_id
|
||||||
|
on public.leaderboard (user_id);
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.leaderboard to anon;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.leaderboard to authenticated;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.leaderboard to service_role;
|
||||||
|
|
||||||
|
create table public.admins
|
||||||
|
(
|
||||||
|
id uuid default gen_random_uuid() not null
|
||||||
|
primary key,
|
||||||
|
user_id text not null
|
||||||
|
unique,
|
||||||
|
display_name text,
|
||||||
|
email text,
|
||||||
|
can_review_levels boolean default true,
|
||||||
|
can_manage_admins boolean default false,
|
||||||
|
can_manage_official boolean default false,
|
||||||
|
can_view_analytics boolean default false,
|
||||||
|
is_active boolean default true,
|
||||||
|
expires_at timestamp with time zone,
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
created_by text,
|
||||||
|
notes text,
|
||||||
|
internal_user_id uuid
|
||||||
|
references public.users
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table public.admins
|
||||||
|
owner to postgres;
|
||||||
|
|
||||||
|
create index idx_admins_user_id
|
||||||
|
on public.admins (user_id);
|
||||||
|
|
||||||
|
create index idx_admins_active
|
||||||
|
on public.admins (is_active)
|
||||||
|
where (is_active = true);
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.admins to anon;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.admins to authenticated;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.admins to service_role;
|
||||||
|
|
||||||
|
create table public.levels
|
||||||
|
(
|
||||||
|
id uuid default gen_random_uuid() not null
|
||||||
|
primary key,
|
||||||
|
slug text
|
||||||
|
unique
|
||||||
|
constraint valid_slug_format
|
||||||
|
check (validate_slug(slug)),
|
||||||
|
name text not null,
|
||||||
|
description text,
|
||||||
|
difficulty text not null
|
||||||
|
constraint valid_difficulty
|
||||||
|
check (difficulty = ANY
|
||||||
|
(ARRAY ['recruit'::text, 'pilot'::text, 'captain'::text, 'commander'::text, 'test'::text])),
|
||||||
|
estimated_time text,
|
||||||
|
tags text[] default '{}'::text[],
|
||||||
|
config jsonb not null,
|
||||||
|
mission_brief text[] default '{}'::text[],
|
||||||
|
level_type text default 'private'::text not null
|
||||||
|
constraint valid_level_type
|
||||||
|
check (level_type = ANY
|
||||||
|
(ARRAY ['official'::text, 'private'::text, 'pending_review'::text, 'published'::text, 'rejected'::text])),
|
||||||
|
sort_order integer default 0,
|
||||||
|
unlock_requirements text[] default '{}'::text[],
|
||||||
|
default_locked boolean default false,
|
||||||
|
submitted_at timestamp with time zone,
|
||||||
|
reviewed_at timestamp with time zone,
|
||||||
|
reviewed_by text,
|
||||||
|
review_notes text,
|
||||||
|
play_count integer default 0,
|
||||||
|
completion_count integer default 0,
|
||||||
|
avg_rating numeric(3, 2) default 0,
|
||||||
|
rating_count integer default 0,
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
updated_at timestamp with time zone default now(),
|
||||||
|
user_id uuid
|
||||||
|
references public.users
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table public.levels
|
||||||
|
owner to postgres;
|
||||||
|
|
||||||
|
create index idx_levels_type
|
||||||
|
on public.levels (level_type);
|
||||||
|
|
||||||
|
create index idx_levels_slug
|
||||||
|
on public.levels (slug);
|
||||||
|
|
||||||
|
create index idx_levels_official_order
|
||||||
|
on public.levels (sort_order)
|
||||||
|
where (level_type = 'official'::text);
|
||||||
|
|
||||||
|
create index idx_levels_published
|
||||||
|
on public.levels (created_at desc)
|
||||||
|
where (level_type = 'published'::text);
|
||||||
|
|
||||||
|
create index idx_levels_pending
|
||||||
|
on public.levels (submitted_at)
|
||||||
|
where (level_type = 'pending_review'::text);
|
||||||
|
|
||||||
|
create index idx_levels_user_id
|
||||||
|
on public.levels (user_id);
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.levels to anon;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.levels to authenticated;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.levels to service_role;
|
||||||
|
|
||||||
|
create table public.level_ratings
|
||||||
|
(
|
||||||
|
id uuid default gen_random_uuid() not null
|
||||||
|
primary key,
|
||||||
|
level_id uuid not null
|
||||||
|
references public.levels
|
||||||
|
on delete cascade,
|
||||||
|
rating integer not null
|
||||||
|
constraint level_ratings_rating_check
|
||||||
|
check ((rating >= 1) AND (rating <= 5)),
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
user_id uuid
|
||||||
|
references public.users,
|
||||||
|
unique (level_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table public.level_ratings
|
||||||
|
owner to postgres;
|
||||||
|
|
||||||
|
create index idx_ratings_level
|
||||||
|
on public.level_ratings (level_id);
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.level_ratings to anon;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.level_ratings to authenticated;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.level_ratings to service_role;
|
||||||
|
|
||||||
|
create index idx_users_auth0_id
|
||||||
|
on public.users (auth0_id);
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.users to anon;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.users to authenticated;
|
||||||
|
|
||||||
|
grant delete, insert, references, select, trigger, truncate, update on public.users to service_role;
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user