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:
2026-02-23 20:12:43 +08:00
commit 360533393f
386 changed files with 70353 additions and 0 deletions

29
scripts/COPY-AND-RUN.txt Normal file
View File

@@ -0,0 +1,29 @@
========================================
請在 Ubuntu Server 上執行以下命令
========================================
1. 打開終端機/SSH 連接到 Ubuntu Server
2. 複製以下 3 個命令 (全選後複製)
3. 貼到終端機並按 Enter
----------------------------------------
命令開始 (請複製以下 3 行)
----------------------------------------
docker stop postgres && docker rm postgres && docker run -d --name postgres --restart unless-stopped -e POSTGRES_PASSWORD="DC1qaz2wsx" -e TZ=Asia/Taipei -p 0.0.0.0:5432:5432 -v postgres-data:/var/lib/postgresql/data postgres:16
sleep 8 && docker exec postgres psql -U hr_user -d hr_portal -c "SELECT 'Database OK: ' || COUNT(*)::text || ' tables found' FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';"
docker ps | grep postgres && docker port postgres
----------------------------------------
命令結束
----------------------------------------
完成後,你應該會看到:
- Database OK: 9 tables found
- postgres 容器運行中
- 0.0.0.0:5432->5432/tcp
然後回到這裡告訴我「完成」,我會測試連接!
========================================

View File

@@ -0,0 +1,252 @@
========================================
建立 HR Portal 獨立 PostgreSQL 容器
微服務架構 - 第三個資料庫容器
========================================
請在 Ubuntu Server 上執行以下命令:
1. 登入: ssh ubuntu@10.1.0.254
2. 複製下面所有命令並執行
========================================
命令開始 (複製以下全部)
========================================
# 建立 hr-postgres 容器
docker run -d \
--name hr-postgres \
--restart unless-stopped \
-e POSTGRES_PASSWORD="DC1qaz2wsx" \
-e POSTGRES_INITDB_ARGS="--encoding=UTF-8" \
-e TZ=Asia/Taipei \
-p 0.0.0.0:5432: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 "Waiting for PostgreSQL to start..."
sleep 10
# 建立 hr_user 和 hr_portal 資料庫
docker exec -i hr-postgres psql -U postgres <<EOF
CREATE USER hr_user WITH PASSWORD 'DC1qaz2wsx';
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
# 初始化 Schema
docker exec -i hr-postgres psql -U hr_user -d hr_portal <<'SCHEMA'
SET timezone = 'Asia/Taipei';
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', '人資、財務、行政、資訊等管理部門');
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
);
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);
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
);
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
);
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
);
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
);
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)
);
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);
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();
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 ""
echo "=========================================="
echo "Verifying setup..."
echo "=========================================="
docker exec hr-postgres psql -U hr_user -d hr_portal -c "SELECT 'Tables: ' || COUNT(*)::text FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';"
docker exec hr-postgres psql -U hr_user -d hr_portal -c "\dt"
echo ""
echo "=========================================="
echo "Microservice Architecture Complete!"
echo "=========================================="
echo ""
docker ps | grep -E "keycloak-db|gitea-db|hr-postgres"
echo ""
echo "Connection String:"
echo "postgresql://hr_user:DC1qaz2wsx@10.1.0.254:5432/hr_portal"
echo ""
========================================
命令結束
========================================
完成後會看到:
✓ Tables: 9
✓ 三個獨立的 PostgreSQL 容器運行中
✓ Connection String
然後回來告訴我「完成」! 🚀
========================================

View File

@@ -0,0 +1,63 @@
# ============================================
# 修復 PostgreSQL 端口綁定
# ============================================
#
# 複製以下命令到 Ubuntu Server 執行
# SSH 登入: ssh ubuntu@10.1.0.254
#
# ============================================
# 步驟 1: 檢查現有配置
echo "Current PostgreSQL configuration:"
docker ps | grep postgres
docker port postgres
# 步驟 2: 獲取 postgres 密碼 (如果不知道,設定一個新的)
# 例如: POSTGRES_PASSWORD="yourpassword"
POSTGRES_PASSWORD="DC1qaz2wsx"
# 步驟 3: 檢查資料卷
VOLUME_NAME=$(docker inspect postgres --format '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{.Name}}{{end}}{{end}}' 2>/dev/null || echo "postgres-data")
echo "Using volume: $VOLUME_NAME"
# 步驟 4: 停止並移除舊容器 (資料不會遺失,在 volume 中)
docker stop postgres
docker rm postgres
# 步驟 5: 重新啟動,綁定到所有網路介面 (0.0.0.0:5432)
docker run -d \
--name postgres \
--restart unless-stopped \
-e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
-e POSTGRES_INITDB_ARGS="--encoding=UTF-8 --locale=en_US.UTF-8" \
-e TZ=Asia/Taipei \
-p 0.0.0.0:5432:5432 \
-v ${VOLUME_NAME}:/var/lib/postgresql/data \
postgres:16
# 步驟 6: 等待啟動
echo "Waiting for PostgreSQL to start..."
sleep 5
# 步驟 7: 驗證
echo ""
echo "Verification:"
docker ps | grep postgres
docker port postgres
docker exec postgres pg_isready -U postgres
# 步驟 8: 檢查資料庫
echo ""
echo "Databases:"
docker exec postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -v "^$"
echo ""
echo "Testing hr_portal database:"
docker exec postgres psql -U hr_user -d hr_portal -c "\dt"
echo ""
echo "=========================================="
echo " PostgreSQL is now accessible!"
echo "=========================================="
echo "Connection: postgresql://hr_user:DC1qaz2wsx@10.1.0.254:5432/hr_portal"
echo ""

210
scripts/EXECUTE-THIS.txt Normal file
View File

@@ -0,0 +1,210 @@
# ============================================
# 複製以下所有內容到 Ubuntu Server 執行
# ============================================
#
# 步驟:
# 1. SSH 登入: ssh ubuntu@10.1.0.254
# 2. 執行: bash
# 3. 複製貼上以下全部內容並按 Enter
# 4. 等待完成
#
# ============================================
docker exec -i postgres psql -U postgres <<'EOF'
DROP USER IF EXISTS hr_user CASCADE;
CREATE USER hr_user WITH PASSWORD 'DC1qaz2wsx';
ALTER USER hr_user WITH SUPERUSER;
DROP DATABASE IF EXISTS hr_portal;
CREATE DATABASE hr_portal OWNER hr_user;
GRANT ALL PRIVILEGES ON DATABASE hr_portal TO hr_user;
EOF
docker exec -i postgres psql -U hr_user -d hr_portal <<'EOF'
SET timezone = 'Asia/Taipei';
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', '人資、財務、行政、資訊等管理部門');
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
);
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);
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
);
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
);
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
);
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
);
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)
);
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);
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();
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;
EOF
echo ""
echo "=========================================="
echo " ✓ Database Setup Complete!"
echo "=========================================="
echo ""
echo "Verification:"
docker exec -i postgres psql -U hr_user -d hr_portal -c "\dt"
echo ""
echo "Connection String:"
echo "postgresql://hr_user:DC1qaz2wsx@10.1.0.254:5432/hr_portal"
echo ""

View File

@@ -0,0 +1,70 @@
========================================
插入測試資料到 hr_portal 資料庫
========================================
在 Ubuntu Server 上複製並執行以下命令:
========================================
命令開始 (複製以下全部)
========================================
docker exec -i hr-postgres psql -U hr_user -d hr_portal <<'SQL'
-- 插入測試員工
INSERT INTO employees (employee_id, username, first_name, last_name, chinese_name, email, mobile, business_unit_id, position, job_level, hire_date, status) VALUES
('E0001', 'porsche.chen', 'Porsche', 'Chen', '陳博駿', 'porsche.chen@porscheworld.tw', '0912-345-678', 4, 'CTO / 技術長', 'C-Level', '2020-01-01', 'active'),
('E1001', 'alice.wang', 'Alice', 'Wang', '王小華', 'alice.wang@lab.taipei', '0922-111-222', 3, 'Technical Director', 'Director', '2020-06-01', 'active'),
('E1002', 'bob.chen', 'Bob', 'Chen', '陳大明', 'bob.chen@lab.taipei', '0933-222-333', 3, 'Senior Software Engineer', 'Senior', '2021-03-15', 'active'),
('E2001', 'charlie.lin', 'Charlie', 'Lin', '林小風', 'charlie.lin@ease.taipei', '0944-333-444', 1, 'Wind Energy Consultant', 'Senior', '2021-08-01', 'active'),
('E3001', 'diana.wu', 'Diana', 'Wu', '吳小綠', 'diana.wu@ease.taipei', '0955-444-555', 2, 'Carbon Credit Specialist', 'Staff', '2022-01-10', 'active');
-- 插入郵件帳號
INSERT INTO email_accounts (employee_id, email_address, mailbox_quota_mb, is_active)
SELECT e.id, e.email, CASE e.job_level WHEN 'C-Level' THEN 20480 WHEN 'Director' THEN 10240 ELSE 5120 END, true FROM employees e;
-- 插入網路硬碟
INSERT INTO network_drives (employee_id, drive_name, drive_path, quota_gb, webdav_url, smb_path, is_active)
SELECT e.id, e.username || '_personal', '/nas/users/' || e.username, CASE e.job_level WHEN 'C-Level' THEN 500 WHEN 'Director' THEN 200 WHEN 'Senior' THEN 80 ELSE 50 END, 'https://nas.porscheworld.tw/webdav/' || e.username, '//10.1.0.30/homes/' || e.username, true FROM employees e;
-- 插入系統權限
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
SELECT e.id, 'HR Portal', CASE WHEN e.job_level IN ('C-Level', 'Director') THEN 'admin' ELSE 'user' END, true, 'Initial access' FROM employees e;
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
SELECT e.id, 'Gitea', 'user', true, 'Git access' FROM employees e WHERE e.business_unit_id = 3;
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
VALUES ((SELECT id FROM employees WHERE username = 'porsche.chen'), 'Portainer', 'admin', true, 'Docker management');
-- 插入專案
INSERT INTO projects (project_code, project_name, description, project_manager_id, start_date, status) VALUES
('WIND-2024-001', '離岸風電技術評估', '台中港離岸風電場技術可行性評估', (SELECT id FROM employees WHERE username = 'charlie.lin'), '2024-01-15', 'in_progress'),
('CARBON-2024-001', '企業碳盤查輔導', '協助製造業進行 ISO 14064-1 碳盤查', (SELECT id FROM employees WHERE username = 'diana.wu'), '2024-02-01', 'in_progress'),
('AI-2024-001', 'HR Portal 系統開發', '開發整合 Keycloak 的人資管理系統', (SELECT id FROM employees WHERE username = 'alice.wang'), '2024-01-01', 'in_progress');
-- 插入專案成員
INSERT INTO project_members (project_id, employee_id, role) VALUES
((SELECT id FROM projects WHERE project_code = 'WIND-2024-001'), (SELECT id FROM employees WHERE username = 'charlie.lin'), 'Project Manager'),
((SELECT id FROM projects WHERE project_code = 'CARBON-2024-001'), (SELECT id FROM employees WHERE username = 'diana.wu'), 'Project Manager'),
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'alice.wang'), 'Project Manager'),
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'bob.chen'), 'Lead Developer'),
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'porsche.chen'), 'Technical Advisor');
-- 驗證
SELECT '=== 完成! ===' as status, (SELECT COUNT(*) FROM employees) as employees, (SELECT COUNT(*) FROM email_accounts) as emails, (SELECT COUNT(*) FROM network_drives) as drives, (SELECT COUNT(*) FROM system_permissions WHERE is_active = true) as permissions, (SELECT COUNT(*) FROM projects) as projects;
SELECT employee_id, username, chinese_name, position, job_level FROM employees ORDER BY employee_id;
SQL
========================================
命令結束
========================================
執行完成後會看到:
- 5 個員工
- 5 個郵件帳號
- 5 個網路硬碟
- 8 個系統權限
- 3 個專案
然後回來告訴我「完成」!
========================================

View File

