Files
hr-portal/scripts/setup-hr-postgres.sh
Porsche Chen 360533393f feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
Major Features:
-  Multi-tenant architecture (tenant isolation)
-  Employee CRUD with lifecycle management (onboarding/offboarding)
-  Department tree structure with email domain management
-  Company info management (single-record editing)
-  System functions CRUD (permission management)
-  Email account management (multi-account per employee)
-  Keycloak SSO integration (auth.lab.taipei)
-  Redis session storage (10.1.0.254:6379)
  - Solves Cookie 4KB limitation
  - Cross-system session sharing
  - Sliding expiration (8 hours)
  - Automatic token refresh

Technical Stack:
Backend:
- FastAPI + SQLAlchemy
- PostgreSQL 16 (10.1.0.20:5433)
- Keycloak Admin API integration
- Docker Mailserver integration (SSH)
- Alembic migrations

Frontend:
- Next.js 14 (App Router)
- NextAuth 4 with Keycloak Provider
- Redis session storage (ioredis)
- Tailwind CSS

Infrastructure:
- Redis 7 (10.1.0.254:6379) - Session + Cache
- Keycloak 26.1.0 (auth.lab.taipei)
- Docker Mailserver (10.1.0.254)

Architecture Highlights:
- Session管理由 Keycloak + Redis 統一控制
- 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session
- Token 自動刷新,異質服務整合
- 未來可無縫遷移到雲端

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-23 20:12:43 +08:00

297 lines
9.7 KiB
Bash

