Skip to content
Merged
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,224 changes: 1,193 additions & 31 deletions docs/schema-postgres.sql

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/schema-summary.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- PostgreSQL schema entrypoint (domain-splitted)
-- Usage (from repository root):
-- psql -U <user> -d <db> -f docs/schema-postgres.sql

\ir sql/postgres/00_base.sql
\ir sql/postgres/10_identity.sql
\ir sql/postgres/20_storage.sql
\ir sql/postgres/30_access_share.sql
\ir sql/postgres/40_audit_security.sql
\ir sql/postgres/50_maintenance_views.sql
\ir sql/postgres/90_comments.sql
81 changes: 81 additions & 0 deletions docs/sql/postgres/00_base.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
-- =========================
-- Base: extensions, enum types, common trigger functions
-- =========================

CREATE EXTENSION IF NOT EXISTS pg_trgm;

DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'upload_status_enum') THEN
CREATE TYPE upload_status_enum AS ENUM ('uploading', 'active', 'deleted', 'failed');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'folder_status_enum') THEN
CREATE TYPE folder_status_enum AS ENUM ('active', 'deleted');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'file_status_enum') THEN
CREATE TYPE file_status_enum AS ENUM ('active', 'deleted');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'upload_mode_enum') THEN
CREATE TYPE upload_mode_enum AS ENUM ('single', 'multipart');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'upload_task_status_enum') THEN
CREATE TYPE upload_task_status_enum AS ENUM ('init', 'uploading', 'completed', 'aborted', 'failed');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'upload_part_status_enum') THEN
CREATE TYPE upload_part_status_enum AS ENUM ('pending', 'uploaded', 'failed');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_status_enum') THEN
CREATE TYPE user_status_enum AS ENUM ('pending_verification', 'active', 'locked', 'disabled');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'folder_type_enum') THEN
CREATE TYPE folder_type_enum AS ENUM ('normal', 'root', 'system');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'share_status_enum') THEN
CREATE TYPE share_status_enum AS ENUM ('active', 'expired', 'revoked', 'deleted');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'share_member_status_enum') THEN
CREATE TYPE share_member_status_enum AS ENUM ('pending', 'accepted', 'rejected', 'revoked');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'favorite_item_type_enum') THEN
CREATE TYPE favorite_item_type_enum AS ENUM ('file', 'folder');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'view_mode_enum') THEN
CREATE TYPE view_mode_enum AS ENUM ('list', 'grid');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'sort_by_enum') THEN
CREATE TYPE sort_by_enum AS ENUM ('name', 'size', 'created_at', 'updated_at', 'last_accessed_at');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'sort_direction_enum') THEN
CREATE TYPE sort_direction_enum AS ENUM ('asc', 'desc');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'preview_status_enum') THEN
CREATE TYPE preview_status_enum AS ENUM ('pending', 'ready', 'failed');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'scan_result_enum') THEN
CREATE TYPE scan_result_enum AS ENUM ('pending', 'clean', 'infected', 'blocked', 'failed');
END IF;
END
$$;

CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
96 changes: 96 additions & 0 deletions docs/sql/postgres/10_identity.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
-- =========================
-- Domain: identity
-- =========================

CREATE TABLE IF NOT EXISTS "user" (
user_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
username VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'USER',
status user_status_enum NOT NULL DEFAULT 'active',
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
email_verified_at TIMESTAMP NULL,
storage_limit BIGINT NOT NULL DEFAULT 10737418240,
storage_used BIGINT NOT NULL DEFAULT 0,
last_login_at TIMESTAMP NULL,
failed_login_count INTEGER NOT NULL DEFAULT 0,
locked_until TIMESTAMP NULL,
password_changed_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE UNIQUE INDEX IF NOT EXISTS uk_user_username_ci ON "user" ((LOWER(username)));
CREATE UNIQUE INDEX IF NOT EXISTS uk_user_email_ci ON "user" ((LOWER(email)));
CREATE INDEX IF NOT EXISTS idx_user_status ON "user" (status);
CREATE INDEX IF NOT EXISTS idx_user_locked_until ON "user" (locked_until);

CREATE TRIGGER trg_user_updated_at
BEFORE UPDATE ON "user"
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();

CREATE TABLE IF NOT EXISTS user_group (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description VARCHAR(255)
);

CREATE TABLE IF NOT EXISTS user_group_member (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
group_id BIGINT NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'member',
CONSTRAINT fk_ugm_user FOREIGN KEY (user_id) REFERENCES "user"(user_id) ON DELETE CASCADE,
CONSTRAINT fk_ugm_group FOREIGN KEY (group_id) REFERENCES user_group(id) ON DELETE CASCADE,
CONSTRAINT uk_user_group UNIQUE (user_id, group_id)
);

CREATE TABLE IF NOT EXISTS password_reset_token (
token_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
token_hash CHAR(64) NOT NULL UNIQUE,
expire_at TIMESTAMP NOT NULL,
used_at TIMESTAMP NULL,
requester_ip VARCHAR(45) NULL,
user_agent VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_password_reset_token_user FOREIGN KEY (user_id) REFERENCES "user"(user_id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_password_reset_token_user_expire
ON password_reset_token (user_id, expire_at);

CREATE TABLE IF NOT EXISTS email_verification_token (
token_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
token_hash CHAR(64) NOT NULL UNIQUE,
expire_at TIMESTAMP NOT NULL,
verified_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_email_verification_token_user FOREIGN KEY (user_id) REFERENCES "user"(user_id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_email_verification_token_user_expire
ON email_verification_token (user_id, expire_at);

CREATE TABLE IF NOT EXISTS user_session (
session_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
refresh_token_hash CHAR(64) NOT NULL UNIQUE,
client_type VARCHAR(50) NOT NULL DEFAULT 'web',
device_id VARCHAR(255) NULL,
device_name VARCHAR(255) NULL,
ip_address VARCHAR(45) NULL,
user_agent VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expire_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP NULL,
CONSTRAINT fk_user_session_user FOREIGN KEY (user_id) REFERENCES "user"(user_id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_user_session_user_expire
ON user_session (user_id, expire_at);
Loading