@@ -0,0 +1,81 @@
========================================
HR Portal 一鍵完成腳本
在 Ubuntu Server (10.1.0.254) 執行
========================================
用 PuTTY 登入: porsche@10.1.0.254
複製以下所有命令並執行:
========================================
開始執行
========================================
echo "=========================================="
echo " HR Portal 資料庫初始化"
echo "=========================================="
echo ""
# 插入測試資料
docker exec -i hr-postgres psql -U hr_user -d hr_portal <<'SQLEND'
INSERT INTO employees (employee_id, username, first_name, last_name, chinese_name, email, mobile, business_unit_id, position, job_level, hire_date, status) VALUES
('E0001', 'porsche.chen', 'Porsche', 'Chen', '陳博駿', 'porsche.chen@porscheworld.tw', '0912-345-678', 4, 'CTO / 技術長', 'C-Level', '2020-01-01', 'active'),
('E1001', 'alice.wang', 'Alice', 'Wang', '王小華', 'alice.wang@lab.taipei', '0922-111-222', 3, 'Technical Director', 'Director', '2020-06-01', 'active'),
('E1002', 'bob.chen', 'Bob', 'Chen', '陳大明', 'bob.chen@lab.taipei', '0933-222-333', 3, 'Senior Software Engineer', 'Senior', '2021-03-15', 'active'),
('E2001', 'charlie.lin', 'Charlie', 'Lin', '林小風', 'charlie.lin@ease.taipei', '0944-333-444', 1, 'Wind Energy Consultant', 'Senior', '2021-08-01', 'active'),
('E3001', 'diana.wu', 'Diana', 'Wu', '吳小綠', 'diana.wu@ease.taipei', '0955-444-555', 2, 'Carbon Credit Specialist', 'Staff', '2022-01-10', 'active');
INSERT INTO email_accounts (employee_id, email_address, mailbox_quota_mb, is_active)
SELECT e.id, e.email, CASE e.job_level WHEN 'C-Level' THEN 20480 WHEN 'Director' THEN 10240 ELSE 5120 END, true FROM employees e;
INSERT INTO network_drives (employee_id, drive_name, drive_path, quota_gb, webdav_url, smb_path, is_active)
SELECT e.id, e.username || '_personal', '/nas/users/' || e.username, CASE e.job_level WHEN 'C-Level' THEN 500 WHEN 'Director' THEN 200 WHEN 'Senior' THEN 80 ELSE 50 END, 'https://nas.porscheworld.tw/webdav/' || e.username, '//10.1.0.30/homes/' || e.username, true FROM employees e;
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
SELECT e.id, 'HR Portal', CASE WHEN e.job_level IN ('C-Level', 'Director') THEN 'admin' ELSE 'user' END, true, 'Initial access' FROM employees e;
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
SELECT e.id, 'Gitea', 'user', true, 'Git access' FROM employees e WHERE e.business_unit_id = 3;
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
VALUES ((SELECT id FROM employees WHERE username = 'porsche.chen'), 'Portainer', 'admin', true, 'Docker management');
INSERT INTO projects (project_code, project_name, description, project_manager_id, start_date, status) VALUES
('WIND-2024-001', '離岸風電技術評估', '台中港離岸風電場技術可行性評估', (SELECT id FROM employees WHERE username = 'charlie.lin'), '2024-01-15', 'in_progress'),
('CARBON-2024-001', '企業碳盤查輔導', '協助製造業進行 ISO 14064-1 碳盤查', (SELECT id FROM employees WHERE username = 'diana.wu'), '2024-02-01', 'in_progress'),
('AI-2024-001', 'HR Portal 系統開發', '開發整合 Keycloak 的人資管理系統', (SELECT id FROM employees WHERE username = 'alice.wang'), '2024-01-01', 'in_progress');
INSERT INTO project_members (project_id, employee_id, role) VALUES
((SELECT id FROM projects WHERE project_code = 'WIND-2024-001'), (SELECT id FROM employees WHERE username = 'charlie.lin'), 'Project Manager'),
((SELECT id FROM projects WHERE project_code = 'CARBON-2024-001'), (SELECT id FROM employees WHERE username = 'diana.wu'), 'Project Manager'),
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'alice.wang'), 'Project Manager'),
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'bob.chen'), 'Lead Developer'),
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'porsche.chen'), 'Technical Advisor');
SQLEND
echo ""
echo "=========================================="
echo " 驗證結果"
echo "=========================================="
docker exec hr-postgres psql -U hr_user -d hr_portal -c "SELECT COUNT(*) as employees FROM employees;"
docker exec hr-postgres psql -U hr_user -d hr_portal -c "SELECT COUNT(*) as email_accounts FROM email_accounts;"
docker exec hr-postgres psql -U hr_user -d hr_portal -c "SELECT COUNT(*) as network_drives FROM network_drives;"
docker exec hr-postgres psql -U hr_user -d hr_portal -c "SELECT COUNT(*) as projects FROM projects;"
echo ""
echo "=========================================="
echo " 完成!"
echo "=========================================="
echo ""
echo "測試資料已插入:"
echo " - 5 個員工"
echo " - 5 個郵件帳號"
echo " - 5 個網路硬碟"
echo " - 3 個專案"
echo ""
echo "請回到 Windows 告訴 Claude '完成'"
echo ""
========================================
執行結束
========================================

11
scripts/QUICK-INSERT.txt Normal file
View File

@@ -0,0 +1,11 @@
========================================
快速插入測試資料 (一行命令)
========================================
複製以下完整命令,在 Ubuntu Server 執行:
docker exec -i hr-postgres psql -U hr_user -d hr_portal <<'ENDOFSQL'
INSERT INTO employees (employee_id, username, first_name, last_name, chinese_name, email, mobile, business_unit_id, position, job_level, hire_date, status) VALUES ('E0001', 'porsche.chen', 'Porsche', 'Chen', '陳博駿', 'porsche.chen@porscheworld.tw', '0912-345-678', 4, 'CTO / 技術長', 'C-Level', '2020-01-01', 'active'), ('E1001', 'alice.wang', 'Alice', 'Wang', '王小華', 'alice.wang@lab.taipei', '0922-111-222', 3, 'Technical Director', 'Director', '2020-06-01', 'active'), ('E1002', 'bob.chen', 'Bob', 'Chen', '陳大明', 'bob.chen@lab.taipei', '0933-222-333', 3, 'Senior Software Engineer', 'Senior', '2021-03-15', 'active'), ('E2001', 'charlie.lin', 'Charlie', 'Lin', '林小風', 'charlie.lin@ease.taipei', '0944-333-444', 1, 'Wind Energy Consultant', 'Senior', '2021-08-01', 'active'), ('E3001', 'diana.wu', 'Diana', 'Wu', '吳小綠', 'diana.wu@ease.taipei', '0955-444-555', 2, 'Carbon Credit Specialist', 'Staff', '2022-01-10', 'active'); INSERT INTO email_accounts (employee_id, email_address, mailbox_quota_mb, is_active) SELECT e.id, e.email, CASE e.job_level WHEN 'C-Level' THEN 20480 WHEN 'Director' THEN 10240 ELSE 5120 END, true FROM employees e; INSERT INTO network_drives (employee_id, drive_name, drive_path, quota_gb, webdav_url, smb_path, is_active) SELECT e.id, e.username || '_personal', '/nas/users/' || e.username, CASE e.job_level WHEN 'C-Level' THEN 500 WHEN 'Director' THEN 200 WHEN 'Senior' THEN 80 ELSE 50 END, 'https://nas.porscheworld.tw/webdav/' || e.username, '//10.1.0.30/homes/' || e.username, true FROM employees e; INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes) SELECT e.id, 'HR Portal', CASE WHEN e.job_level IN ('C-Level', 'Director') THEN 'admin' ELSE 'user' END, true, 'Initial access' FROM employees e; INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes) SELECT e.id, 'Gitea', 'user', true, 'Git access' FROM employees e WHERE e.business_unit_id = 3; INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes) VALUES ((SELECT id FROM employees WHERE username = 'porsche.chen'), 'Portainer', 'admin', true, 'Docker management'); INSERT INTO projects (project_code, project_name, description, project_manager_id, start_date, status) VALUES ('WIND-2024-001', '離岸風電技術評估', '台中港離岸風電場技術可行性評估', (SELECT id FROM employees WHERE username = 'charlie.lin'), '2024-01-15', 'in_progress'), ('CARBON-2024-001', '企業碳盤查輔導', '協助製造業進行 ISO 14064-1 碳盤查', (SELECT id FROM employees WHERE username = 'diana.wu'), '2024-02-01', 'in_progress'), ('AI-2024-001', 'HR Portal 系統開發', '開發整合 Keycloak 的人資管理系統', (SELECT id FROM employees WHERE username = 'alice.wang'), '2024-01-01', 'in_progress'); INSERT INTO project_members (project_id, employee_id, role) VALUES ((SELECT id FROM projects WHERE project_code = 'WIND-2024-001'), (SELECT id FROM employees WHERE username = 'charlie.lin'), 'Project Manager'), ((SELECT id FROM projects WHERE project_code = 'CARBON-2024-001'), (SELECT id FROM employees WHERE username = 'diana.wu'), 'Project Manager'), ((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'alice.wang'), 'Project Manager'), ((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'bob.chen'), 'Lead Developer'), ((SELECT id FROM projects WHERE project_code = 'AI-2024-001'), (SELECT id FROM employees WHERE username = 'porsche.chen'), 'Technical Advisor'); SELECT '插入完成!' as status; SELECT COUNT(*) || ' 個員工' FROM employees; SELECT COUNT(*) || ' 個郵件帳號' FROM email_accounts; SELECT COUNT(*) || ' 個網路硬碟' FROM network_drives; SELECT COUNT(*) || ' 個系統權限' FROM system_permissions WHERE is_active = true; SELECT COUNT(*) || ' 個專案' FROM projects; SELECT '--- 員工列表 ---' as info; SELECT employee_id, username, chinese_name, position FROM employees ORDER BY employee_id;
ENDOFSQL
========================================

View File

@@ -0,0 +1,212 @@
# HR Portal 資料庫設定指南
## 🎯 目標
在 Ubuntu Server (10.1.0.254) 上建立 HR Portal 資料庫
## 📋 方案選擇
### ⭐ 方案 A: 一鍵執行 (推薦)
**步驟 1: 登入 Ubuntu Server**
```bash
ssh ubuntu@10.1.0.254
```
**步驟 2: 下載並執行安裝腳本**
```bash
# 建立工作目錄
mkdir -p ~/hr-portal-setup && cd ~/hr-portal-setup
# 執行設定腳本 (會自動從 Windows 複製檔案)
bash <(cat <<'SETUP_SCRIPT'
#!/bin/bash
set -e
echo "========================================"
echo " HR Portal Database Setup"
echo "========================================"
echo ""
# 配置
DB_NAME="hr_portal"
DB_USER="hr_user"
POSTGRES_CONTAINER="postgres"
# 提示輸入密碼
read -sp "Enter password for 'hr_user': " DB_PASSWORD
echo ""
# 檢查 PostgreSQL
echo "[1/5] Checking PostgreSQL container..."
docker ps | grep postgres
# 建立用戶
echo ""
echo "[2/5] Creating database user..."
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 ""
echo "[3/5] Creating database..."
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
# 下載 Schema
echo ""
echo "[4/5] Downloading schema..."
wget -q -O init-db.sql "https://raw.githubusercontent.com/.../init-db.sql" 2>/dev/null || \
curl -s -o init-db.sql "https://..." 2>/dev/null || \
echo "Please upload init-db.sql manually"
# 初始化 Schema
echo ""
echo "[5/5] Initializing schema..."
if [ -f init-db.sql ]; then
docker exec -i $POSTGRES_CONTAINER psql -U $DB_USER -d $DB_NAME < init-db.sql
echo "✓ Schema initialized"
else
echo "⚠ Please run: docker exec -i postgres psql -U hr_user -d hr_portal < init-db.sql"
fi
echo ""
echo "========================================"
echo " Setup Complete!"
echo "========================================"
echo ""
echo "Connection String:"
echo "postgresql://$DB_USER:$DB_PASSWORD@10.1.0.254:5432/$DB_NAME"
echo ""
SETUP_SCRIPT
)
```
---
### 📝 方案 B: 手動逐步執行
登入 Ubuntu Server 後執行:
#### 1. 檢查 PostgreSQL 容器
```bash
docker ps -a | grep postgres
```
#### 2. 建立資料庫用戶
```bash
docker exec -i postgres psql -U postgres <<EOF
CREATE USER hr_user WITH PASSWORD 'your_password_here';
ALTER USER hr_user WITH SUPERUSER;
EOF
```
#### 3. 建立資料庫
```bash
docker exec -i postgres psql -U postgres <<EOF
CREATE DATABASE hr_portal OWNER hr_user;
GRANT ALL PRIVILEGES ON DATABASE hr_portal TO hr_user;
EOF
```
#### 4. 上傳 SQL 檔案到 Ubuntu Server
**Windows PowerShell** 執行:
```powershell
scp W:\DevOps-Workspace\hr-portal\scripts\init-db.sql ubuntu@10.1.0.254:/tmp/
```
#### 5. 初始化 Schema
回到 **Ubuntu Server** 執行:
```bash
docker exec -i postgres psql -U hr_user -d hr_portal < /tmp/init-db.sql
```
#### 6. 驗證設定
```bash
docker exec -i postgres psql -U hr_user -d hr_portal -c "\dt"
```
應該看到 9 個資料表:
- audit_logs
- business_units
- divisions
- email_accounts
- employees
- network_drives
- project_members
- projects
- system_permissions
---
## ✅ 完成後
### 更新後端配置
編輯 `W:\DevOps-Workspace\hr-portal\backend\.env`:
```env
DATABASE_URL=postgresql://hr_user:your_password@10.1.0.254:5432/hr_portal
```
### 插入測試資料 (可選)
```bash
# 上傳測試資料
scp W:\DevOps-Workspace\hr-portal\scripts\insert-test-data.sql ubuntu@10.1.0.254:/tmp/
# 執行插入
ssh ubuntu@10.1.0.254 "docker exec -i postgres psql -U hr_user -d hr_portal < /tmp/insert-test-data.sql"
```
### 驗證資料
```bash
ssh ubuntu@10.1.0.254 "docker exec -i postgres psql -U hr_user -d hr_portal -c 'SELECT * FROM employees;'"
```
---
## 🆘 常見問題
### Q: 資料庫已存在怎麼辦?
```bash
# 刪除舊資料庫
docker exec postgres psql -U postgres -c "DROP DATABASE hr_portal;"
# 重新建立
docker exec postgres psql -U postgres -c "CREATE DATABASE hr_portal OWNER hr_user;"
```
### Q: 用戶已存在?
```bash
# 刪除用戶
docker exec postgres psql -U postgres -c "DROP USER hr_user;"
# 重新建立
docker exec postgres psql -U postgres -c "CREATE USER hr_user WITH PASSWORD 'new_password';"
```
### Q: 無法連接 PostgreSQL?
```bash
# 檢查容器狀態
docker ps | grep postgres
# 檢查端口
sudo netstat -tlnp | grep 5432
# 重啟容器
docker restart postgres
```
---
## 📞 需要協助?
如遇到問題,請提供以下資訊:
1. 執行的命令
2. 錯誤訊息
3. Docker 容器狀態 (`docker ps`)

