릴리즈 v0.2.7: 빌드 오류 수정, supervisor 권한 보완 및 Gitea 설정 기능 고도화
This commit is contained in:
parent
98b52c390e
commit
bc996a3980
9
.gitignore
vendored
9
.gitignore
vendored
@ -22,14 +22,6 @@ dist-ssr
|
|||||||
build
|
build
|
||||||
out
|
out
|
||||||
|
|
||||||
# Environment Variables (CRITICAL)
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
@ -50,7 +42,6 @@ Desktop.ini
|
|||||||
server/uploads/*
|
server/uploads/*
|
||||||
!server/uploads/.gitkeep
|
!server/uploads/.gitkeep
|
||||||
server/server.zip
|
server/server.zip
|
||||||
server/public_key.pem
|
|
||||||
server/private_key.pem
|
server/private_key.pem
|
||||||
|
|
||||||
# Project Specific - Camera/Stream
|
# Project Specific - Camera/Stream
|
||||||
|
|||||||
@ -22,19 +22,22 @@ SmartIMS 배포는 **Git Tag 기반 배포** 방식을 권장합니다. 이는
|
|||||||
|
|
||||||
### 2.2. 설치 단계
|
### 2.2. 설치 단계
|
||||||
1. **소스 클론**:
|
1. **소스 클론**:
|
||||||
|
`/volume1/web` 위치에서 아래 명령어를 실행하면 `smartims` 폴더가 생성되며 그 안에 소스가 들어갑니다.
|
||||||
```bash
|
```bash
|
||||||
git clone [저장소_URL] smartims
|
cd /volume1/web
|
||||||
|
# 저장소 URL 뒤에 'smartims'를 붙여 폴더명을 지정합니다.
|
||||||
|
git clone https://gitea.qideun.com/SOKUREE/smart_ims.git smartims
|
||||||
cd smartims
|
cd smartims
|
||||||
```
|
```
|
||||||
2. **안정 버전(Tag) 전환**:
|
2. **안정 버전(Tag) 전환**:
|
||||||
```bash
|
```bash
|
||||||
git fetch --all --tags
|
git fetch --all --tags
|
||||||
# 현재 배포 가능한 최신 태그로 전환 (예: v0.2.5)
|
# v0.2.6 버전으로 전환
|
||||||
git checkout v0.2.5
|
git checkout v0.2.6
|
||||||
```
|
```
|
||||||
3. **환경 설정**:
|
3. **환경 설정**:
|
||||||
* `server/.env` 파일을 환경에 맞게 생성(DB 정보 등 입력).
|
* `server/.env` 파일을 환경에 맞게 생성(DB 정보 등 입력).
|
||||||
* `server/public_key.pem` 파일이 존재하는지 확인 (라이선스 검증용).
|
* `server/config/public_key.pem` 파일이 존재하는지 확인 (라이선스 검증용).
|
||||||
4. **패키지 설치 및 빌드**:
|
4. **패키지 설치 및 빌드**:
|
||||||
```bash
|
```bash
|
||||||
# 전체 의존성 설치 및 프론트엔드 빌드
|
# 전체 의존성 설치 및 프론트엔드 빌드
|
||||||
@ -71,17 +74,19 @@ npm run build
|
|||||||
pm2 reload smartims-api
|
pm2 reload smartims-api
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2. 시스템 관리 메뉴를 통한 업데이트 (검토 중)
|
### 3.2. 시스템 관리 메뉴를 통한 업데이트 (지원됨)
|
||||||
시스템의 "버전 정보" 화면에서 신규 버전을 감지하고 업데이트하는 기능을 검토 중입니다.
|
시스템의 **"시스템 관리 > 버전 정보"** 화면에서 신규 버전을 감지하고 원 클릭으로 업데이트할 수 있습니다.
|
||||||
|
|
||||||
|
* 참고: 이 기능을 사용하려면 **[기본 설정]** 메뉴에서 Gitea 원격 저장소 URL과 (필요 시) 계정 정보를 먼저 설정해야 합니다.
|
||||||
|
|
||||||
* **동작 원리**:
|
* **동작 원리**:
|
||||||
1. 서버가 원격 저장소의 최신 Tag 리스트를 정기적으로 또는 요청 시 확인합니다.
|
1. 서버가 설정된 원격 저장소의 최신 Tag 리스트를 확인합니다.
|
||||||
2. `package.json`의 현재 버전과 원격의 최신 Tag를 비교하여 업데이트 버튼을 활성화합니다.
|
2. `package.json`의 현재 버전과 원격의 최신 Tag를 비교합니다.
|
||||||
3. 업데이트 실행 시 서버 내부적으로 `git checkout` -> `npm install` -> `npm run build` -> `pm2 reload` 과정을 자동화된 스크립트로 수행합니다.
|
3. 업데이트 실행 시 서버가 백그라운드에서 `git checkout` -> `npm install` -> `npm run build` -> `pm2 reload` 과정을 자동으로 수행합니다.
|
||||||
|
|
||||||
* **기대 효과**:
|
* **기대 효과**:
|
||||||
* 터미널(SSH) 접속 없이 관리자 화면에서 즉시 최신 기능 반영 가능.
|
* 터미널(SSH) 접속 없이 관리자 화면에서 즉시 최신 기능 반영 가능.
|
||||||
* 버전 불일치 문제 예방 및 운영 편의성 증대.
|
* 운영 서버 환경에서도 편리한 버전 관리.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
23
server/.env
Normal file
23
server/.env
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# ==============================================
|
||||||
|
# [Common Settings]
|
||||||
|
# ==============================================
|
||||||
|
DB_HOST=sokuree.com
|
||||||
|
DB_USER=choibk
|
||||||
|
DB_PASSWORD=^Ocean1472bk
|
||||||
|
PORT=3005
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# [Development Environment] - Local Windows
|
||||||
|
# ==============================================
|
||||||
|
# 로컬 개발용 DB (분리됨: sokuree_platform_dev)
|
||||||
|
DB_NAME=sokuree_platform_dev
|
||||||
|
DB_PORT=3307
|
||||||
|
# Windows 환경 호환성 (tcp는 권한 오류 발생 가능)
|
||||||
|
CCTV_TRANSPORT_OVERRIDE=auto
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# [Production Environment] - Synology NAS
|
||||||
|
# ==============================================
|
||||||
|
# DB_NAME=sokuree_platform_prod
|
||||||
|
# DB_PORT=3307
|
||||||
|
|
||||||
@ -7,6 +7,7 @@ const fs = require('fs');
|
|||||||
const { isAuthenticated, hasRole } = require('../middleware/authMiddleware');
|
const { isAuthenticated, hasRole } = require('../middleware/authMiddleware');
|
||||||
const { generateLicense, verifyLicense } = require('../utils/licenseManager');
|
const { generateLicense, verifyLicense } = require('../utils/licenseManager');
|
||||||
const { checkRemoteKey } = require('../utils/remoteLicense');
|
const { checkRemoteKey } = require('../utils/remoteLicense');
|
||||||
|
const cryptoUtil = require('../utils/cryptoUtil');
|
||||||
|
|
||||||
// Load Public Key for Verification
|
// Load Public Key for Verification
|
||||||
const publicKeyPath = path.join(__dirname, '../config/public_key.pem');
|
const publicKeyPath = path.join(__dirname, '../config/public_key.pem');
|
||||||
@ -32,7 +33,10 @@ const ALLOWED_SETTING_KEYS = [
|
|||||||
'asset_categories',
|
'asset_categories',
|
||||||
'asset_locations',
|
'asset_locations',
|
||||||
'asset_statuses',
|
'asset_statuses',
|
||||||
'asset_maintenance_types'
|
'asset_maintenance_types',
|
||||||
|
'gitea_url',
|
||||||
|
'gitea_user',
|
||||||
|
'gitea_password'
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- .env File Utilities ---
|
// --- .env File Utilities ---
|
||||||
@ -70,7 +74,7 @@ const mysql = require('mysql2/promise');
|
|||||||
// 0. Server Configuration (Subscriber ID & Session Timeout)
|
// 0. Server Configuration (Subscriber ID & Session Timeout)
|
||||||
router.get('/settings', isAuthenticated, hasRole('admin'), async (req, res) => {
|
router.get('/settings', isAuthenticated, hasRole('admin'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.query("SELECT setting_key, setting_value FROM system_settings WHERE setting_key IN ('subscriber_id', 'session_timeout', 'encryption_key')");
|
const [rows] = await db.query("SELECT setting_key, setting_value FROM system_settings WHERE setting_key IN ('subscriber_id', 'session_timeout', 'encryption_key', 'gitea_url', 'gitea_user', 'gitea_password')");
|
||||||
const settings = {};
|
const settings = {};
|
||||||
rows.forEach(r => settings[r.setting_key] = r.setting_value);
|
rows.forEach(r => settings[r.setting_key] = r.setting_value);
|
||||||
|
|
||||||
@ -81,6 +85,9 @@ router.get('/settings', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|||||||
subscriber_id: settings.subscriber_id || '',
|
subscriber_id: settings.subscriber_id || '',
|
||||||
session_timeout: parseInt(settings.session_timeout) || 60,
|
session_timeout: parseInt(settings.session_timeout) || 60,
|
||||||
encryption_key: settings.encryption_key || '',
|
encryption_key: settings.encryption_key || '',
|
||||||
|
gitea_url: settings.gitea_url || 'https://gitea.qideun.com/SOKUREE/smart_ims.git',
|
||||||
|
gitea_user: settings.gitea_user || '',
|
||||||
|
gitea_password: settings.gitea_password ? cryptoUtil.decryptMasterKey(settings.gitea_password) : '',
|
||||||
db_config: {
|
db_config: {
|
||||||
host: env.DB_HOST || '',
|
host: env.DB_HOST || '',
|
||||||
user: env.DB_USER || '',
|
user: env.DB_USER || '',
|
||||||
@ -109,6 +116,16 @@ router.post('/settings', isAuthenticated, hasRole('admin'), async (req, res) =>
|
|||||||
const encryptedKeyForDb = cryptoUtil.encryptMasterKey(encryption_key);
|
const encryptedKeyForDb = cryptoUtil.encryptMasterKey(encryption_key);
|
||||||
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('encryption_key', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [encryptedKeyForDb]);
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('encryption_key', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [encryptedKeyForDb]);
|
||||||
}
|
}
|
||||||
|
if (req.body.gitea_url !== undefined) {
|
||||||
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('gitea_url', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [req.body.gitea_url]);
|
||||||
|
}
|
||||||
|
if (req.body.gitea_user !== undefined) {
|
||||||
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('gitea_user', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [req.body.gitea_user]);
|
||||||
|
}
|
||||||
|
if (req.body.gitea_password !== undefined) {
|
||||||
|
const encryptedPass = cryptoUtil.encryptMasterKey(req.body.gitea_password);
|
||||||
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('gitea_password', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [encryptedPass]);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle .env DB settings
|
// Handle .env DB settings
|
||||||
if (db_config) {
|
if (db_config) {
|
||||||
@ -129,7 +146,6 @@ router.post('/settings', isAuthenticated, hasRole('admin'), async (req, res) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Crypto & Key Rotation ---
|
// --- Crypto & Key Rotation ---
|
||||||
const cryptoUtil = require('../utils/cryptoUtil');
|
|
||||||
|
|
||||||
// 0.2 Test DB Connection
|
// 0.2 Test DB Connection
|
||||||
router.post('/test-db', isAuthenticated, hasRole('admin'), async (req, res) => {
|
router.post('/test-db', isAuthenticated, hasRole('admin'), async (req, res) => {
|
||||||
@ -234,8 +250,8 @@ router.post('/settings/:key', isAuthenticated, hasRole('admin'), async (req, res
|
|||||||
try {
|
try {
|
||||||
let stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
let stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
||||||
|
|
||||||
// Special handling for encryption_key to protect it in DB
|
// Special handling for sensitive keys to protect it in DB
|
||||||
if (key === 'encryption_key') {
|
if (key === 'encryption_key' || key === 'gitea_password') {
|
||||||
stringValue = cryptoUtil.encryptMasterKey(stringValue);
|
stringValue = cryptoUtil.encryptMasterKey(stringValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,6 +443,25 @@ router.post('/modules/:code/deactivate', isAuthenticated, hasRole('admin'), asyn
|
|||||||
|
|
||||||
// --- System Update Logic ---
|
// --- System Update Logic ---
|
||||||
|
|
||||||
|
const getGiteaAuth = async () => {
|
||||||
|
try {
|
||||||
|
const [rows] = await db.query("SELECT setting_key, setting_value FROM system_settings WHERE setting_key IN ('gitea_url', 'gitea_user', 'gitea_password')");
|
||||||
|
const settings = {};
|
||||||
|
rows.forEach(r => settings[r.setting_key] = r.setting_value);
|
||||||
|
|
||||||
|
const url = settings.gitea_url || 'https://gitea.qideun.com/SOKUREE/smart_ims.git';
|
||||||
|
|
||||||
|
if (settings.gitea_user && settings.gitea_password) {
|
||||||
|
const pass = cryptoUtil.decryptMasterKey(settings.gitea_password);
|
||||||
|
return { url, user: settings.gitea_user, pass: pass };
|
||||||
|
}
|
||||||
|
return { url, user: null, pass: null };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get Gitea auth:', e);
|
||||||
|
}
|
||||||
|
return { url: 'https://gitea.qideun.com/SOKUREE/smart_ims.git', user: null, pass: null };
|
||||||
|
};
|
||||||
|
|
||||||
// 5. Get Version Info (Current & Remote)
|
// 5. Get Version Info (Current & Remote)
|
||||||
router.get('/version/remote', isAuthenticated, hasRole('admin'), async (req, res) => {
|
router.get('/version/remote', isAuthenticated, hasRole('admin'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -434,15 +469,27 @@ router.get('/version/remote', isAuthenticated, hasRole('admin'), async (req, res
|
|||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
const currentVersion = packageJson.version;
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
// Run git fetch to update tags from remote
|
// Prepare git fetch command with auth if available
|
||||||
exec('git fetch --tags', (err, stdout, stderr) => {
|
const auth = await getGiteaAuth();
|
||||||
|
let fetchCmd = 'git fetch --tags';
|
||||||
|
|
||||||
|
if (auth.user && auth.pass) {
|
||||||
|
// Inject auth into URL
|
||||||
|
const authenticatedUrl = auth.url.replace('https://', `https://${encodeURIComponent(auth.user)}:${encodeURIComponent(auth.pass)}@`);
|
||||||
|
fetchCmd = `git fetch ${authenticatedUrl} --tags`;
|
||||||
|
} else {
|
||||||
|
fetchCmd = `git fetch ${auth.url} --tags`;
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(fetchCmd, (err, stdout, stderr) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Git fetch failed:', err);
|
console.error('Git fetch failed:', err);
|
||||||
console.error('Stderr:', stderr);
|
// Mask password in error message
|
||||||
|
const sanitizedError = stderr.replace(/:[^@]+@/g, ':****@');
|
||||||
return res.json({
|
return res.json({
|
||||||
current: currentVersion,
|
current: currentVersion,
|
||||||
latest: null,
|
latest: null,
|
||||||
error: `원격 저장소 동기화 실패: ${stderr || err.message}`
|
error: `원격 저장소 동기화 실패: ${sanitizedError || err.message}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,7 +532,18 @@ router.post('/version/update', isAuthenticated, hasRole('admin'), async (req, re
|
|||||||
// This operation is asynchronous. We start it and return a message.
|
// This operation is asynchronous. We start it and return a message.
|
||||||
// In a real production, we might want to log this to a terminal-like view.
|
// In a real production, we might want to log this to a terminal-like view.
|
||||||
|
|
||||||
|
const auth = await getGiteaAuth();
|
||||||
|
let authPrefix = '';
|
||||||
|
if (auth.user && auth.pass) {
|
||||||
|
const authenticatedUrl = auth.url.replace('https://', `https://${encodeURIComponent(auth.user)}:${encodeURIComponent(auth.pass)}@`);
|
||||||
|
authPrefix = `git remote set-url origin ${authenticatedUrl} && `;
|
||||||
|
} else {
|
||||||
|
authPrefix = `git remote set-url origin ${auth.url} && `;
|
||||||
|
}
|
||||||
|
|
||||||
const updateScript = `
|
const updateScript = `
|
||||||
|
${authPrefix}
|
||||||
|
git fetch --tags &&
|
||||||
git checkout ${targetTag} &&
|
git checkout ${targetTag} &&
|
||||||
npm install &&
|
npm install &&
|
||||||
npm run build &&
|
npm run build &&
|
||||||
|
|||||||
@ -1,23 +1,27 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { ModuleGuard } from '../shared/auth/ModuleGuard';
|
import { AuthProvider } from '../shared/auth/AuthContext';
|
||||||
|
import { SystemProvider } from '../shared/context/SystemContext';
|
||||||
|
import { AuthGuard } from '../shared/auth/AuthGuard';
|
||||||
import { MainLayout } from '../widgets/layout/MainLayout';
|
import { MainLayout } from '../widgets/layout/MainLayout';
|
||||||
import { LoginPage } from '../pages/auth/LoginPage';
|
import { LoginPage } from '../pages/auth/LoginPage';
|
||||||
import { DashboardPage } from '../modules/asset/pages/DashboardPage';
|
|
||||||
import { AssetListPage } from '../modules/asset/pages/AssetListPage';
|
// Modules
|
||||||
import { AssetRegisterPage } from '../modules/asset/pages/AssetRegisterPage';
|
import { assetModule } from '../modules/asset/module';
|
||||||
import { AssetSettingsPage } from '../modules/asset/pages/AssetSettingsPage';
|
import { cctvModule } from '../modules/cctv/module';
|
||||||
import { AssetDetailPage } from '../modules/asset/pages/AssetDetailPage';
|
import { productionModule } from '../modules/production/module';
|
||||||
|
import ModuleLoader from '../platform/ModuleLoader';
|
||||||
|
|
||||||
|
// Platform / System Pages
|
||||||
import { UserManagementPage } from '../platform/pages/UserManagementPage';
|
import { UserManagementPage } from '../platform/pages/UserManagementPage';
|
||||||
import { BasicSettingsPage } from '../platform/pages/BasicSettingsPage';
|
import { BasicSettingsPage } from '../platform/pages/BasicSettingsPage';
|
||||||
import { ProductionPage } from '../production/pages/ProductionPage';
|
import { VersionPage } from '../platform/pages/VersionPage';
|
||||||
import { MonitoringPage } from '../modules/cctv/pages/MonitoringPage';
|
|
||||||
import { AuthProvider } from '../shared/auth/AuthContext';
|
|
||||||
import { AuthGuard } from '../shared/auth/AuthGuard';
|
|
||||||
|
|
||||||
import { SystemProvider } from '../shared/context/SystemContext';
|
|
||||||
import { LicensePage } from '../system/pages/LicensePage';
|
import { LicensePage } from '../system/pages/LicensePage';
|
||||||
|
|
||||||
function App() {
|
import '../platform/styles/global.css';
|
||||||
|
|
||||||
|
const modules = [assetModule, cctvModule, productionModule];
|
||||||
|
|
||||||
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SystemProvider>
|
<SystemProvider>
|
||||||
@ -29,39 +33,20 @@ function App() {
|
|||||||
{/* Protected Routes */}
|
{/* Protected Routes */}
|
||||||
<Route element={
|
<Route element={
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<MainLayout />
|
<MainLayout modulesList={modules} />
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
}>
|
}>
|
||||||
{/* Asset Management Routes */}
|
{/* Dynamic Module Routes */}
|
||||||
<Route element={<ModuleGuard moduleCode="asset"><Outlet /></ModuleGuard>}>
|
<Route path="/*" element={<ModuleLoader modules={modules} />} />
|
||||||
<Route path="/" element={<Navigate to="/asset/dashboard" replace />} />
|
|
||||||
<Route path="/asset/dashboard" element={<DashboardPage />} />
|
|
||||||
<Route path="/asset/register" element={<AssetRegisterPage />} />
|
|
||||||
<Route path="/asset/list" element={<AssetListPage />} />
|
|
||||||
<Route path="/asset/facilities" element={<AssetListPage />} />
|
|
||||||
<Route path="/asset/tools" element={<AssetListPage />} />
|
|
||||||
<Route path="/asset/general" element={<AssetListPage />} />
|
|
||||||
<Route path="/asset/settings" element={<AssetSettingsPage />} />
|
|
||||||
<Route path="/asset/detail/:assetId" element={<AssetDetailPage />} />
|
|
||||||
<Route path="/asset/consumables" element={<AssetListPage />} />
|
|
||||||
<Route path="/asset/instruments" element={<AssetListPage />} />
|
|
||||||
<Route path="/asset/vehicles" element={<AssetListPage />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Production Management Routes */}
|
{/* Navigation Fallback within Layout */}
|
||||||
<Route element={<ModuleGuard moduleCode="production"><Outlet /></ModuleGuard>}>
|
<Route index element={<Navigate to="/asset/dashboard" replace />} />
|
||||||
<Route path="/production/dashboard" element={<ProductionPage />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Monitoring Routes */}
|
{/* Platform Admin Routes */}
|
||||||
<Route element={<ModuleGuard moduleCode="monitoring"><Outlet /></ModuleGuard>}>
|
|
||||||
<Route path="/monitoring" element={<MonitoringPage />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Admin Routes */}
|
|
||||||
<Route path="/admin/users" element={<UserManagementPage />} />
|
<Route path="/admin/users" element={<UserManagementPage />} />
|
||||||
<Route path="/admin/settings" element={<BasicSettingsPage />} />
|
<Route path="/admin/settings" element={<BasicSettingsPage />} />
|
||||||
<Route path="/admin/license" element={<LicensePage />} />
|
<Route path="/admin/license" element={<LicensePage />} />
|
||||||
|
<Route path="/admin/version" element={<VersionPage />} />
|
||||||
<Route path="/admin" element={<Navigate to="/admin/settings" replace />} />
|
<Route path="/admin" element={<Navigate to="/admin/settings" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|||||||
@ -33,9 +33,12 @@ export const ModuleLoader = ({ modules }: ModuleLoaderProps) => {
|
|||||||
|
|
||||||
if (!isActive) return null;
|
if (!isActive) return null;
|
||||||
|
|
||||||
// 2. 沅뚰븳 泥댄겕 (Role 湲곕컲)
|
// 2. 권한 체크 (Role 기반)
|
||||||
if (module.requiredRoles && user && !module.requiredRoles.includes(user.role)) {
|
if (module.requiredRoles && user) {
|
||||||
return null;
|
// supervisor는 모든 권한을 가짐
|
||||||
|
if (user.role !== 'supervisor' && !module.requiredRoles.includes(user.role)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -10,6 +10,9 @@ interface SystemSettings {
|
|||||||
session_timeout: number;
|
session_timeout: number;
|
||||||
encryption_key: string;
|
encryption_key: string;
|
||||||
subscriber_id: string;
|
subscriber_id: string;
|
||||||
|
gitea_url: string;
|
||||||
|
gitea_user: string;
|
||||||
|
gitea_password: string;
|
||||||
db_config: {
|
db_config: {
|
||||||
host: string;
|
host: string;
|
||||||
user: string;
|
user: string;
|
||||||
@ -25,6 +28,9 @@ export function BasicSettingsPage() {
|
|||||||
session_timeout: 60,
|
session_timeout: 60,
|
||||||
encryption_key: '',
|
encryption_key: '',
|
||||||
subscriber_id: '',
|
subscriber_id: '',
|
||||||
|
gitea_url: '',
|
||||||
|
gitea_user: '',
|
||||||
|
gitea_password: '',
|
||||||
db_config: {
|
db_config: {
|
||||||
host: '',
|
host: '',
|
||||||
user: '',
|
user: '',
|
||||||
@ -39,6 +45,7 @@ export function BasicSettingsPage() {
|
|||||||
const [saveResults, setSaveResults] = useState<{ [key: string]: { success: boolean; message: string } | null }>({
|
const [saveResults, setSaveResults] = useState<{ [key: string]: { success: boolean; message: string } | null }>({
|
||||||
security: null,
|
security: null,
|
||||||
encryption: null,
|
encryption: null,
|
||||||
|
repository: null,
|
||||||
database: null
|
database: null
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -176,7 +183,7 @@ export function BasicSettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveSection = async (section: 'security' | 'encryption' | 'database') => {
|
const handleSaveSection = async (section: 'security' | 'encryption' | 'database' | 'repository') => {
|
||||||
if (section === 'database' && !isDbVerified) {
|
if (section === 'database' && !isDbVerified) {
|
||||||
alert('DB 접속 정보가 변경되었습니다. 저장 전 반드시 [연결 테스트]를 수행하십시오.');
|
alert('DB 접속 정보가 변경되었습니다. 저장 전 반드시 [연결 테스트]를 수행하십시오.');
|
||||||
return;
|
return;
|
||||||
@ -187,6 +194,11 @@ export function BasicSettingsPage() {
|
|||||||
|
|
||||||
if (section === 'security') payload = { session_timeout: settings.session_timeout };
|
if (section === 'security') payload = { session_timeout: settings.session_timeout };
|
||||||
else if (section === 'encryption') payload = { encryption_key: settings.encryption_key };
|
else if (section === 'encryption') payload = { encryption_key: settings.encryption_key };
|
||||||
|
else if (section === 'repository') payload = {
|
||||||
|
gitea_url: settings.gitea_url,
|
||||||
|
gitea_user: settings.gitea_user,
|
||||||
|
gitea_password: settings.gitea_password
|
||||||
|
};
|
||||||
else if (section === 'database') {
|
else if (section === 'database') {
|
||||||
if (!confirm('DB 설정을 저장하면 서버가 재시작되어 접속이 끊길 수 있습니다. 진행하시겠습니까?')) return;
|
if (!confirm('DB 설정을 저장하면 서버가 재시작되어 접속이 끊길 수 있습니다. 진행하시겠습니까?')) return;
|
||||||
payload = { db_config: settings.db_config };
|
payload = { db_config: settings.db_config };
|
||||||
@ -351,6 +363,62 @@ export function BasicSettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Section 2.5: Gitea Repository Update Settings */}
|
||||||
|
<Card className="overflow-hidden border-slate-200 shadow-sm">
|
||||||
|
<div className="p-6 border-b border-slate-50 bg-slate-50/50">
|
||||||
|
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
||||||
|
<RefreshCcw size={20} className="text-emerald-600" />
|
||||||
|
시스템 업데이트 저장소 설정 (Gitea)
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-xs text-slate-500 mb-4">원격 저장소(Gitea)에서 최신 업데이트 태그를 가져오기 위한 주소 및 인증 정보를 설정합니다.</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-slate-700">저장소 URL (Git 주소)</label>
|
||||||
|
<Input
|
||||||
|
value={settings.gitea_url}
|
||||||
|
onChange={e => setSettings({ ...settings, gitea_url: e.target.value })}
|
||||||
|
placeholder="https://gitea.example.com/org/repo.git"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-slate-700">Gitea 접속 ID</label>
|
||||||
|
<Input
|
||||||
|
value={settings.gitea_user}
|
||||||
|
onChange={e => setSettings({ ...settings, gitea_user: e.target.value })}
|
||||||
|
placeholder="gitea_update_user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-slate-700">Gitea 비밀번호 (또는 Token)</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={settings.gitea_password}
|
||||||
|
onChange={e => setSettings({ ...settings, gitea_password: e.target.value })}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-6 py-4 bg-slate-50/30 border-t border-slate-50 flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
{saveResults.repository && (
|
||||||
|
<div className={`flex items-center gap-2 text-sm font-medium ${saveResults.repository.success ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{saveResults.repository.success ? <CheckCircle2 size={16} /> : <AlertCircle size={16} />}
|
||||||
|
{saveResults.repository.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="secondary" size="sm" onClick={fetchSettings}>취소</Button>
|
||||||
|
<Button size="sm" onClick={() => handleSaveSection('repository')} disabled={saving} icon={<Save size={14} />}>계정 저장</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Section 3: Database Infrastructure (Supervisor Protected) */}
|
{/* Section 3: Database Infrastructure (Supervisor Protected) */}
|
||||||
<Card className="overflow-hidden border-slate-200 shadow-sm border-2 border-slate-100">
|
<Card className="overflow-hidden border-slate-200 shadow-sm border-2 border-slate-100">
|
||||||
<div className="p-6 border-b border-slate-50 bg-slate-100/50 flex justify-between items-center">
|
<div className="p-6 border-b border-slate-50 bg-slate-100/50 flex justify-between items-center">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user