Compare commits
1 Commits
master
...
self-hoste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afb04e5cd3 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -39,4 +39,7 @@ postgres_data/
|
||||
#test folder profiles
|
||||
loadtest/pprof
|
||||
|
||||
/docs
|
||||
/docs
|
||||
|
||||
# K3s secrets
|
||||
k3s/secret.yaml
|
||||
@@ -265,3 +265,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS uniq_update_history_document_seq
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_update_history_document_seq
|
||||
ON document_update_history(document_id, seq);
|
||||
|
||||
-- Add 'guest' as a valid provider for guest mode login
|
||||
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_provider_check;
|
||||
ALTER TABLE users ADD CONSTRAINT users_provider_check CHECK (provider IN ('google', 'github', 'guest'));
|
||||
|
||||
49
k3s/backend.yaml
Normal file
49
k3s/backend.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: realtime-collab-backend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: realtime-collab-backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: realtime-collab-backend
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
image: realtime-collab-backend:latest
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: realtime-collab-secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "300m"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: realtime-collab-backend-svc
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: realtime-collab-backend
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
nodePort: 30080
|
||||
178
k3s/configmap.yaml
Normal file
178
k3s/configmap.yaml
Normal file
@@ -0,0 +1,178 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: postgres-init-sql
|
||||
data:
|
||||
init.sql: |
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL CHECK (type IN ('editor', 'kanban')),
|
||||
yjs_state BYTEA,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_documents_type ON documents(type);
|
||||
CREATE INDEX idx_documents_created_at ON documents(created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_updates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
update BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_updates_document_id ON document_updates(document_id);
|
||||
CREATE INDEX idx_updates_created_at ON document_updates(created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
avatar_url TEXT,
|
||||
provider VARCHAR(50) NOT NULL CHECK (provider IN ('google', 'github', 'guest')),
|
||||
provider_user_id VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_login_at TIMESTAMPTZ,
|
||||
UNIQUE(provider, provider_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_provider ON users(provider, provider_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(64) NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
user_agent TEXT,
|
||||
ip_address VARCHAR(45),
|
||||
UNIQUE(token_hash)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
|
||||
CREATE INDEX idx_sessions_token_hash ON sessions(token_hash);
|
||||
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
|
||||
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES users(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_owner_id ON documents(owner_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_shares (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
permission VARCHAR(20) NOT NULL CHECK (permission IN ('view', 'edit')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
UNIQUE(document_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_shares_document_id ON document_shares(document_id);
|
||||
CREATE INDEX idx_shares_user_id ON document_shares(user_id);
|
||||
CREATE INDEX idx_shares_permission ON document_shares(document_id, permission);
|
||||
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS share_token VARCHAR(255);
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS is_public BOOLEAN DEFAULT false NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_share_token ON documents(share_token) WHERE share_token IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_is_public ON documents(is_public) WHERE is_public = true;
|
||||
|
||||
ALTER TABLE documents ADD CONSTRAINT check_public_has_token
|
||||
CHECK (is_public = false OR (is_public = true AND share_token IS NOT NULL));
|
||||
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS share_permission VARCHAR(20) DEFAULT 'edit' CHECK (share_permission IN ('view', 'edit'));
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_share_permission ON documents(share_permission) WHERE is_public = true;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(50) NOT NULL,
|
||||
access_token TEXT NOT NULL,
|
||||
refresh_token TEXT,
|
||||
token_type VARCHAR(50) DEFAULT 'Bearer',
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
scope TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
CONSTRAINT oauth_tokens_user_id_provider_key UNIQUE (user_id, provider)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oauth_tokens_user_id ON oauth_tokens(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_versions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
yjs_snapshot BYTEA NOT NULL,
|
||||
text_preview TEXT,
|
||||
version_number INTEGER NOT NULL,
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
version_label TEXT,
|
||||
is_auto_generated BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
CONSTRAINT unique_document_version UNIQUE(document_id, version_number)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_versions_document_id ON document_versions(document_id, created_at DESC);
|
||||
CREATE INDEX idx_document_versions_created_by ON document_versions(created_by);
|
||||
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS version_count INTEGER DEFAULT 0;
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS last_snapshot_at TIMESTAMPTZ;
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_next_version_number(p_document_id UUID)
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
next_version INTEGER;
|
||||
BEGIN
|
||||
SELECT COALESCE(MAX(version_number), 0) + 1
|
||||
INTO next_version
|
||||
FROM document_versions
|
||||
WHERE document_id = p_document_id;
|
||||
RETURN next_version;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE oauth_tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE document_updates ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE document_shares ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE document_versions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Allow all operations on users" ON users FOR ALL USING (true);
|
||||
CREATE POLICY "Allow all operations on sessions" ON sessions FOR ALL USING (true);
|
||||
CREATE POLICY "Allow all operations on oauth_tokens" ON oauth_tokens FOR ALL USING (true);
|
||||
CREATE POLICY "Allow all operations on documents" ON documents FOR ALL USING (true);
|
||||
CREATE POLICY "Allow all operations on document_updates" ON document_updates FOR ALL USING (true);
|
||||
CREATE POLICY "Allow all operations on document_shares" ON document_shares FOR ALL USING (true);
|
||||
CREATE POLICY "Allow all operations on document_versions" ON document_versions FOR ALL USING (true);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stream_checkpoints (
|
||||
document_id UUID PRIMARY KEY REFERENCES documents(id) ON DELETE CASCADE,
|
||||
last_stream_id TEXT NOT NULL,
|
||||
last_seq BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_stream_checkpoints_updated_at ON stream_checkpoints(updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_update_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
stream_id TEXT NOT NULL,
|
||||
seq BIGINT NOT NULL,
|
||||
payload BYTEA NOT NULL,
|
||||
msg_type TEXT,
|
||||
server_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uniq_update_history_document_stream_id ON document_update_history(document_id, stream_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uniq_update_history_document_seq ON document_update_history(document_id, seq);
|
||||
CREATE INDEX IF NOT EXISTS idx_update_history_document_seq ON document_update_history(document_id, seq);
|
||||
69
k3s/postgres.yaml
Normal file
69
k3s/postgres.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
args: ["-c", "shared_buffers=128MB", "-c", "max_connections=50"]
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: realtime-collab-secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
volumeMounts:
|
||||
- name: postgres-data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
- name: init-sql
|
||||
mountPath: /docker-entrypoint-initdb.d
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"]
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-pvc
|
||||
- name: init-sql
|
||||
configMap:
|
||||
name: postgres-init-sql
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
61
k3s/redis.yaml
Normal file
61
k3s/redis.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: redis-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
args: ["redis-server", "--appendonly", "yes", "--maxmemory", "64mb", "--maxmemory-policy", "allkeys-lru"]
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "200m"
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["redis-cli", "ping"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: redis-data
|
||||
persistentVolumeClaim:
|
||||
claimName: redis-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
25
k3s/secret.example.yaml
Normal file
25
k3s/secret.example.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: realtime-collab-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Postgres
|
||||
POSTGRES_USER: "replace"
|
||||
POSTGRES_PASSWORD: "replace"
|
||||
POSTGRES_DB: "replace"
|
||||
# Backend
|
||||
DATABASE_URL: "postgres://user:pass@postgres:5432/dbname?sslmode=disable"
|
||||
REDIS_URL: "redis://redis:6379"
|
||||
JWT_SECRET: "replace"
|
||||
PORT: "8080"
|
||||
ENVIRONMENT: "production"
|
||||
BACKEND_URL: "https://collab.m1ngdaxie.com"
|
||||
FRONTEND_URL: "https://collab.m1ngdaxie.com"
|
||||
ALLOWED_ORIGINS: "https://collab.m1ngdaxie.com"
|
||||
GOOGLE_CLIENT_ID: "replace"
|
||||
GOOGLE_CLIENT_SECRET: "replace"
|
||||
GOOGLE_REDIRECT_URL: "https://collab.m1ngdaxie.com/api/auth/google/callback"
|
||||
GITHUB_CLIENT_ID: "replace"
|
||||
GITHUB_CLIENT_SECRET: "replace"
|
||||
GITHUB_REDIRECT_URL: "https://collab.m1ngdaxie.com/api/auth/github/callback"
|
||||
Reference in New Issue
Block a user