View File

@@ -0,0 +1,43 @@
========================================
SSH 免密碼登入設定 (超簡單版)
========================================
只需要 3 步驟,1 分鐘完成!
========================================
步驟 1: 在 Windows PowerShell 執行
========================================
# 檢查是否已有 SSH 金鑰
if (Test-Path "$env:USERPROFILE\.ssh\id_rsa.pub") {
Write-Host "SSH 金鑰已存在!" -ForegroundColor Green
Get-Content "$env:USERPROFILE\.ssh\id_rsa.pub"
} else {
Write-Host "正在生成 SSH 金鑰..." -ForegroundColor Yellow
ssh-keygen -t rsa -b 4096 -f "$env:USERPROFILE\.ssh\id_rsa" -N ""
Write-Host "金鑰生成完成!" -ForegroundColor Green
Get-Content "$env:USERPROFILE\.ssh\id_rsa.pub"
}
========================================
步驟 2: 上傳公鑰到 Ubuntu Server
========================================
# 執行這一行命令 (需要輸入密碼最後一次)
type $env:USERPROFILE\.ssh\id_rsa.pub | ssh ubuntu@10.1.0.254 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && echo '========== SSH 金鑰設定完成! =========='"
========================================
步驟 3: 測試免密碼登入
========================================
# 測試 (應該不用輸入密碼)
ssh ubuntu@10.1.0.254 "echo '免密碼登入成功! 🎉'"
========================================
完成!
========================================
之後所有操作都可以全自動執行了!
回來告訴我「設定完成」,我就能自動幫你執行所有任務! 🚀
========================================

251
scripts/auto-setup.sh Normal file
View File

@@ -0,0 +1,251 @@
#!/bin/bash
# Auto setup with password
set -e
DB_NAME="hr_portal"
DB_USER="hr_user"
DB_PASSWORD="DC1qaz2wsx"
POSTGRES_CONTAINER="postgres"
echo "========================================"
echo " HR Portal Database Auto Setup"
echo "========================================"
echo ""
# Check PostgreSQL
echo "[1/5] Checking PostgreSQL..."
docker ps | grep postgres
# Create user
echo ""
echo "[2/5] Creating user..."
docker exec -i $POSTGRES_CONTAINER psql -U postgres <<SQL
DROP USER IF EXISTS $DB_USER CASCADE;
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
ALTER USER $DB_USER WITH SUPERUSER;
SQL
# Create database
echo ""
echo "[3/5] Creating database..."
docker exec -i $POSTGRES_CONTAINER psql -U postgres <<SQL
DROP DATABASE IF EXISTS $DB_NAME;
CREATE DATABASE $DB_NAME OWNER $DB_USER;
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
SQL
# Create schema
echo ""
echo "[4/5] Creating schema..."
docker exec -i $POSTGRES_CONTAINER psql -U $DB_USER -d $DB_NAME <<'SCHEMA'
SET timezone = 'Asia/Taipei';
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;
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
);
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);
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
);
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
);
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
);
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
);
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)
);
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);
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();
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
# Verify
echo ""
echo "[5/5] Verifying..."
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"
docker exec -i $POSTGRES_CONTAINER psql -U $DB_USER -d $DB_NAME -c "\dt"
echo ""
echo "========================================"
echo " Setup Complete!"
echo "========================================"
echo ""
echo "Connection String:"
echo "postgresql://$DB_USER:$DB_PASSWORD@10.1.0.254:5432/$DB_NAME"
echo ""

102
scripts/check-postgres.ps1 Normal file
View File

