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>
This commit is contained in:
396
scripts/complete-setup.sh
Normal file
396
scripts/complete-setup.sh
Normal file
@@ -0,0 +1,396 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# HR Portal Complete Database Setup
|
||||
# 完整的一鍵設定腳本 - 包含所有 SQL
|
||||
#
|
||||
# 使用方法:
|
||||
# 1. 複製此腳本內容
|
||||
# 2. SSH 登入 Ubuntu Server: ssh ubuntu@10.1.0.254
|
||||
# 3. 建立檔案: nano ~/setup-hr-portal.sh
|
||||
# 4. 貼上內容並儲存 (Ctrl+X, Y, Enter)
|
||||
# 5. 執行: bash ~/setup-hr-portal.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# 顏色定義
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${CYAN}========================================"
|
||||
echo " HR Portal Database Setup"
|
||||
echo "========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# 配置
|
||||
DB_NAME="hr_portal"
|
||||
DB_USER="hr_user"
|
||||
POSTGRES_CONTAINER="postgres"
|
||||
|
||||
# 提示輸入密碼
|
||||
echo -e "${YELLOW}Please enter database password:${NC}"
|
||||
read -sp "Password for 'hr_user': " DB_PASSWORD
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
if [ -z "$DB_PASSWORD" ]; then
|
||||
echo -e "${RED}Password cannot be empty!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
# Step 1: 檢查 PostgreSQL
|
||||
# ====================================
|
||||
echo -e "${YELLOW}[1/6] Checking PostgreSQL container...${NC}"
|
||||
|
||||
if docker ps | grep -q "$POSTGRES_CONTAINER"; then
|
||||
echo -e "${GREEN}✓ PostgreSQL container is running${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ PostgreSQL container not found${NC}"
|
||||
echo "Available containers:"
|
||||
docker ps -a | grep -i postgres || echo "No postgres containers"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ====================================
|
||||
# Step 2: 建立資料庫用戶
|
||||
# ====================================
|
||||
echo -e "${YELLOW}[2/6] Creating database user...${NC}"
|
||||
|
||||
# 檢查用戶是否存在
|
||||
USER_EXISTS=$(docker exec -i $POSTGRES_CONTAINER psql -U postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" 2>&1 || echo "0")
|
||||
|
||||
if echo "$USER_EXISTS" | grep -q "1"; then
|
||||
echo -e "${YELLOW}⚠ User '$DB_USER' already exists${NC}"
|
||||
read -p "Drop and recreate? (yes/no): " RECREATE_USER
|
||||
if [ "$RECREATE_USER" = "yes" ]; then
|
||||
docker exec -i $POSTGRES_CONTAINER psql -U postgres -c "DROP USER IF EXISTS $DB_USER CASCADE;"
|
||||
docker exec -i $POSTGRES_CONTAINER psql -U postgres <<SQL
|
||||
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
|
||||
ALTER USER $DB_USER WITH SUPERUSER;
|
||||
SQL
|
||||
echo -e "${GREEN}✓ User recreated${NC}"
|
||||
fi
|
||||
else
|
||||
docker exec -i $POSTGRES_CONTAINER psql -U postgres <<SQL
|
||||
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
|
||||
ALTER USER $DB_USER WITH SUPERUSER;
|
||||
SQL
|
||||
echo -e "${GREEN}✓ User '$DB_USER' created${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ====================================
|
||||
# Step 3: 建立資料庫
|
||||
# ====================================
|
||||
echo -e "${YELLOW}[3/6] Creating database...${NC}"
|
||||
|
||||
# 檢查資料庫是否存在
|
||||
DB_EXISTS=$(docker exec -i $POSTGRES_CONTAINER psql -U postgres -lqt | cut -d \| -f 1 | grep -w "$DB_NAME" | wc -l)
|
||||
|
||||
if [ "$DB_EXISTS" -gt 0 ]; then
|
||||
echo -e "${YELLOW}⚠ Database '$DB_NAME' already exists${NC}"
|
||||
read -p "Drop and recreate? (yes/no): " RECREATE_DB
|
||||
if [ "$RECREATE_DB" = "yes" ]; then
|
||||
docker exec -i $POSTGRES_CONTAINER psql -U postgres -c "DROP DATABASE IF EXISTS $DB_NAME;"
|
||||
docker exec -i $POSTGRES_CONTAINER psql -U postgres <<SQL
|
||||
CREATE DATABASE $DB_NAME OWNER $DB_USER;
|
||||
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
|
||||
SQL
|
||||
echo -e "${GREEN}✓ Database recreated${NC}"
|
||||
fi
|
||||
else
|
||||
docker exec -i $POSTGRES_CONTAINER psql -U postgres <<SQL
|
||||
CREATE DATABASE $DB_NAME OWNER $DB_USER;
|
||||
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
|
||||
SQL
|
||||
echo -e "${GREEN}✓ Database '$DB_NAME' created${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ====================================
|
||||
# Step 4: 建立 Schema (內嵌 SQL)
|
||||
# ====================================
|
||||
echo -e "${YELLOW}[4/6] Creating database schema...${NC}"
|
||||
|
||||
docker exec -i $POSTGRES_CONTAINER psql -U $DB_USER -d $DB_NAME <<'SCHEMA_SQL'
|
||||
-- Set timezone
|
||||
SET timezone = 'Asia/Taipei';
|
||||
|
||||
-- Business Units
|
||||
CREATE TABLE IF NOT EXISTS 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', '人資、財務、行政、資訊等管理部門')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- Divisions
|
||||
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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_SQL
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Schema created successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Failed to create schema${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ====================================
|
||||
# Step 5: 驗證設定
|
||||
# ====================================
|
||||
echo -e "${YELLOW}[5/6] Verifying setup...${NC}"
|
||||
|
||||
TABLE_COUNT=$(docker exec -i $POSTGRES_CONTAINER psql -U $DB_USER -d $DB_NAME -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" -ge 9 ]; then
|
||||
echo -e "${GREEN}✓ All tables created${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Warning: Expected 9 tables, found $TABLE_COUNT${NC}"
|
||||
fi
|
||||
|
||||
# 列出資料表
|
||||
echo ""
|
||||
echo "Tables in database:"
|
||||
docker exec -i $POSTGRES_CONTAINER psql -U $DB_USER -d $DB_NAME -c "\dt" | head -20
|
||||
echo ""
|
||||
|
||||
# ====================================
|
||||
# Step 6: 輸出連接字串
|
||||
# ====================================
|
||||
echo -e "${YELLOW}[6/6] Setup complete!${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${GREEN} Database Ready!${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Connection String:${NC}"
|
||||
echo -e "${CYAN}postgresql://$DB_USER:$DB_PASSWORD@10.1.0.254:5432/$DB_NAME${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Update your backend/.env file:${NC}"
|
||||
echo "DATABASE_URL=postgresql://$DB_USER:$DB_PASSWORD@10.1.0.254:5432/$DB_NAME"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next steps:${NC}"
|
||||
echo " 1. Copy the connection string above"
|
||||
echo " 2. Update W:\DevOps-Workspace\hr-portal\backend\.env"
|
||||
echo " 3. (Optional) Insert test data using insert-test-data.sql"
|
||||
echo " 4. Start the backend service"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user