#!/bin/bash
#
# 建立 HR Portal 專用的 PostgreSQL 容器
# 遵循微服務架構原則: Database per Service
#
set -e
echo "=========================================="
echo " Setup HR Portal PostgreSQL"
echo " Microservice Architecture"
echo "=========================================="
echo ""
DB_PASSWORD="DC1qaz2wsx"
CONTAINER_NAME="hr-postgres"
PORT="5432"
# Step 1: 建立容器
echo "[1/5] Creating hr-postgres container..."
docker run -d \
--name ${CONTAINER_NAME} \
--restart unless-stopped \
-e POSTGRES_PASSWORD="${DB_PASSWORD}" \
-e POSTGRES_INITDB_ARGS="--encoding=UTF-8" \
-e TZ=Asia/Taipei \
-p 0.0.0.0:${PORT}:5432 \
-v hr-postgres-data:/var/lib/postgresql/data \
--health-cmd="pg_isready -U postgres" \
--health-interval=10s \
--health-timeout=5s \
--health-retries=5 \
postgres:16-alpine
echo " ✓ Container created"
echo ""
# Step 2: 等待啟動
echo "[2/5] Waiting for PostgreSQL to be ready..."
for i in {1..30}; do
if docker exec ${CONTAINER_NAME} pg_isready -U postgres >/dev/null 2>&1; then
echo " ✓ PostgreSQL is ready!"
break
fi
echo " Waiting... ($i/30)"
sleep 1
done
echo ""
# Step 3: 建立用戶和資料庫
echo "[3/5] Creating hr_user and hr_portal database..."
docker exec -i ${CONTAINER_NAME} psql -U postgres <<EOF
-- 建立用戶
CREATE USER hr_user WITH PASSWORD '${DB_PASSWORD}';
ALTER USER hr_user WITH SUPERUSER;
-- 建立資料庫
CREATE DATABASE hr_portal OWNER hr_user;
GRANT ALL PRIVILEGES ON DATABASE hr_portal TO hr_user;
-- 顯示結果
\l
EOF
echo " ✓ Database created"
echo ""
# Step 4: 初始化 Schema
echo "[4/5] Initializing database schema..."
docker exec -i ${CONTAINER_NAME} psql -U hr_user -d hr_portal <<'SCHEMA'
SET timezone = 'Asia/Taipei';
-- Business Units
CREATE TABLE business_units (
id SERIAL PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
name_en VARCHAR(100),
description TEXT,
manager_id INTEGER,
email_domain VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO business_units (code, name, name_en, email_domain, description) VALUES
('wind-energy', '玄鐵風能授權服務事業部', 'Wind Energy Licensing', 'ease.taipei', '風力發電技術授權與風場評估服務'),
('carbon-credit', '國際碳權申請服務事業部', 'Carbon Credit Services', 'ease.taipei', '碳權申請、碳盤查與碳交易媒合服務'),
('smart-rd', '智能研發服務事業部', 'Smart R&D Services', 'lab.taipei', 'AI/ML、IoT 與能源管理系統研發'),
('management', '管理部門', 'Management', 'porscheworld.tw', '人資、財務、行政、資訊等管理部門');
-- Divisions
CREATE TABLE divisions (
id SERIAL PRIMARY KEY,
business_unit_id INTEGER REFERENCES business_units(id) ON DELETE CASCADE,
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
name_en VARCHAR(100),
manager_id INTEGER,
email VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Employees
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
employee_id VARCHAR(20) UNIQUE NOT NULL,
keycloak_user_id UUID UNIQUE,
username VARCHAR(100) UNIQUE NOT NULL,
first_name VARCHAR(50),
last_name VARCHAR(50),
chinese_name VARCHAR(50),
email VARCHAR(100) UNIQUE NOT NULL,
phone VARCHAR(20),
mobile VARCHAR(20),
business_unit_id INTEGER REFERENCES business_units(id),
division_id INTEGER REFERENCES divisions(id),
position VARCHAR(100),
job_level VARCHAR(50),
hire_date DATE,
termination_date DATE,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_employees_username ON employees(username);
CREATE INDEX idx_employees_email ON employees(email);
CREATE INDEX idx_employees_keycloak_id ON employees(keycloak_user_id);
CREATE INDEX idx_employees_status ON employees(status);
-- Email Accounts
CREATE TABLE email_accounts (
id SERIAL PRIMARY KEY,
employee_id INTEGER REFERENCES employees(id) ON DELETE CASCADE,
email_address VARCHAR(100) UNIQUE NOT NULL,
mailbox_quota_mb INTEGER DEFAULT 5120,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Network Drives
CREATE TABLE network_drives (
id SERIAL PRIMARY KEY,
employee_id INTEGER REFERENCES employees(id) ON DELETE CASCADE,
drive_name VARCHAR(100) NOT NULL,
drive_path VARCHAR(255),
quota_gb INTEGER DEFAULT 50,
webdav_url VARCHAR(255),
smb_path VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- System Permissions
CREATE TABLE system_permissions (
id SERIAL PRIMARY KEY,
employee_id INTEGER REFERENCES employees(id) ON DELETE CASCADE,
system_name VARCHAR(100) NOT NULL,
access_level VARCHAR(50),
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
revoked_at TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
notes TEXT
);
-- Projects
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
project_code VARCHAR(50) UNIQUE NOT NULL,
project_name VARCHAR(200) NOT NULL,
description TEXT,
project_manager_id INTEGER REFERENCES employees(id),
start_date DATE,
end_date DATE,
status VARCHAR(50) DEFAULT 'planning',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Project Members
CREATE TABLE project_members (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
employee_id INTEGER REFERENCES employees(id) ON DELETE CASCADE,
role VARCHAR(100),
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
left_at TIMESTAMP,
UNIQUE(project_id, employee_id)
);
-- Audit Logs
CREATE TABLE audit_logs (
id SERIAL PRIMARY KEY,
employee_id INTEGER REFERENCES employees(id),
action VARCHAR(100) NOT NULL,
table_name VARCHAR(100),
record_id INTEGER,
old_values JSONB,
new_values JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_audit_logs_employee ON audit_logs(employee_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
-- Triggers
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_business_units_updated_at BEFORE UPDATE ON business_units FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_divisions_updated_at BEFORE UPDATE ON divisions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_employees_updated_at BEFORE UPDATE ON employees FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_email_accounts_updated_at BEFORE UPDATE ON email_accounts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_network_drives_updated_at BEFORE UPDATE ON network_drives FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_projects_updated_at BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Views
CREATE OR REPLACE VIEW v_employees_full AS
SELECT e.id, e.employee_id, e.username, e.first_name, e.last_name, e.chinese_name, e.email, e.phone, e.mobile,
bu.name as business_unit, bu.code as business_unit_code, d.name as division, d.code as division_code,
e.position, e.job_level, e.hire_date, e.status, e.created_at, e.updated_at
FROM employees e
LEFT JOIN business_units bu ON e.business_unit_id = bu.id
LEFT JOIN divisions d ON e.division_id = d.id;
CREATE OR REPLACE VIEW v_division_headcount AS
SELECT bu.name as business_unit, d.name as division,
COUNT(e.id) as employee_count,
COUNT(CASE WHEN e.status = 'active' THEN 1 END) as active_count
FROM divisions d
LEFT JOIN business_units bu ON d.business_unit_id = bu.id
LEFT JOIN employees e ON d.id = e.division_id
GROUP BY bu.name, d.name;
CREATE OR REPLACE VIEW v_project_stats AS
SELECT p.project_code, p.project_name, p.status, e.chinese_name as project_manager,
COUNT(pm.id) as member_count, p.start_date, p.end_date
FROM projects p
LEFT JOIN employees e ON p.project_manager_id = e.id
LEFT JOIN project_members pm ON p.id = pm.project_id
GROUP BY p.id, p.project_code, p.project_name, p.status, e.chinese_name, p.start_date, p.end_date;
SCHEMA
echo " ✓ Schema initialized"
echo ""
# Step 5: 驗證
echo "[5/5] Verifying setup..."
TABLE_COUNT=$(docker exec -i ${CONTAINER_NAME} psql -U hr_user -d hr_portal -tAc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';")
echo " Tables created: ${TABLE_COUNT} / 9"
if [ "${TABLE_COUNT}" -eq 9 ]; then
echo " ✓ All tables verified"
else
echo " ⚠ Expected 9 tables, found ${TABLE_COUNT}"
fi
docker exec -i ${CONTAINER_NAME} psql -U hr_user -d hr_portal -c "\dt" | head -15
echo ""
echo "=========================================="
echo " Setup Complete!"
echo "=========================================="
echo ""
echo "Microservice Architecture:"
echo " ├── keycloak-db (Keycloak)"
echo " ├── gitea-db (Gitea)"
echo " └── hr-postgres (HR Portal) ← NEW"
echo ""
echo "Connection String:"
echo " postgresql://hr_user:${DB_PASSWORD}@10.1.0.254:${PORT}/hr_portal"
echo ""
echo "Container Status:"
docker ps | grep -E "keycloak-db|gitea-db|hr-postgres"
echo ""