@@ -0,0 +1,102 @@
# PostgreSQL Connection Check Script
# This script checks if PostgreSQL is accessible and ready for HR Portal setup
$ErrorActionPreference = "Continue"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " PostgreSQL Connection Check" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Configuration
$POSTGRES_HOST = "10.1.0.254"
$POSTGRES_PORT = 5432
# Step 1: Test Network Connectivity
Write-Host "[1/4] Testing network connectivity..." -ForegroundColor Yellow
try {
$tcpResult = Test-NetConnection -ComputerName $POSTGRES_HOST -Port $POSTGRES_PORT -WarningAction SilentlyContinue
if ($tcpResult.TcpTestSucceeded) {
Write-Host " [OK] Port $POSTGRES_PORT is accessible on $POSTGRES_HOST" -ForegroundColor Green
} else {
Write-Host " [FAIL] Cannot reach port $POSTGRES_PORT on $POSTGRES_HOST" -ForegroundColor Red
Write-Host " Please check if PostgreSQL container is running" -ForegroundColor Yellow
exit 1
}
} catch {
Write-Host " [ERROR] Network test failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host ""
# Step 2: Check Docker Containers
Write-Host "[2/4] Checking Docker containers on remote host..." -ForegroundColor Yellow
try {
$containers = ssh ubuntu@$POSTGRES_HOST "docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | grep postgres"
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] PostgreSQL containers found:" -ForegroundColor Green
Write-Host " $containers" -ForegroundColor White
} else {
Write-Host " [WARNING] No PostgreSQL containers found or SSH failed" -ForegroundColor Yellow
Write-Host " This is not critical if you can connect to database" -ForegroundColor Gray
}
} catch {
Write-Host " [WARNING] Could not check containers: $($_.Exception.Message)" -ForegroundColor Yellow
}
Write-Host ""
# Step 3: Test Database Connection
Write-Host "[3/4] Testing database connection..." -ForegroundColor Yellow
Write-Host " Please enter postgres password when prompted" -ForegroundColor Gray
try {
# Test connection to postgres default database
$testQuery = "SELECT version();"
$result = ssh ubuntu@$POSTGRES_HOST "docker exec -i postgres psql -U postgres -c `"$testQuery`"" 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] Successfully connected to PostgreSQL" -ForegroundColor Green
Write-Host " Version info:" -ForegroundColor Gray
Write-Host " $result" -ForegroundColor White
} else {
Write-Host " [FAIL] Could not connect to PostgreSQL" -ForegroundColor Red
Write-Host " Error: $result" -ForegroundColor Red
exit 1
}
} catch {
Write-Host " [ERROR] Connection test failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host ""
# Step 4: Check if hr_portal database exists
Write-Host "[4/4] Checking for existing hr_portal database..." -ForegroundColor Yellow
try {
$dbCheck = ssh ubuntu@$POSTGRES_HOST "docker exec -i postgres psql -U postgres -lqt | cut -d '|' -f 1 | grep -w hr_portal" 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " [INFO] hr_portal database already exists" -ForegroundColor Yellow
Write-Host " You may want to drop it before running setup-db-simple.ps1" -ForegroundColor Gray
Write-Host " Command: docker exec postgres psql -U postgres -c 'DROP DATABASE hr_portal;'" -ForegroundColor Gray
} else {
Write-Host " [OK] hr_portal database does not exist (ready for setup)" -ForegroundColor Green
}
} catch {
Write-Host " [INFO] Could not check database: $($_.Exception.Message)" -ForegroundColor Gray
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Check completed!" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host " 1. Run setup-db-simple.ps1 to create hr_portal database" -ForegroundColor White
Write-Host " 2. Execute test data insertion if needed" -ForegroundColor White
Write-Host ""

View File

@@ -0,0 +1,68 @@
#!/bin/bash
#
# 檢查系統資源使用狀況
#
echo "=========================================="
echo " System Resource Check"
echo "=========================================="
echo ""
# CPU 資訊
echo "[1/5] CPU Information:"
lscpu | grep -E "^CPU\(s\)|^Model name|^Thread"
echo ""
# 記憶體使用
echo "[2/5] Memory Usage:"
free -h
echo ""
# 磁碟使用
echo "[3/5] Disk Usage:"
df -h | grep -E "^Filesystem|/$|/data"
echo ""
# Docker 資源使用
echo "[4/5] Docker Resource Usage:"
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
echo ""
# PostgreSQL 容器資源
echo "[5/5] PostgreSQL Containers:"
docker ps --filter "ancestor=postgres:16-alpine" --format "table {{.Names}}\t{{.Status}}\t{{.Size}}"
echo ""
echo "=========================================="
echo " Resource Summary"
echo "=========================================="
echo ""
# 計算 PostgreSQL 容器數量和總記憶體
PG_COUNT=$(docker ps | grep postgres | wc -l)
echo "Current PostgreSQL containers: $PG_COUNT"
echo ""
# 預估資源需求
echo "Estimated resource requirements:"
echo ""
echo "Option A: Shared PostgreSQL (1 container)"
echo " ├── CPU: ~2-5%"
echo " ├── Memory: ~100-200 MB"
echo " └── Disk: ~500 MB"
echo ""
echo "Option B: Microservices (3 containers)"
echo " ├── CPU: ~6-15%"
echo " ├── Memory: ~300-600 MB"
echo " └── Disk: ~1.5 GB"
echo ""
# 系統規格回顧
echo "Your Ubuntu Server specs:"
echo " ├── CPU: 12th Gen Intel Core i5-12400F"
echo " ├── RAM: 32 GB"
echo " └── Storage: SSD 216GB + HDD 931GB"
echo ""
echo "Recommendation: You have MORE than enough resources!"
echo " Both options are viable."
echo ""

396
scripts/complete-setup.sh Normal file
View 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 ""

View File

@@ -0,0 +1,123 @@
#!/bin/bash
#
# 修復 PostgreSQL 端口綁定
# 讓 PostgreSQL 可以從外部訪問
#
set -e
echo "=========================================="
echo " Fix PostgreSQL Port Binding"
echo "=========================================="
echo ""
# 1. 檢查現有 PostgreSQL 容器
echo "[1/5] Checking existing PostgreSQL container..."
docker ps -a | grep postgres
echo ""
echo "Current port bindings:"
docker port postgres 2>/dev/null || echo "No port bindings found"
echo ""
read -p "Continue to reconfigure PostgreSQL? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Aborted."
exit 0
fi
echo ""
# 2. 備份現有配置
echo "[2/5] Getting container information..."
# Get current postgres password
POSTGRES_PASSWORD=$(docker exec postgres env | grep POSTGRES_PASSWORD | cut -d= -f2)
if [ -z "$POSTGRES_PASSWORD" ]; then
read -sp "Enter postgres superuser password: " POSTGRES_PASSWORD
echo ""
fi
# Check for data volume
VOLUME_NAME=$(docker inspect postgres --format '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{.Name}}{{end}}{{end}}')
if [ -z "$VOLUME_NAME" ]; then
VOLUME_NAME="postgres-data"
echo " Using default volume name: $VOLUME_NAME"
else
echo " Found existing volume: $VOLUME_NAME"
fi
echo ""
# 3. 停止並移除舊容器
echo "[3/5] Stopping and removing old container..."
docker stop postgres
docker rm postgres
echo " ✓ Old container removed"
echo ""
# 4. 重新啟動容器,綁定到所有網路介面
echo "[4/5] Starting new PostgreSQL container..."
docker run -d \
--name postgres \
--restart unless-stopped \
-e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
-e POSTGRES_INITDB_ARGS="--encoding=UTF-8 --locale=en_US.UTF-8" \
-e TZ=Asia/Taipei \
-p 0.0.0.0:5432:5432 \
-v ${VOLUME_NAME}:/var/lib/postgresql/data \
postgres:16
echo " ✓ New container started"
echo ""
# 5. 等待 PostgreSQL 啟動
echo "[5/5] Waiting for PostgreSQL to be ready..."
sleep 5
for i in {1..30}; do
if docker exec postgres pg_isready -U postgres >/dev/null 2>&1; then
echo " ✓ PostgreSQL is ready!"
break
fi
echo " Waiting... ($i/30)"
sleep 1
done
echo ""
# 驗證配置
echo "Verifying configuration..."
echo ""
echo "Port bindings:"
docker port postgres
echo ""
echo "Testing local connection:"
docker exec postgres psql -U postgres -c "SELECT version();" | head -3
echo ""
echo "Database list:"
docker exec postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -v "^$" | grep -v "template"
echo ""
echo "=========================================="
echo " ✓ Configuration Complete!"
echo "=========================================="
echo ""
echo "PostgreSQL is now accessible from external hosts"
echo ""
echo "Connection details:"
echo " Host: 10.1.0.254"
echo " Port: 5432"
echo " Databases: postgres, hr_portal (if created)"
echo ""
echo "Test from Windows:"
echo " psql -h 10.1.0.254 -U hr_user -d hr_portal"
echo ""
echo "Or update backend/.env:"
echo " DATABASE_URL=postgresql://hr_user:DC1qaz2wsx@10.1.0.254:5432/hr_portal"
echo ""

View File

@@ -0,0 +1,60 @@
#!/bin/bash
# Fix PostgreSQL port binding - Remote execution script
POSTGRES_PASSWORD="DC1qaz2wsx"
echo "=========================================="
echo " Reconfiguring PostgreSQL Container"
echo "=========================================="
echo ""
echo "[1/4] Stopping old container..."
docker stop postgres
docker rm postgres
echo ""
echo "[2/4] Starting new container with external access..."
docker run -d \
--name postgres \
--restart unless-stopped \
-e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
-e POSTGRES_INITDB_ARGS="--encoding=UTF-8 --locale=en_US.UTF-8" \
-e TZ=Asia/Taipei \
-p 0.0.0.0:5432:5432 \
-v postgres-data:/var/lib/postgresql/data \
postgres:16
echo ""
echo "[3/4] Waiting for PostgreSQL to be ready..."
sleep 5
for i in {1..20}; do
if docker exec postgres pg_isready -U postgres >/dev/null 2>&1; then
echo " PostgreSQL is ready!"
break
fi
echo " Waiting... ($i/20)"
sleep 1
done
echo ""
echo "[4/4] Verifying configuration..."
echo ""
echo "Container status:"
docker ps | grep postgres
echo ""
echo "Port binding:"
docker port postgres
echo ""
echo "Database check:"
docker exec postgres psql -U hr_user -d hr_portal -c "SELECT COUNT(*) as table_count FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';"
echo ""
echo "=========================================="
echo " Configuration Complete!"
echo "=========================================="
echo ""
echo "PostgreSQL is now accessible from 10.1.0.254:5432"
echo ""

378
scripts/init-db.sql Normal file
View File

@@ -0,0 +1,378 @@
-- ====================================
-- HR Portal Database Schema
-- ====================================
-- 創建時間: 2026-02-08
-- 資料庫: PostgreSQL 16
-- ====================================
-- 設定時區
SET timezone = 'Asia/Taipei';
-- ====================================
-- 1. 事業部表 (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, -- 將在建立 employees 表後設定外鍵
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;
-- ====================================
-- 2. 部門表 (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, -- 將在建立 employees 表後設定外鍵
email VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 初始資料 - 玄鐵風能授權服務
INSERT INTO divisions (business_unit_id, code, name, name_en, email) VALUES
(1, 'wind-licensing', '技術授權部', 'Technical Licensing', 'wind-licensing@ease.taipei'),
(1, 'wind-assessment', '風場評估部', 'Wind Assessment', 'wind-assessment@ease.taipei'),
(1, 'wind-service', '客戶服務部', 'Customer Service', 'wind-service@ease.taipei')
ON CONFLICT (code) DO NOTHING;
-- 國際碳權申請服務
INSERT INTO divisions (business_unit_id, code, name, name_en, email) VALUES
(2, 'carbon-apply', '碳權申請部', 'Carbon Application', 'carbon-apply@ease.taipei'),
(2, 'carbon-audit', '碳盤查部', 'Carbon Audit', 'carbon-audit@ease.taipei'),
(2, 'carbon-trade', '碳交易部', 'Carbon Trading', 'carbon-trade@ease.taipei')
ON CONFLICT (code) DO NOTHING;
-- 智能研發服務
INSERT INTO divisions (business_unit_id, code, name, name_en, email) VALUES
(3, 'software-dev', '軟體研發部', 'Software Development', 'software@lab.taipei'),
(3, 'hardware-dev', '硬體研發部', 'Hardware Development', 'hardware@lab.taipei'),
(3, 'product-mgmt', '產品管理部', 'Product Management', 'product@lab.taipei')
ON CONFLICT (code) DO NOTHING;
-- 管理部門
INSERT INTO divisions (business_unit_id, code, name, name_en, email) VALUES
(4, 'hr', '人力資源部', 'Human Resources', 'hr@porscheworld.tw'),
(4, 'finance', '財務部', 'Finance', 'finance@porscheworld.tw'),
(4, 'admin', '行政部', 'Administration', 'admin@porscheworld.tw'),
(4, 'it', '資訊部', 'Information Technology', 'it@porscheworld.tw')
ON CONFLICT (code) DO NOTHING;
-- ====================================
-- 3. 員工表 (Employees)
-- ====================================
CREATE TABLE IF NOT EXISTS employees (
id SERIAL PRIMARY KEY,
keycloak_user_id UUID UNIQUE,
employee_id VARCHAR(20) UNIQUE NOT NULL,
username VARCHAR(100) UNIQUE NOT NULL,
-- 基本資料
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
chinese_name VARCHAR(50),
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20),
mobile VARCHAR(20),
-- 任職資訊
business_unit_id INTEGER REFERENCES business_units(id),
division_id INTEGER REFERENCES divisions(id),
team VARCHAR(100),
position VARCHAR(100),
job_title VARCHAR(100),
job_level VARCHAR(20), -- C-Level, VP, Director, Manager, Senior, Staff, etc.
employment_type VARCHAR(20) DEFAULT 'full-time', -- full-time, part-time, contractor, intern
-- 日期
hire_date DATE,
termination_date DATE,
-- 狀態
status VARCHAR(20) DEFAULT 'active', -- active, inactive, suspended, terminated
-- 審計
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100),
updated_by VARCHAR(100)
);
-- 索引
CREATE INDEX idx_employees_keycloak_id ON employees(keycloak_user_id);
CREATE INDEX idx_employees_status ON employees(status);
CREATE INDEX idx_employees_business_unit ON employees(business_unit_id);
CREATE INDEX idx_employees_division ON employees(division_id);
CREATE INDEX idx_employees_email ON employees(email);
-- 新增外鍵到 business_units 和 divisions
ALTER TABLE business_units ADD CONSTRAINT fk_business_unit_manager
FOREIGN KEY (manager_id) REFERENCES employees(id) ON DELETE SET NULL;
ALTER TABLE divisions ADD CONSTRAINT fk_division_manager
FOREIGN KEY (manager_id) REFERENCES employees(id) ON DELETE SET NULL;
-- ====================================
-- 4. 郵件帳號表 (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(255) UNIQUE NOT NULL,
mailbox_quota_mb INTEGER DEFAULT 1024,
mailbox_used_mb INTEGER DEFAULT 0,
-- 郵件設定
forward_to VARCHAR(255),
auto_reply BOOLEAN DEFAULT FALSE,
auto_reply_message TEXT,
-- 狀態
is_active BOOLEAN DEFAULT TRUE,
-- 審計
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100)
);
CREATE INDEX idx_email_accounts_employee ON email_accounts(employee_id);
CREATE INDEX idx_email_accounts_email ON email_accounts(email_address);
-- ====================================
-- 5. 網路硬碟表 (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(500) NOT NULL,
quota_gb INTEGER DEFAULT 10,
used_gb DECIMAL(10,2) DEFAULT 0,
-- 存取設定
webdav_url VARCHAR(500),
smb_path VARCHAR(500),
-- 權限
can_share BOOLEAN DEFAULT FALSE,
-- 狀態
is_active BOOLEAN DEFAULT TRUE,
-- 審計
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_network_drives_employee ON network_drives(employee_id);
-- ====================================
-- 6. 系統權限表 (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,
system_url VARCHAR(500),
access_level VARCHAR(50), -- admin, user, readonly
-- 狀態
is_active BOOLEAN DEFAULT TRUE,
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
granted_by VARCHAR(100),
revoked_at TIMESTAMP,
revoked_by VARCHAR(100),
UNIQUE(employee_id, system_name)
);
CREATE INDEX idx_system_permissions_employee ON system_permissions(employee_id);
CREATE INDEX idx_system_permissions_system ON system_permissions(system_name);
-- ====================================
-- 7. 專案表 (Projects)
-- ====================================
CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
business_unit_id INTEGER REFERENCES business_units(id),
project_code VARCHAR(50) UNIQUE NOT NULL,
project_name VARCHAR(200) NOT NULL,
description TEXT,
client_name VARCHAR(200),
-- 專案狀態
status VARCHAR(20) DEFAULT 'planning', -- planning, active, on-hold, completed, cancelled
-- 專案經理
project_manager_id INTEGER REFERENCES employees(id),
-- 時間
start_date DATE,
end_date DATE,
actual_end_date DATE,
-- 預算
budget_amount DECIMAL(15,2),
budget_currency VARCHAR(10) DEFAULT 'TWD',
-- 專案空間
nas_path VARCHAR(500),
git_repo VARCHAR(200),
-- 審計
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100)
);
CREATE INDEX idx_projects_business_unit ON projects(business_unit_id);
CREATE INDEX idx_projects_status ON projects(status);
CREATE INDEX idx_projects_manager ON projects(project_manager_id);
-- ====================================
-- 8. 專案成員表 (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(50), -- project-manager, developer, consultant, support
allocation_percentage INTEGER DEFAULT 100, -- 0-100
joined_date DATE DEFAULT CURRENT_DATE,
left_date DATE,
UNIQUE(project_id, employee_id)
);
CREATE INDEX idx_project_members_project ON project_members(project_id);
CREATE INDEX idx_project_members_employee ON project_members(employee_id);
-- ====================================
-- 9. 審計日誌表 (Audit Logs)
-- ====================================
CREATE TABLE IF NOT EXISTS audit_logs (
id SERIAL PRIMARY KEY,
employee_id INTEGER REFERENCES employees(id),
action VARCHAR(50) NOT NULL, -- create, update, delete, login, logout
resource_type VARCHAR(50), -- employee, email, drive, project
resource_id INTEGER,
old_value JSONB,
new_value JSONB,
ip_address VARCHAR(50),
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_resource ON audit_logs(resource_type, resource_id);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at DESC);
-- ====================================
-- 10. 更新時間觸發器函數
-- ====================================
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();
-- ====================================
-- 11. 建立視圖 (Views)
-- ====================================
-- 員工完整資訊視圖
CREATE OR REPLACE VIEW v_employees_full AS
SELECT
e.*,
bu.name as business_unit_name,
bu.name_en as business_unit_name_en,
d.name as division_name,
d.name_en as division_name_en
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_project_stats AS
SELECT
p.id,
p.project_code,
p.project_name,
p.status,
COUNT(pm.id) as member_count,
SUM(pm.allocation_percentage) as total_allocation
FROM projects p
LEFT JOIN project_members pm ON p.id = pm.project_id AND pm.left_date IS NULL
GROUP BY p.id, p.project_code, p.project_name, p.status;
-- 部門人力統計視圖
CREATE OR REPLACE VIEW v_division_headcount AS
SELECT
bu.name as business_unit_name,
d.name as division_name,
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 e.division_id = d.id
GROUP BY bu.name, d.name;
-- ====================================
-- 完成
-- ====================================
-- 授予權限 (依實際用戶名調整)
-- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO hr_user;
-- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO hr_user;
SELECT 'HR Portal Database Schema 初始化完成!' as message;

View File

@@ -0,0 +1,275 @@
-- ====================================
-- HR Portal - 測試資料 (修正版)
-- ====================================
-- 插入測試員工
INSERT INTO employees (
employee_id,
username,
first_name,
last_name,
chinese_name,
email,
mobile,
business_unit_id,
position,
job_level,
hire_date,
status
) VALUES
-- 管理層
(
'E0001',
'porsche.chen',
'Porsche',
'Chen',
'陳博駿',
'porsche.chen@porscheworld.tw',
'0912-345-678',
4, -- 管理部門
'CTO / 技術長',
'C-Level',
'2020-01-01',
'active'
),
-- 智能研發服務事業部
(
'E1001',
'alice.wang',
'Alice',
'Wang',
'王小華',
'alice.wang@lab.taipei',
'0922-111-222',
3, -- 智能研發
'Technical Director',
'Director',
'2020-06-01',
'active'
),
(
'E1002',
'bob.chen',
'Bob',
'Chen',
'陳大明',
'bob.chen@lab.taipei',
'0933-222-333',
3, -- 智能研發
'Senior Software Engineer',
'Senior',
'2021-03-15',
'active'
),
-- 玄鐵風能授權服務事業部
(
'E2001',
'charlie.lin',
'Charlie',
'Lin',
'林小風',
'charlie.lin@ease.taipei',
'0944-333-444',
1, -- 玄鐵風能
'Wind Energy Consultant',
'Senior',
'2021-08-01',
'active'
),
-- 國際碳權申請服務事業部
(
'E3001',
'diana.wu',
'Diana',
'Wu',
'吳小綠',
'diana.wu@ease.taipei',
'0955-444-555',
2, -- 國際碳權
'Carbon Credit Specialist',
'Staff',
'2022-01-10',
'active'
);
-- 插入郵件帳號
INSERT INTO email_accounts (employee_id, email_address, mailbox_quota_mb, is_active)
SELECT
e.id,
e.email,
CASE e.job_level
WHEN 'C-Level' THEN 20480
WHEN 'Director' THEN 10240
WHEN 'Manager' THEN 10240
WHEN 'Senior' THEN 5120
ELSE 5120
END as quota,
true
FROM employees e;
-- 插入網路硬碟
INSERT INTO network_drives (
employee_id,
drive_name,
drive_path,
quota_gb,
webdav_url,
smb_path,
is_active
)
SELECT
e.id,
e.username || '_personal',
'/nas/users/' || e.username,
CASE e.job_level
WHEN 'C-Level' THEN 500
WHEN 'Director' THEN 200
WHEN 'Manager' THEN 100
WHEN 'Senior' THEN 80
ELSE 50
END as quota,
'https://nas.porscheworld.tw/webdav/' || e.username,
'//10.1.0.30/homes/' || e.username,
true
FROM employees e;
-- 插入系統權限
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
SELECT e.id, 'HR Portal',
CASE
WHEN e.job_level IN ('C-Level', 'Director') THEN 'admin'
WHEN e.job_level = 'Manager' THEN 'manager'
ELSE 'user'
END,
true,
'Initial access granted'
FROM employees e;
-- 額外權限: Gitea
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
SELECT e.id, 'Gitea', 'user', true, 'Git repository access'
FROM employees e
WHERE e.business_unit_id = 3; -- 智能研發事業部
-- 額外權限: Portainer (給 IT 人員)
INSERT INTO system_permissions (employee_id, system_name, access_level, is_active, notes)
VALUES
((SELECT id FROM employees WHERE username = 'porsche.chen'), 'Portainer', 'admin', true, 'Docker management');
-- 插入測試專案
INSERT INTO projects (project_code, project_name, description, project_manager_id, start_date, status)
VALUES
(
'WIND-2024-001',
'離岸風電技術評估',
'台中港離岸風電場技術可行性評估',
(SELECT id FROM employees WHERE username = 'charlie.lin'),
'2024-01-15',
'in_progress'
),
(
'CARBON-2024-001',
'企業碳盤查輔導',
'協助製造業進行 ISO 14064-1 碳盤查',
(SELECT id FROM employees WHERE username = 'diana.wu'),
'2024-02-01',
'in_progress'
),
(
'AI-2024-001',
'HR Portal 系統開發',
'開發整合 Keycloak 的人資管理系統',
(SELECT id FROM employees WHERE username = 'alice.wang'),
'2024-01-01',
'in_progress'
);
-- 插入專案成員
INSERT INTO project_members (project_id, employee_id, role)
VALUES
-- WIND-2024-001 團隊
((SELECT id FROM projects WHERE project_code = 'WIND-2024-001'),
(SELECT id FROM employees WHERE username = 'charlie.lin'),
'Project Manager'),
-- CARBON-2024-001 團隊
((SELECT id FROM projects WHERE project_code = 'CARBON-2024-001'),
(SELECT id FROM employees WHERE username = 'diana.wu'),
'Project Manager'),
-- AI-2024-001 團隊
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'),
(SELECT id FROM employees WHERE username = 'alice.wang'),
'Project Manager'),
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'),
(SELECT id FROM employees WHERE username = 'bob.chen'),
'Lead Developer'),
((SELECT id FROM projects WHERE project_code = 'AI-2024-001'),
(SELECT id FROM employees WHERE username = 'porsche.chen'),
'Technical Advisor');
-- 驗證結果
SELECT '=== 員工列表 ===' as info;
SELECT
e.employee_id,
e.username,
e.chinese_name,
bu.name as business_unit,
e.position,
e.job_level
FROM employees e
LEFT JOIN business_units bu ON e.business_unit_id = bu.id
ORDER BY e.employee_id;
SELECT '=== 郵件帳號 ===' as info;
SELECT
e.username,
ea.email_address,
ea.mailbox_quota_mb || ' MB' as quota
FROM email_accounts ea
JOIN employees e ON ea.employee_id = e.id
ORDER BY e.employee_id;
SELECT '=== 網路硬碟 ===' as info;
SELECT
e.username,
nd.drive_name,
nd.quota_gb || ' GB' as quota
FROM network_drives nd
JOIN employees e ON nd.employee_id = e.id
ORDER BY e.employee_id;
SELECT '=== 系統權限 ===' as info;
SELECT
e.username,
sp.system_name,
sp.access_level
FROM system_permissions sp
JOIN employees e ON sp.employee_id = e.id
WHERE sp.is_active = true
ORDER BY e.employee_id, sp.system_name;
SELECT '=== 專案列表 ===' as info;
SELECT
p.project_code,
p.project_name,
e.chinese_name as manager,
COUNT(pm.id) as members,
p.status
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, e.chinese_name, p.status
ORDER BY p.project_code;
SELECT '=== 完成! ===' as info;
SELECT
(SELECT COUNT(*) FROM employees) as employees,
(SELECT COUNT(*) FROM email_accounts) as email_accounts,
(SELECT COUNT(*) FROM network_drives) as network_drives,
(SELECT COUNT(*) FROM system_permissions WHERE is_active = true) as permissions,
(SELECT COUNT(*) FROM projects) as projects;

View File

@@ -0,0 +1,393 @@
-- ====================================
-- HR Portal - 測試資料
-- ====================================
-- 用途: 插入測試資料以驗證系統功能
-- ====================================
-- 檢查事業部資料
SELECT '=== 事業部資料 ===' as info;
SELECT id, code, name, name_en FROM business_units ORDER BY id;
-- 檢查部門資料
SELECT '=== 部門資料 ===' as info;
SELECT id, business_unit_id, code, name, name_en FROM divisions ORDER BY id;
-- 插入測試員工
INSERT INTO employees (
employee_id,
username,
first_name,
last_name,
chinese_name,
email,
mobile,
business_unit_id,
division_id,
position,
job_title,
job_level,
employment_type,
hire_date,
status
) VALUES
-- 管理層
(
'E0001',
'porsche.chen',
'Porsche',
'Chen',
'陳博駿',
'porsche.chen@porscheworld.tw',
'0912-345-678',
4, -- 管理部門
10, -- 資訊部
'CTO',
'技術長',
'C-Level',
'full-time',
'2020-01-01',
'active'
),
-- 智能研發服務 - 軟體工程師
(
'E1001',
'alice.wang',
'Alice',
'Wang',
'王小華',
'alice.wang@lab.taipei',
'0923-456-789',
3, -- 智能研發服務
7, -- 軟體研發部
'Frontend Developer',
'前端工程師',
'Engineer',
'full-time',
'2024-03-01',
'active'
),
(
'E1002',
'bob.chen',
'Bob',
'Chen',
'陳大明',
'bob.chen@lab.taipei',
'0934-567-890',
3, -- 智能研發服務
7, -- 軟體研發部
'Backend Developer',
'後端工程師',
'Senior',
'full-time',
'2023-06-15',
'active'
),
-- 玄鐵風能授權服務
(
'E2001',
'charlie.lin',
'Charlie',
'Lin',
'林志明',
'charlie.lin@ease.taipei',
'0945-678-901',
1, -- 玄鐵風能
1, -- 技術授權部
'Sales Manager',
'業務經理',
'Manager',
'full-time',
'2022-08-20',
'active'
),
-- 國際碳權申請服務
(
'E3001',
'diana.wu',
'Diana',
'Wu',
'吳雅婷',
'diana.wu@ease.taipei',
'0956-789-012',
2, -- 國際碳權
4, -- 碳權申請部
'Carbon Credit Consultant',
'碳權顧問',
'Senior',
'full-time',
'2023-02-10',
'active'
)
ON CONFLICT (employee_id) DO NOTHING;
-- 插入測試郵件帳號
INSERT INTO email_accounts (
employee_id,
email_address,
mailbox_quota_mb,
is_active
)
SELECT
id,
email,
CASE
WHEN job_level = 'C-Level' THEN 5120 -- 5GB
WHEN job_level = 'Manager' THEN 2048 -- 2GB
ELSE 1024 -- 1GB
END,
TRUE
FROM employees
WHERE email IS NOT NULL
ON CONFLICT (email_address) DO NOTHING;
-- 插入測試網路硬碟
INSERT INTO network_drives (
employee_id,
drive_name,
drive_path,
quota_gb,
webdav_url,
smb_path,
is_active
)
SELECT
id,
username || '_personal',
'/volume1/homes/' || username,
CASE
WHEN job_level = 'C-Level' THEN 50
WHEN job_level = 'VP' THEN 30
WHEN job_level = 'Director' THEN 30
WHEN job_level = 'Manager' THEN 20
WHEN job_level = 'Senior' THEN 15
ELSE 10
END,
'https://nas.porscheworld.tw/webdav/' || username,
'\\10.1.0.30\homes\' || username,
TRUE
FROM employees
ON CONFLICT DO NOTHING;
-- 插入測試系統權限
INSERT INTO system_permissions (
employee_id,
system_name,
system_url,
access_level,
is_active,
granted_by
)
SELECT
id,
system,
url,
access_level,
TRUE,
'system'
FROM employees
CROSS JOIN (
VALUES
('HR Portal', 'https://hr.porscheworld.tw', 'user'),
('Webmail', 'https://webmail.porscheworld.tw', 'user')
) AS systems(system, url, access_level)
WHERE status = 'active'
ON CONFLICT (employee_id, system_name) DO NOTHING;
-- 為研發人員新增額外權限
INSERT INTO system_permissions (
employee_id,
system_name,
system_url,
access_level,
is_active,
granted_by
)
SELECT
id,
system,
url,
access_level,
TRUE,
'system'
FROM employees
CROSS JOIN (
VALUES
('Gitea', 'https://git.lab.taipei', 'developer'),
('Portainer', 'https://portainer.porscheworld.tw', 'user')
) AS dev_systems(system, url, access_level)
WHERE division_id IN (7, 8, 9) -- 研發部門
AND status = 'active'
ON CONFLICT (employee_id, system_name) DO NOTHING;
-- 插入測試專案
INSERT INTO projects (
business_unit_id,
project_code,
project_name,
description,
client_name,
status,
project_manager_id,
start_date,
end_date,
budget_amount,
budget_currency
) VALUES
(
3, -- 智能研發
'PRJ-2024-001',
'HR Portal 開發專案',
'企業人資管理系統開發',
'內部專案',
'active',
(SELECT id FROM employees WHERE username = 'porsche.chen'),
'2024-01-15',
'2024-06-30',
1000000.00,
'TWD'
),
(
1, -- 玄鐵風能
'PRJ-2024-002',
'台中風場評估案',
'台中離岸風場技術評估與授權',
'台灣風電公司',
'active',
(SELECT id FROM employees WHERE username = 'charlie.lin'),
'2024-02-01',
'2024-08-31',
5000000.00,
'TWD'
),
(
2, -- 國際碳權
'PRJ-2024-003',
'某企業碳盤查專案',
'ISO 14064-1 組織型溫室氣體盤查',
'科技公司 A',
'planning',
(SELECT id FROM employees WHERE username = 'diana.wu'),
'2024-03-01',
'2024-12-31',
800000.00,
'TWD'
)
ON CONFLICT (project_code) DO NOTHING;
-- 插入專案成員
INSERT INTO project_members (
project_id,
employee_id,
role,
allocation_percentage
)
SELECT
p.id,
e.id,
member.role,
member.allocation
FROM projects p
CROSS JOIN LATERAL (
VALUES
((SELECT id FROM employees WHERE username = 'porsche.chen'), 'project-manager', 50),
((SELECT id FROM employees WHERE username = 'alice.wang'), 'developer', 80),
((SELECT id FROM employees WHERE username = 'bob.chen'), 'developer', 100)
) AS member(employee_id, role, allocation)
JOIN employees e ON e.id = member.employee_id
WHERE p.project_code = 'PRJ-2024-001'
ON CONFLICT (project_id, employee_id) DO NOTHING;
-- ====================================
-- 查詢驗證資料
-- ====================================
SELECT '=== 員工資料 ===' as info;
SELECT
employee_id,
username,
chinese_name,
email,
position,
job_level,
status
FROM employees
ORDER BY employee_id;
SELECT '=== 郵件帳號 ===' as info;
SELECT
e.username,
ea.email_address,
ea.mailbox_quota_mb || 'MB' as quota,
ea.is_active
FROM email_accounts ea
JOIN employees e ON ea.employee_id = e.id
ORDER BY e.employee_id;
SELECT '=== 網路硬碟 ===' as info;
SELECT
e.username,
nd.drive_name,
nd.quota_gb || 'GB' as quota,
nd.is_active
FROM network_drives nd
JOIN employees e ON nd.employee_id = e.id
ORDER BY e.employee_id;
SELECT '=== 系統權限 ===' as info;
SELECT
e.username,
sp.system_name,
sp.access_level,
sp.is_active
FROM system_permissions sp
JOIN employees e ON sp.employee_id = e.id
ORDER BY e.username, sp.system_name;
SELECT '=== 專案資訊 ===' as info;
SELECT
p.project_code,
p.project_name,
p.status,
e.chinese_name as project_manager,
COUNT(pm.id) as member_count
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
ORDER BY p.project_code;
-- 統計資訊
SELECT '=== 統計資訊 ===' as info;
SELECT
'員工總數' as metric,
COUNT(*) as count
FROM employees
UNION ALL
SELECT
'活躍員工',
COUNT(*)
FROM employees
WHERE status = 'active'
UNION ALL
SELECT
'郵件帳號',
COUNT(*)
FROM email_accounts
UNION ALL
SELECT
'網路硬碟',
COUNT(*)
FROM network_drives
UNION ALL
SELECT
'活躍專案',
COUNT(*)
FROM projects
WHERE status = 'active';
SELECT '=== 資料插入完成 ===' as info;

108
scripts/run-auto-setup.ps1 Normal file
View File

@@ -0,0 +1,108 @@
# Auto run database setup with password
$ErrorActionPreference = "Stop"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " HR Portal Database Auto Setup" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
$REMOTE_HOST = "10.1.0.254"
$REMOTE_USER = "ubuntu"
$PASSWORD = "DC1qaz2wsx"
$LOCAL_SCRIPT = "W:\DevOps-Workspace\hr-portal\scripts\auto-setup.sh"
# Step 1: Upload script using plink (PuTTY)
Write-Host "[1/3] Uploading setup script..." -ForegroundColor Yellow
# Create expect-like script for SSH
$uploadCommand = @"
`$password = '$PASSWORD'
`$process = Start-Process plink -ArgumentList "-ssh $REMOTE_USER@$REMOTE_HOST -pw `$password 'cat > /tmp/auto-setup.sh'" -NoNewWindow -Wait -RedirectStandardInput '$LOCAL_SCRIPT' -PassThru
"@
# Fallback: Try with cmd echo
Write-Host " Trying direct SSH connection..." -ForegroundColor Gray
# Create a temporary script with password
$tempScript = @"
#!/usr/bin/expect -f
set timeout 60
spawn scp $LOCAL_SCRIPT ${REMOTE_USER}@${REMOTE_HOST}:/tmp/auto-setup.sh
expect "password:"
send "${PASSWORD}\r"
expect eof
"@
# Write temp script
$tempScriptPath = "$env:TEMP\upload-script.exp"
$tempScript | Out-File -FilePath $tempScriptPath -Encoding ASCII
# Try using WSL if available
$wslAvailable = Get-Command wsl -ErrorAction SilentlyContinue
if ($wslAvailable) {
Write-Host " Using WSL for transfer..." -ForegroundColor Gray
# Copy file to WSL
$wslTempPath = "/tmp/auto-setup.sh"
wsl cp "$LOCAL_SCRIPT" $wslTempPath
# Use sshpass in WSL
$result = wsl bash -c "sshpass -p '$PASSWORD' scp /tmp/auto-setup.sh ${REMOTE_USER}@${REMOTE_HOST}:/tmp/"
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] Script uploaded" -ForegroundColor Green
} else {
Write-Host " [FAIL] Upload failed" -ForegroundColor Red
exit 1
}
} else {
# Manual method
Write-Host ""
Write-Host " WSL not available. Please manually upload the script:" -ForegroundColor Yellow
Write-Host " 1. Open WinSCP or FileZilla" -ForegroundColor White
Write-Host " 2. Connect to: ubuntu@10.1.0.254 (password: DC1qaz2wsx)" -ForegroundColor White
Write-Host " 3. Upload: W:\DevOps-Workspace\hr-portal\scripts\auto-setup.sh" -ForegroundColor White
Write-Host " 4. To: /tmp/auto-setup.sh" -ForegroundColor White
Write-Host ""
Read-Host "Press Enter after upload completes"
}
Write-Host ""
# Step 2: Make executable and run
Write-Host "[2/3] Executing setup on remote server..." -ForegroundColor Yellow
if ($wslAvailable) {
$executeCommands = @"
chmod +x /tmp/auto-setup.sh
bash /tmp/auto-setup.sh
"@
$result = wsl bash -c "sshpass -p '$PASSWORD' ssh -o StrictHostKeyChecking=no ${REMOTE_USER}@${REMOTE_HOST} '$executeCommands'"
Write-Host $result
if ($LASTEXITCODE -eq 0) {
Write-Host ""
Write-Host " [OK] Setup completed!" -ForegroundColor Green
} else {
Write-Host " [WARNING] Check output above for any errors" -ForegroundColor Yellow
}
} else {
Write-Host " Please run these commands on Ubuntu Server:" -ForegroundColor Yellow
Write-Host " chmod +x /tmp/auto-setup.sh" -ForegroundColor White
Write-Host " bash /tmp/auto-setup.sh" -ForegroundColor White
Write-Host ""
}
Write-Host ""
Write-Host "[3/3] Database connection info:" -ForegroundColor Yellow
Write-Host ""
Write-Host " CONNECTION STRING:" -ForegroundColor Cyan
Write-Host " postgresql://hr_user:DC1qaz2wsx@10.1.0.254:5432/hr_portal" -ForegroundColor White
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Next: Update backend/.env file" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""

