-- Reference schema for the lookup direct backend: Microsoft SQL Server 2016+. Direct backend only. Run each statement separately (see ddl.Statements). -- Table and column names are the lookup.DefaultSchema defaults, override them with lookup.Config.Schema. -- Generated by the project, edit freely for your deployment (types, collations, extra columns). -- password: bcrypt hash (nullable for OAuth2 users), legacy cleartext is accepted at login -- roles: comma-separated roles IF OBJECT_ID(N'users', N'U') IS NULL CREATE TABLE users ( id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(255) NOT NULL UNIQUE, email NVARCHAR(255) NOT NULL UNIQUE, password NVARCHAR(255), user_level INT DEFAULT 0, roles NVARCHAR(500), is_active BIT DEFAULT 1, created_at DATETIME2 DEFAULT SYSUTCDATETIME(), updated_at DATETIME2 DEFAULT SYSUTCDATETIME(), last_login_at DATETIME2, program_user_id INT DEFAULT 0, program_user_table NVARCHAR(255) DEFAULT '', remote_id NVARCHAR(255), auth_provider NVARCHAR(50), totp_secret NVARCHAR(255), totp_enabled BIT DEFAULT 0, totp_enabled_at DATETIME2 ); IF OBJECT_ID(N'user_sessions', N'U') IS NULL CREATE TABLE user_sessions ( id INT IDENTITY(1,1) PRIMARY KEY, session_token NVARCHAR(450) NOT NULL UNIQUE, user_id INT NOT NULL, expires_at DATETIME2 NOT NULL, created_at DATETIME2 DEFAULT SYSUTCDATETIME(), last_activity_at DATETIME2 DEFAULT SYSUTCDATETIME(), ip_address NVARCHAR(45), user_agent NVARCHAR(MAX), access_token NVARCHAR(MAX), refresh_token NVARCHAR(MAX), token_type NVARCHAR(50) DEFAULT 'Bearer', auth_provider NVARCHAR(50), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_sessions_user_id' AND object_id = OBJECT_ID(N'user_sessions')) CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_sessions_expires_at' AND object_id = OBJECT_ID(N'user_sessions')) CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at); IF OBJECT_ID(N'token_blacklist', N'U') IS NULL CREATE TABLE token_blacklist ( id INT IDENTITY(1,1) PRIMARY KEY, token NVARCHAR(500) NOT NULL, user_id INT, expires_at DATETIME2 NOT NULL, created_at DATETIME2 DEFAULT SYSUTCDATETIME(), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- code_hash: SHA-256 hex of the backup code IF OBJECT_ID(N'user_totp_backup_codes', N'U') IS NULL CREATE TABLE user_totp_backup_codes ( id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL, code_hash NVARCHAR(64) NOT NULL, used BIT DEFAULT 0, used_at DATETIME2, created_at DATETIME2 DEFAULT SYSUTCDATETIME(), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_totp_user_id' AND object_id = OBJECT_ID(N'user_totp_backup_codes')) CREATE INDEX idx_totp_user_id ON user_totp_backup_codes(user_id); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_totp_code_hash' AND object_id = OBJECT_ID(N'user_totp_backup_codes')) CREATE INDEX idx_totp_code_hash ON user_totp_backup_codes(code_hash); -- credential_id: base64 text -- public_key: base64 text -- aaguid: base64 text -- transports: JSON-encoded array IF OBJECT_ID(N'user_passkey_credentials', N'U') IS NULL CREATE TABLE user_passkey_credentials ( id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL, credential_id VARCHAR(900) NOT NULL UNIQUE, public_key NVARCHAR(MAX) NOT NULL, attestation_type NVARCHAR(50) DEFAULT 'none', aaguid NVARCHAR(MAX), sign_count INT DEFAULT 0, clone_warning BIT DEFAULT 0, transports NVARCHAR(MAX), backup_eligible BIT DEFAULT 0, backup_state BIT DEFAULT 0, name NVARCHAR(255), created_at DATETIME2 DEFAULT SYSUTCDATETIME(), last_used_at DATETIME2 DEFAULT SYSUTCDATETIME(), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_passkey_user_id' AND object_id = OBJECT_ID(N'user_passkey_credentials')) CREATE INDEX idx_passkey_user_id ON user_passkey_credentials(user_id); -- token_hash: SHA-256 hex of the raw token IF OBJECT_ID(N'user_password_resets', N'U') IS NULL CREATE TABLE user_password_resets ( id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL, token_hash NVARCHAR(64) NOT NULL UNIQUE, expires_at DATETIME2 NOT NULL, created_at DATETIME2 DEFAULT SYSUTCDATETIME(), used BIT DEFAULT 0, used_at DATETIME2, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_pw_reset_user_id' AND object_id = OBJECT_ID(N'user_password_resets')) CREATE INDEX idx_pw_reset_user_id ON user_password_resets(user_id); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_pw_reset_expires_at' AND object_id = OBJECT_ID(N'user_password_resets')) CREATE INDEX idx_pw_reset_expires_at ON user_password_resets(expires_at); -- redirect_uris: JSON-encoded array -- grant_types: JSON-encoded array -- allowed_scopes: JSON-encoded array -- client_secret_hash: SHA-256 hex of the confidential-client secret, NULL for public clients IF OBJECT_ID(N'oauth_clients', N'U') IS NULL CREATE TABLE oauth_clients ( id INT IDENTITY(1,1) PRIMARY KEY, client_id NVARCHAR(255) NOT NULL UNIQUE, redirect_uris NVARCHAR(MAX) NOT NULL, client_name NVARCHAR(255), grant_types NVARCHAR(MAX), allowed_scopes NVARCHAR(MAX), client_secret_hash NVARCHAR(MAX), token_endpoint_auth_method NVARCHAR(30) DEFAULT 'none', is_active BIT DEFAULT 1, created_at DATETIME2 DEFAULT SYSUTCDATETIME() ); -- scopes: JSON-encoded array IF OBJECT_ID(N'oauth_codes', N'U') IS NULL CREATE TABLE oauth_codes ( id INT IDENTITY(1,1) PRIMARY KEY, code NVARCHAR(255) NOT NULL UNIQUE, client_id NVARCHAR(255) NOT NULL, redirect_uri NVARCHAR(MAX) NOT NULL, client_state NVARCHAR(MAX), code_challenge NVARCHAR(255) NOT NULL, code_challenge_method NVARCHAR(10) DEFAULT 'S256', session_token NVARCHAR(MAX) NOT NULL, refresh_token NVARCHAR(MAX), scopes NVARCHAR(MAX), expires_at DATETIME2 NOT NULL, created_at DATETIME2 DEFAULT SYSUTCDATETIME() ); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_codes_expires' AND object_id = OBJECT_ID(N'oauth_codes')) CREATE INDEX idx_oauth_codes_expires ON oauth_codes(expires_at); -- key_hash: SHA-256 hex -- scopes: JSON-encoded array -- meta: JSON-encoded object IF OBJECT_ID(N'user_keys', N'U') IS NULL CREATE TABLE user_keys ( id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL, key_type NVARCHAR(50) NOT NULL, key_hash NVARCHAR(64) NOT NULL UNIQUE, name NVARCHAR(255) NOT NULL DEFAULT '', scopes NVARCHAR(MAX), meta NVARCHAR(MAX), expires_at DATETIME2, created_at DATETIME2 DEFAULT SYSUTCDATETIME(), last_used_at DATETIME2, is_active BIT DEFAULT 1, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_keys_user_id' AND object_id = OBJECT_ID(N'user_keys')) CREATE INDEX idx_user_keys_user_id ON user_keys(user_id); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_keys_key_type' AND object_id = OBJECT_ID(N'user_keys')) CREATE INDEX idx_user_keys_key_type ON user_keys(key_type); -- Optional: omit to use per-user rules only. IF OBJECT_ID(N'sec_group_members', N'U') IS NULL CREATE TABLE sec_group_members ( group_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY (group_id, user_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- column_path: dot path under the table: col or col.sub.field -- access_type: mask, hide, read, ... -- extra_filters: JSON object IF OBJECT_ID(N'sec_column_rules', N'U') IS NULL CREATE TABLE sec_column_rules ( id INT IDENTITY(1,1) PRIMARY KEY, user_id INT, group_id INT, schema_name NVARCHAR(255) NOT NULL, table_name NVARCHAR(255) NOT NULL, column_path NVARCHAR(255) NOT NULL, access_type NVARCHAR(50) NOT NULL, mask_start INT DEFAULT 0, mask_end INT DEFAULT 0, mask_invert BIT DEFAULT 0, mask_char NVARCHAR(10) DEFAULT '*', extra_filters NVARCHAR(MAX), is_active BIT NOT NULL DEFAULT 1, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL)) ); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_sec_column_rules_table' AND object_id = OBJECT_ID(N'sec_column_rules')) CREATE INDEX idx_sec_column_rules_table ON sec_column_rules(schema_name, table_name); -- template: SQL fragment, e.g. user_id = {UserID} IF OBJECT_ID(N'sec_row_rules', N'U') IS NULL CREATE TABLE sec_row_rules ( id INT IDENTITY(1,1) PRIMARY KEY, user_id INT, group_id INT, schema_name NVARCHAR(255) NOT NULL, table_name NVARCHAR(255) NOT NULL, template NVARCHAR(MAX), has_block BIT NOT NULL DEFAULT 0, is_active BIT NOT NULL DEFAULT 1, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL)) ); IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_sec_row_rules_table' AND object_id = OBJECT_ID(N'sec_row_rules')) CREATE INDEX idx_sec_row_rules_table ON sec_row_rules(schema_name, table_name);