-- 010_auth_and_users.sql -- Core Authentication and User Identity Tables (Aligned with ORIGIN) -- === USERS === CREATE TABLE public.users ( -- Primary Key id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Authentication email VARCHAR(255) NOT NULL, email_verified_at TIMESTAMPTZ, password_hash VARCHAR(255), -- Profile Basic username VARCHAR(30) NOT NULL, slug VARCHAR(255), first_name VARCHAR(100), last_name VARCHAR(100), display_name VARCHAR(100), -- Legacy Profile fields (kept for Go compatibility, prefer user_profiles) avatar TEXT, bio TEXT, location VARCHAR(100), birthdate TIMESTAMPTZ, gender VARCHAR(20), -- Role & Status role public.user_role NOT NULL DEFAULT 'user', is_active BOOLEAN NOT NULL DEFAULT true, is_verified BOOLEAN NOT NULL DEFAULT false, is_banned BOOLEAN NOT NULL DEFAULT false, is_admin BOOLEAN DEFAULT false, -- Legacy boolean, prefer role='admin' is_public BOOLEAN DEFAULT true, -- Legacy visibility -- Security token_version INTEGER NOT NULL DEFAULT 0, last_password_change_at TIMESTAMPTZ, -- Tracking last_login_at TIMESTAMPTZ, login_count INTEGER NOT NULL DEFAULT 0, last_login_ip INET, username_changed_at TIMESTAMPTZ, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, -- Constraints CONSTRAINT chk_users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), CONSTRAINT chk_users_username_format CHECK (username ~* '^[a-zA-Z0-9_]{3,30}$') ); -- Indexes CREATE UNIQUE INDEX idx_users_email ON public.users(email) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX idx_users_username ON public.users(username) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX idx_users_slug ON public.users(slug) WHERE deleted_at IS NULL; CREATE INDEX idx_users_role ON public.users(role); CREATE INDEX idx_users_created_at_desc ON public.users(created_at DESC); CREATE INDEX idx_users_deleted_at ON public.users(deleted_at) WHERE deleted_at IS NOT NULL; -- === FEDERATED IDENTITIES (OAuth) === CREATE TABLE public.federated_identities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, -- Provider provider VARCHAR(50) NOT NULL, provider_user_id VARCHAR(255) NOT NULL, -- ORIGIN name provider_id TEXT, -- Legacy name (kept for compatibility if needed, else deprecate) -- OAuth Data access_token TEXT, refresh_token TEXT, token_expires_at TIMESTAMPTZ, -- ORIGIN name expires_at TIMESTAMPTZ, -- Legacy name -- Profile Data provider_email VARCHAR(255), provider_username VARCHAR(255), provider_avatar_url TEXT, provider_profile_data JSONB, -- Legacy fields email TEXT, -- Maps to provider_email display_name TEXT, avatar_url TEXT, -- Maps to provider_avatar_url -- Status is_primary BOOLEAN NOT NULL DEFAULT false, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_federated_identities_provider_user UNIQUE (provider, provider_user_id) ); CREATE INDEX idx_federated_identities_user_id ON public.federated_identities(user_id); CREATE INDEX idx_federated_identities_provider ON public.federated_identities(provider); -- === REFRESH TOKENS === CREATE TABLE public.refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, -- Token token VARCHAR(255) NOT NULL UNIQUE, token_hash VARCHAR(255) NOT NULL, -- Metadata device_name VARCHAR(255), device_type VARCHAR(50), user_agent TEXT, ip_address INET, -- Expiration expires_at TIMESTAMPTZ NOT NULL, last_used_at TIMESTAMPTZ, -- Status is_revoked BOOLEAN NOT NULL DEFAULT false, revoked_at TIMESTAMPTZ, revoked_reason VARCHAR(255), -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, -- Legacy soft delete CONSTRAINT chk_refresh_tokens_expires_future CHECK (expires_at > created_at) ); CREATE INDEX idx_refresh_tokens_user_id ON public.refresh_tokens(user_id); CREATE INDEX idx_refresh_tokens_token_hash ON public.refresh_tokens(token_hash); CREATE INDEX idx_refresh_tokens_expires_at ON public.refresh_tokens(expires_at); CREATE INDEX idx_refresh_tokens_is_revoked ON public.refresh_tokens(is_revoked) WHERE is_revoked = false; -- === PASSWORD RESET TOKENS === CREATE TABLE public.password_reset_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, -- Token token VARCHAR(255) NOT NULL UNIQUE, token_hash VARCHAR(255) NOT NULL, -- Status used BOOLEAN NOT NULL DEFAULT false, used_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL, -- Metadata ip_address INET, user_agent TEXT, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_password_reset_expires CHECK (expires_at > created_at) ); CREATE INDEX idx_password_reset_tokens_user_id ON public.password_reset_tokens(user_id); CREATE INDEX idx_password_reset_tokens_token_hash ON public.password_reset_tokens(token_hash); CREATE INDEX idx_password_reset_tokens_expires_at ON public.password_reset_tokens(expires_at); -- === EMAIL VERIFICATION TOKENS === CREATE TABLE public.email_verification_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, -- Token token VARCHAR(255) NOT NULL UNIQUE, token_hash VARCHAR(255) NOT NULL, -- Email email VARCHAR(255) NOT NULL, -- Status verified BOOLEAN NOT NULL DEFAULT false, -- Legacy used used BOOLEAN NOT NULL DEFAULT false, -- Legacy used verified_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_email_verification_expires CHECK (expires_at > created_at) ); CREATE INDEX idx_email_verification_tokens_user_id ON public.email_verification_tokens(user_id); CREATE INDEX idx_email_verification_tokens_token_hash ON public.email_verification_tokens(token_hash); CREATE INDEX idx_email_verification_tokens_email ON public.email_verification_tokens(email); -- === USER SESSIONS (Legacy/Auth) === CREATE TABLE public.user_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, session_token VARCHAR(255) NOT NULL UNIQUE, ip_address INET, -- Changed to INET per Origin style user_agent TEXT, is_active BOOLEAN DEFAULT true, last_activity TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_user_sessions_user_id ON public.user_sessions(user_id); CREATE INDEX idx_user_sessions_expires_at ON public.user_sessions(expires_at); CREATE INDEX idx_user_sessions_last_activity ON public.user_sessions(last_activity DESC);