109
scripts/run-db-setup.ps1 Normal file
View File

@@ -0,0 +1,109 @@
# Run Database Setup on Ubuntu Server
# This script uploads files to Ubuntu Server and executes the setup
$ErrorActionPreference = "Stop"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " HR Portal Database Setup Launcher" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
$REMOTE_HOST = "ubuntu@10.1.0.254"
$REMOTE_DIR = "/tmp/hr-portal-setup"
$LOCAL_SCRIPT_DIR = "W:\DevOps-Workspace\hr-portal\scripts"
# Step 1: Create remote directory
Write-Host "[1/4] Preparing remote directory..." -ForegroundColor Yellow
ssh $REMOTE_HOST "mkdir -p $REMOTE_DIR && chmod 755 $REMOTE_DIR"
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] Remote directory ready: $REMOTE_DIR" -ForegroundColor Green
} else {
Write-Host " [FAIL] Could not create remote directory" -ForegroundColor Red
Write-Host ""
Write-Host "Please ensure SSH key-based authentication is configured." -ForegroundColor Yellow
Write-Host "Run: type `$env:USERPROFILE\.ssh\id_rsa.pub | ssh ubuntu@10.1.0.254 'cat >> ~/.ssh/authorized_keys'" -ForegroundColor Gray
exit 1
}
Write-Host ""
# Step 2: Upload files
Write-Host "[2/4] Uploading setup files..." -ForegroundColor Yellow
$filesToUpload = @(
"setup-hr-portal-db.sh",
"init-db.sql",
"insert-test-data.sql"
)
foreach ($file in $filesToUpload) {
$localPath = Join-Path $LOCAL_SCRIPT_DIR $file
if (Test-Path $localPath) {
Write-Host " Uploading $file..." -ForegroundColor Gray
scp $localPath "${REMOTE_HOST}:${REMOTE_DIR}/"
if ($LASTEXITCODE -ne 0) {
Write-Host " [FAIL] Failed to upload $file" -ForegroundColor Red
exit 1
}
} else {
Write-Host " [WARNING] File not found: $file" -ForegroundColor Yellow
}
}
Write-Host " [OK] Files uploaded successfully" -ForegroundColor Green
Write-Host ""
# Step 3: Make script executable
Write-Host "[3/4] Setting permissions..." -ForegroundColor Yellow
ssh $REMOTE_HOST "chmod +x $REMOTE_DIR/setup-hr-portal-db.sh"
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] Script is executable" -ForegroundColor Green
} else {
Write-Host " [FAIL] Could not set permissions" -ForegroundColor Red
exit 1
}
Write-Host ""
# Step 4: Execute setup script
Write-Host "[4/4] Executing database setup on remote server..." -ForegroundColor Yellow
Write-Host " You will be prompted for database passwords" -ForegroundColor Gray
Write-Host ""
ssh -t $REMOTE_HOST "cd $REMOTE_DIR && bash setup-hr-portal-db.sh"
if ($LASTEXITCODE -eq 0) {
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Database Setup Completed!" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Green
Write-Host " 1. The connection string has been displayed above" -ForegroundColor White
Write-Host " 2. Update W:\DevOps-Workspace\hr-portal\backend\.env" -ForegroundColor White
Write-Host " 3. Run insert-test-data.sql for sample data (optional)" -ForegroundColor White
Write-Host ""
} else {
Write-Host ""
Write-Host "[ERROR] Setup script failed" -ForegroundColor Red
Write-Host "Check the error messages above for details" -ForegroundColor Yellow
exit 1
}
# Cleanup prompt
Write-Host ""
$cleanup = Read-Host "Clean up remote setup files? (y/N)"
if ($cleanup -eq "y" -or $cleanup -eq "Y") {
Write-Host "Cleaning up..." -ForegroundColor Gray
ssh $REMOTE_HOST "rm -rf $REMOTE_DIR"
Write-Host " [OK] Cleanup complete" -ForegroundColor Green
}
Write-Host ""

View File

@@ -0,0 +1,103 @@
# Execute PostgreSQL fix remotely via SSH
$ErrorActionPreference = "Continue"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Fixing PostgreSQL Port Configuration" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
$REMOTE_HOST = "10.1.0.254"
$REMOTE_USER = "ubuntu"
$PASSWORD = "DC1qaz2wsx"
# Create the command as a single string
$commands = @"
echo '[1/4] Stopping old PostgreSQL container...'
docker stop postgres 2>/dev/null
docker rm postgres 2>/dev/null
echo ''
echo '[2/4] Starting new container with external access...'
docker run -d \
--name postgres \
--restart unless-stopped \
-e POSTGRES_PASSWORD="$PASSWORD" \
-e TZ=Asia/Taipei \
-p 0.0.0.0:5432:5432 \
-v postgres-data:/var/lib/postgresql/data \
postgres:16
echo ''
echo '[3/4] Waiting for PostgreSQL...'
sleep 8
echo ''
echo '[4/4] Verifying...'
docker ps | grep postgres
docker port postgres
docker exec postgres psql -U hr_user -d hr_portal -c 'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='\''public'\'' AND table_type='\''BASE TABLE'\'';' 2>/dev/null || echo 'Database check will be done after connection test'
echo ''
echo '=========================================='
echo 'PostgreSQL reconfiguration complete!'
echo '=========================================='
"@
Write-Host "Connecting to Ubuntu Server..." -ForegroundColor Yellow
Write-Host ""
# Try using plink if available
$plinkPath = Get-Command plink -ErrorAction SilentlyContinue
if ($plinkPath) {
Write-Host "Using plink for connection..." -ForegroundColor Gray
$commands | plink -ssh -batch -pw $PASSWORD "${REMOTE_USER}@${REMOTE_HOST}"
} else {
# Fallback to regular ssh
Write-Host "Using ssh for connection..." -ForegroundColor Gray
Write-Host "You may need to enter password: $PASSWORD" -ForegroundColor Yellow
Write-Host ""
# Save commands to temp file
$tempFile = "$env:TEMP\fix-postgres-commands.sh"
$commands | Out-File -FilePath $tempFile -Encoding UTF8
# Try to execute via ssh with heredoc
$sshCommand = "bash -s"
Get-Content $tempFile | ssh "${REMOTE_USER}@${REMOTE_HOST}" $sshCommand
}
$exitCode = $LASTEXITCODE
Write-Host ""
if ($exitCode -eq 0) {
Write-Host "========================================" -ForegroundColor Green
Write-Host " Success!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "PostgreSQL is now accessible from:" -ForegroundColor White
Write-Host " Host: 10.1.0.254" -ForegroundColor Cyan
Write-Host " Port: 5432" -ForegroundColor Cyan
Write-Host ""
Write-Host "Testing connection from Windows..." -ForegroundColor Yellow
Write-Host ""
# Test connection
Start-Sleep -Seconds 3
Write-Host "Running database connection test..." -ForegroundColor Yellow
cd "W:\DevOps-Workspace\hr-portal\backend"
python test_db_connection.py
} else {
Write-Host "========================================" -ForegroundColor Red
Write-Host " Error occurred" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Red
Write-Host ""
Write-Host "Exit code: $exitCode" -ForegroundColor Yellow
Write-Host ""
Write-Host "Please check the output above for errors." -ForegroundColor Yellow
}
Write-Host ""

102
scripts/setup-database.ps1 Normal file
View File

@@ -0,0 +1,102 @@
# ====================================
# HR Portal - 資料庫設定腳本 (PowerShell)
# ====================================
Write-Host "=== HR Portal 資料庫設定 ===" -ForegroundColor Cyan
Write-Host ""
# 設定變數
$DB_HOST = "10.1.0.254"
$DB_NAME = "hr_portal"
$DB_USER = "hr_user"
$DB_PASSWORD = "hr_password_change_me" # 請修改為強密碼
Write-Host "連接到 PostgreSQL..." -ForegroundColor Yellow
Write-Host ""
# 檢查 psql 是否可用
$psqlAvailable = Get-Command psql -ErrorAction SilentlyContinue
if (-not $psqlAvailable) {
Write-Host "錯誤: 找不到 psql 命令" -ForegroundColor Red
Write-Host "請確認 PostgreSQL 客戶端已安裝" -ForegroundColor Red
Write-Host "或使用 Docker 方式執行" -ForegroundColor Yellow
exit 1
}
try {
# 1. 創建資料庫用戶
Write-Host "1. 創建資料庫用戶: $DB_USER" -ForegroundColor Green
$createUserSQL = @"
DO `$`$
BEGIN
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '$DB_USER') THEN
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
RAISE NOTICE 'User $DB_USER created';
ELSE
RAISE NOTICE 'User $DB_USER already exists';
END IF;
END
`$`$;
"@
$createUserSQL | psql -h $DB_HOST -U postgres -w
Write-Host ""
# 2. 創建資料庫
Write-Host "2. 創建資料庫: $DB_NAME" -ForegroundColor Green
$createDbSQL = @"
SELECT 'CREATE DATABASE $DB_NAME OWNER $DB_USER'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$DB_NAME')\gexec
"@
$createDbSQL | psql -h $DB_HOST -U postgres -w
Write-Host ""
# 3. 授予權限
Write-Host "3. 授予權限" -ForegroundColor Green
$grantSQL = @"
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO $DB_USER;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO $DB_USER;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $DB_USER;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $DB_USER;
"@
$grantSQL | psql -h $DB_HOST -U postgres -d $DB_NAME -w
Write-Host ""
# 4. 執行 Schema 初始化
Write-Host "4. 執行 Schema 初始化" -ForegroundColor Green
$schemaFile = Join-Path $PSScriptRoot "init-db.sql"
if (Test-Path $schemaFile) {
Get-Content $schemaFile | psql -h $DB_HOST -U $DB_USER -d $DB_NAME
} else {
Write-Host "警告: 找不到 init-db.sql" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "✅ 資料庫設定完成!" -ForegroundColor Green
Write-Host ""
Write-Host "資料庫連接資訊:" -ForegroundColor Cyan
Write-Host " Host: $DB_HOST"
Write-Host " Database: $DB_NAME"
Write-Host " User: $DB_USER"
Write-Host " Password: $DB_PASSWORD"
Write-Host ""
Write-Host "DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}" -ForegroundColor Yellow
Write-Host ""
Write-Host "請將上述 DATABASE_URL 複製到 backend/.env 檔案中" -ForegroundColor Cyan
} catch {
Write-Host "錯誤: $_" -ForegroundColor Red
exit 1
}

73
scripts/setup-database.sh Normal file
View File

@@ -0,0 +1,73 @@
#!/bin/bash
# ====================================
# HR Portal - 資料庫設定腳本
# ====================================
set -e
echo "=== HR Portal 資料庫設定 ==="
echo ""
# 設定變數
DB_HOST="10.1.0.254"
DB_NAME="hr_portal"
DB_USER="hr_user"
DB_PASSWORD="hr_password_change_me" # 請修改為強密碼
echo "連接到 PostgreSQL..."
echo ""
# 1. 創建資料庫用戶 (如果不存在)
echo "1. 創建資料庫用戶: $DB_USER"
ssh ubuntu@$DB_HOST "docker exec -i postgres psql -U postgres" <<EOF
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '$DB_USER') THEN
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
RAISE NOTICE 'User $DB_USER created';
ELSE
RAISE NOTICE 'User $DB_USER already exists';
END IF;
END
\$\$;
EOF
echo ""
# 2. 創建資料庫 (如果不存在)
echo "2. 創建資料庫: $DB_NAME"
ssh ubuntu@$DB_HOST "docker exec -i postgres psql -U postgres" <<EOF
SELECT 'CREATE DATABASE $DB_NAME OWNER $DB_USER'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$DB_NAME')\gexec
EOF
echo ""
# 3. 授予權限
echo "3. 授予權限"
ssh ubuntu@$DB_HOST "docker exec -i postgres psql -U postgres -d $DB_NAME" <<EOF
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO $DB_USER;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO $DB_USER;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $DB_USER;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $DB_USER;
EOF
echo ""
# 4. 執行 Schema 初始化
echo "4. 執行 Schema 初始化"
ssh ubuntu@$DB_HOST "docker exec -i postgres psql -U $DB_USER -d $DB_NAME" < init-db.sql
echo ""
echo "✅ 資料庫設定完成!"
echo ""
echo "資料庫連接資訊:"
echo " Host: $DB_HOST"
echo " Database: $DB_NAME"
echo " User: $DB_USER"
echo " Password: $DB_PASSWORD"
echo ""
echo "DATABASE_URL: postgresql://$DB_USER:$DB_PASSWORD@$DB_HOST:5432/$DB_NAME"
echo ""
echo "請將上述 DATABASE_URL 複製到 backend/.env 檔案中"

185
scripts/setup-db-simple.ps1 Normal file
View File

@@ -0,0 +1,185 @@
# ====================================
# HR Portal - 簡化版資料庫設定
# ====================================
param(
[string]$DBHost = "10.1.0.254",
[string]$DBName = "hr_portal",
[string]$DBUser = "hr_user",
[string]$DBPassword = "",
[string]$PostgresPassword = "",
[string]$ContainerName = "postgres"
)
Write-Host "=== HR Portal 資料庫設定 ===" -ForegroundColor Cyan
Write-Host ""
# 檢查必要參數
if (-not $DBPassword) {
$DBPassword = Read-Host "請輸入 hr_user 的密碼" -AsSecureString
$DBPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($DBPassword)
)
}
if (-not $PostgresPassword) {
$PostgresPassword = Read-Host "請輸入 postgres 超級用戶密碼" -AsSecureString
$PostgresPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($PostgresPassword)
)
}
Write-Host ""
Write-Host "設定資訊:" -ForegroundColor Yellow
Write-Host " 資料庫主機: $DBHost"
Write-Host " 資料庫名稱: $DBName"
Write-Host " 資料庫用戶: $DBUser"
Write-Host " 容器名稱: $ContainerName"
Write-Host ""
$continue = Read-Host "是否繼續? (y/N)"
if ($continue -ne 'y' -and $continue -ne 'Y') {
Write-Host "已取消" -ForegroundColor Yellow
exit 0
}
Write-Host ""
# === 步驟 1: 創建用戶 ===
Write-Host "步驟 1/4: 創建資料庫用戶..." -ForegroundColor Green
$createUserSQL = @"
DO `$`$
BEGIN
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '$DBUser') THEN
CREATE USER $DBUser WITH PASSWORD '$DBPassword';
RAISE NOTICE 'User $DBUser created';
ELSE
RAISE NOTICE 'User $DBUser already exists';
END IF;
END
`$`$;
"@
$createUserCmd = "echo `"$createUserSQL`" | docker exec -i $ContainerName psql -U postgres"
try {
ssh ubuntu@$DBHost $createUserCmd
Write-Host "✓ 用戶創建完成" -ForegroundColor Green
} catch {
Write-Host "✗ 用戶創建失敗: $_" -ForegroundColor Red
exit 1
}
Write-Host ""
# === 步驟 2: 創建資料庫 ===
Write-Host "步驟 2/4: 創建資料庫..." -ForegroundColor Green
$createDbCmd = "docker exec $ContainerName psql -U postgres -c `"CREATE DATABASE $DBName OWNER $DBUser;`" 2>&1 || echo 'Database may already exist'"
try {
ssh ubuntu@$DBHost $createDbCmd
Write-Host "✓ 資料庫創建完成" -ForegroundColor Green
} catch {
Write-Host "⚠ 資料庫可能已存在" -ForegroundColor Yellow
}
Write-Host ""
# === 步驟 3: 授予權限 ===
Write-Host "步驟 3/4: 授予權限..." -ForegroundColor Green
$grantSQL = @"
GRANT ALL PRIVILEGES ON DATABASE $DBName TO $DBUser;
GRANT ALL PRIVILEGES ON SCHEMA public TO $DBUser;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $DBUser;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $DBUser;
"@
$grantCmd = "echo `"$grantSQL`" | docker exec -i $ContainerName psql -U postgres -d $DBName"
try {
ssh ubuntu@$DBHost $grantCmd
Write-Host "✓ 權限授予完成" -ForegroundColor Green
} catch {
Write-Host "✗ 權限授予失敗: $_" -ForegroundColor Red
}
Write-Host ""
# === 步驟 4: 執行 Schema ===
Write-Host "步驟 4/4: 執行資料庫 Schema..." -ForegroundColor Green
$schemaFile = Join-Path $PSScriptRoot "init-db.sql"
if (Test-Path $schemaFile) {
Write-Host " 讀取 Schema 檔案..." -ForegroundColor Gray
# 上傳 SQL 檔案到遠端
scp $schemaFile ubuntu@${DBHost}:/tmp/hr-portal-schema.sql
# 執行 SQL
$execSchemaCmd = "docker exec -i $ContainerName psql -U $DBUser -d $DBName < /tmp/hr-portal-schema.sql"
try {
ssh ubuntu@$DBHost $execSchemaCmd
Write-Host "✓ Schema 執行完成" -ForegroundColor Green
# 清理臨時檔案
ssh ubuntu@$DBHost "rm /tmp/hr-portal-schema.sql"
} catch {
Write-Host "✗ Schema 執行失敗: $_" -ForegroundColor Red
exit 1
}
} else {
Write-Host "✗ 找不到 Schema 檔案: $schemaFile" -ForegroundColor Red
exit 1
}
Write-Host ""
# === 驗證 ===
Write-Host "驗證資料庫設定..." -ForegroundColor Yellow
$verifyCmd = "docker exec $ContainerName psql -U $DBUser -d $DBName -c '\dt' 2>&1"
try {
$tables = ssh ubuntu@$DBHost $verifyCmd
if ($tables -match "business_units|employees|email_accounts") {
Write-Host "✓ 資料表已成功建立" -ForegroundColor Green
Write-Host ""
Write-Host "找到的資料表:" -ForegroundColor Cyan
Write-Host $tables -ForegroundColor Gray
} else {
Write-Host "⚠ 資料表可能未正確建立" -ForegroundColor Yellow
}
} catch {
Write-Host "⚠ 無法驗證資料表" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "=== 設定完成! ===" -ForegroundColor Green
Write-Host ""
# 顯示連接字串
Write-Host "資料庫連接資訊:" -ForegroundColor Cyan
Write-Host " Host: $DBHost"
Write-Host " Port: 5432"
Write-Host " Database: $DBName"
Write-Host " User: $DBUser"
Write-Host " Password: $DBPassword"
Write-Host ""
$databaseUrl = "postgresql://${DBUser}:${DBPassword}@${DBHost}:5432/${DBName}"
Write-Host "DATABASE_URL:" -ForegroundColor Yellow
Write-Host " $databaseUrl" -ForegroundColor White
Write-Host ""
Write-Host "下一步:" -ForegroundColor Green
Write-Host " 1. 複製上面的 DATABASE_URL"
Write-Host " 2. 編輯 backend\.env"
Write-Host " 3. 設定 DATABASE_URL 環境變數"
Write-Host " 4. 啟動後端: uvicorn app.main:app --reload"
Write-Host ""

View File

@@ -0,0 +1,172 @@
#!/bin/bash
#
# HR Portal Database Setup Script
# Run this script on Ubuntu Server (10.1.0.254)
#
# Usage: bash setup-hr-portal-db.sh
#
set -e
echo "========================================"
echo " HR Portal Database Setup"
echo "========================================"
echo ""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Configuration
DB_NAME="hr_portal"
DB_USER="hr_user"
DB_PASSWORD=""
POSTGRES_CONTAINER="postgres"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Step 1: Check PostgreSQL container
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}"
docker ps --filter "name=$POSTGRES_CONTAINER" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
else
echo -e "${RED} ✗ PostgreSQL container not found or not running${NC}"
echo ""
echo "Available containers:"
docker ps -a | grep -i postgres || echo "No PostgreSQL containers found"
echo ""
read -p "Enter PostgreSQL container name: " POSTGRES_CONTAINER
fi
echo ""
# Step 2: Prompt for passwords
echo -e "${YELLOW}[2/6] Setting up credentials...${NC}"
read -sp "Enter password for database user 'hr_user': " DB_PASSWORD
echo ""
if [ -z "$DB_PASSWORD" ]; then
echo -e "${RED} ✗ Password cannot be empty${NC}"
exit 1
fi
echo -e "${GREEN} ✓ Credentials configured${NC}"
echo ""
# Step 3: Create database user
echo -e "${YELLOW}[3/6] Creating database user '$DB_USER'...${NC}"
# Check if user exists
USER_EXISTS=$(docker exec -i $POSTGRES_CONTAINER psql -U postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" 2>&1)
if echo "$USER_EXISTS" | grep -q "1"; then
echo -e "${YELLOW} ⚠ User '$DB_USER' already exists, skipping...${NC}"
else
docker exec -i $POSTGRES_CONTAINER psql -U postgres <<EOF
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
ALTER USER $DB_USER WITH SUPERUSER;
EOF
if [ $? -eq 0 ]; then
echo -e "${GREEN} ✓ User '$DB_USER' created successfully${NC}"
else
echo -e "${RED} ✗ Failed to create user${NC}"
exit 1
fi
fi
echo ""
# Step 4: Create database
echo -e "${YELLOW}[4/6] Creating database '$DB_NAME'...${NC}"
# Check if database exists
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 " Do you want to drop and recreate it? (yes/no): " CONFIRM
if [ "$CONFIRM" = "yes" ]; then
echo " Dropping existing database..."
docker exec -i $POSTGRES_CONTAINER psql -U postgres -c "DROP DATABASE $DB_NAME;"
echo " Creating new database..."
docker exec -i $POSTGRES_CONTAINER psql -U postgres -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
echo -e "${GREEN} ✓ Database recreated${NC}"
else
echo -e "${YELLOW} ⚠ Skipping database creation${NC}"
fi
else
docker exec -i $POSTGRES_CONTAINER psql -U postgres <<EOF
CREATE DATABASE $DB_NAME OWNER $DB_USER;
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
EOF
if [ $? -eq 0 ]; then
echo -e "${GREEN} ✓ Database '$DB_NAME' created successfully${NC}"
else
echo -e "${RED} ✗ Failed to create database${NC}"
exit 1
fi
fi
echo ""
# Step 5: Initialize schema
echo -e "${YELLOW}[5/6] Initializing database schema...${NC}"
if [ -f "$SCRIPT_DIR/init-db.sql" ]; then
echo " Executing init-db.sql..."
docker exec -i $POSTGRES_CONTAINER psql -U $DB_USER -d $DB_NAME < "$SCRIPT_DIR/init-db.sql"
if [ $? -eq 0 ]; then
echo -e "${GREEN} ✓ Schema initialized successfully${NC}"
else
echo -e "${RED} ✗ Failed to initialize schema${NC}"
exit 1
fi
else
echo -e "${RED} ✗ init-db.sql not found in $SCRIPT_DIR${NC}"
exit 1
fi
echo ""
# Step 6: Verify setup
echo -e "${YELLOW}[6/6] Verifying database setup...${NC}"
# Count tables
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"
if [ "$TABLE_COUNT" -ge 9 ]; then
echo -e "${GREEN} ✓ Database setup verified${NC}"
else
echo -e "${YELLOW} ⚠ Expected 9 tables, found $TABLE_COUNT${NC}"
fi
# List tables
echo ""
echo " Tables in database:"
docker exec -i $POSTGRES_CONTAINER psql -U $DB_USER -d $DB_NAME -c "\dt"
echo ""
echo -e "${GREEN}========================================"
echo " Setup Completed Successfully!"
echo "========================================${NC}"
echo ""
echo "Database Connection String:"
echo -e "${CYAN}postgresql://$DB_USER:$DB_PASSWORD@10.1.0.254:5432/$DB_NAME${NC}"
echo ""
echo "Next steps:"
echo " 1. Update backend/.env with the connection string"
echo " 2. Run insert-test-data.sql to add sample data"
echo " 3. Test backend database connection"
echo ""

View File

@@ -0,0 +1,296 @@
#!/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 ""

119
scripts/setup-ssh-key.ps1 Normal file
View File

@@ -0,0 +1,119 @@
# Setup SSH Key-based Authentication
# This script configures passwordless SSH login to Ubuntu Server
$ErrorActionPreference = "Stop"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " SSH Key Setup for Ubuntu Server" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
$SSH_DIR = "$env:USERPROFILE\.ssh"
$SSH_KEY = "$SSH_DIR\id_rsa"
$SSH_PUB = "$SSH_KEY.pub"
$REMOTE_HOST = "ubuntu@10.1.0.254"
# Step 1: Check if SSH directory exists
Write-Host "[1/4] Checking SSH directory..." -ForegroundColor Yellow
if (-not (Test-Path $SSH_DIR)) {
Write-Host " Creating .ssh directory..." -ForegroundColor Gray
New-Item -ItemType Directory -Path $SSH_DIR -Force | Out-Null
Write-Host " [OK] Directory created" -ForegroundColor Green
} else {
Write-Host " [OK] Directory exists" -ForegroundColor Green
}
Write-Host ""
# Step 2: Generate SSH key if not exists
Write-Host "[2/4] Checking for SSH key..." -ForegroundColor Yellow
if (Test-Path $SSH_PUB) {
Write-Host " [OK] SSH key already exists" -ForegroundColor Green
Write-Host " Key location: $SSH_KEY" -ForegroundColor Gray
Write-Host ""
Write-Host " Public key content:" -ForegroundColor Gray
Get-Content $SSH_PUB | Write-Host -ForegroundColor White
} else {
Write-Host " Generating new SSH key pair..." -ForegroundColor Gray
Write-Host " (Press Enter when prompted for passphrase to skip)" -ForegroundColor Yellow
ssh-keygen -t rsa -b 4096 -f $SSH_KEY -C "windows-to-ubuntu"
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] SSH key generated successfully" -ForegroundColor Green
} else {
Write-Host " [FAIL] Failed to generate SSH key" -ForegroundColor Red
exit 1
}
}
Write-Host ""
# Step 3: Copy public key to remote server
Write-Host "[3/4] Copying public key to Ubuntu Server..." -ForegroundColor Yellow
Write-Host " You will need to enter the Ubuntu password ONE TIME" -ForegroundColor Yellow
Write-Host ""
try {
# Read public key content
$pubKeyContent = Get-Content $SSH_PUB -Raw
# Create command to add key to authorized_keys
$remoteCommand = @"
mkdir -p ~/.ssh && \
chmod 700 ~/.ssh && \
echo '$pubKeyContent' >> ~/.ssh/authorized_keys && \
chmod 600 ~/.ssh/authorized_keys && \
echo 'SSH key added successfully'
"@
# Execute remote command
Write-Host " Connecting to $REMOTE_HOST..." -ForegroundColor Gray
ssh $REMOTE_HOST $remoteCommand
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] Public key copied successfully" -ForegroundColor Green
} else {
Write-Host " [FAIL] Failed to copy public key" -ForegroundColor Red
exit 1
}
} catch {
Write-Host " [ERROR] $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host ""
# Step 4: Test passwordless login
Write-Host "[4/4] Testing passwordless SSH login..." -ForegroundColor Yellow
try {
$testResult = ssh -o BatchMode=yes -o ConnectTimeout=5 $REMOTE_HOST "echo 'Success'"
if ($LASTEXITCODE -eq 0 -and $testResult -eq "Success") {
Write-Host " [OK] Passwordless login works!" -ForegroundColor Green
} else {
Write-Host " [FAIL] Passwordless login test failed" -ForegroundColor Red
Write-Host " You may need to restart SSH service on Ubuntu Server" -ForegroundColor Yellow
Write-Host " Command: sudo systemctl restart ssh" -ForegroundColor Gray
exit 1
}
} catch {
Write-Host " [ERROR] Test failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Setup Completed!" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "You can now run commands without password:" -ForegroundColor Green
Write-Host " ssh $REMOTE_HOST 'docker ps'" -ForegroundColor White
Write-Host " ssh $REMOTE_HOST 'docker exec postgres psql -U postgres -l'" -ForegroundColor White
Write-Host ""
Write-Host "Next step:" -ForegroundColor Yellow
Write-Host " Run check-postgres.ps1 to verify PostgreSQL connection" -ForegroundColor White
Write-Host ""