Feat: Add authentication system and comprehensive usage/CLI documentation
This commit is contained in:
parent
ac92702af1
commit
d8c8227c40
@ -1,6 +1,10 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Key, Plus, Copy, CheckCircle, Clock, Search, Users, ChevronRight, ArrowLeft, Trash2 } from 'lucide-react';
|
||||
import { Key, Plus, Copy, CheckCircle, Clock, Search, Users, ChevronRight, ArrowLeft, Trash2, LogOut } from 'lucide-react';
|
||||
import Login from './components/Login';
|
||||
|
||||
// Axios settings for cookies
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
const API_BASE = import.meta.env.DEV
|
||||
? 'http://localhost:3006/api'
|
||||
@ -16,7 +20,9 @@ interface License {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const App = () => {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [authChecked, setAuthChecked] = useState(false);
|
||||
const [licenses, setLicenses] = useState<License[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@ -38,18 +44,18 @@ function App() {
|
||||
|
||||
// Calculate unique subscribers for the master list
|
||||
const allSubscribers = useMemo(() => {
|
||||
return Array.from(new Set(licenses.map(lic => lic.subscriber_id))).sort();
|
||||
return Array.from(new Set(licenses.map((lic: License) => lic.subscriber_id))).sort();
|
||||
}, [licenses]);
|
||||
|
||||
// Filtered subscribers based on applied search
|
||||
const filteredSubscribers = useMemo(() => {
|
||||
if (!appliedSearch.trim()) return allSubscribers;
|
||||
return allSubscribers.filter(sub => sub.toUpperCase().includes(appliedSearch.toUpperCase()));
|
||||
return allSubscribers.filter((sub: string) => sub.toUpperCase().includes(appliedSearch.toUpperCase()));
|
||||
}, [allSubscribers, appliedSearch]);
|
||||
|
||||
// Detail view licenses
|
||||
const detailLicenses = useMemo(() => {
|
||||
return licenses.filter(lic => lic.subscriber_id === selectedSubscriber);
|
||||
return licenses.filter((lic: License) => lic.subscriber_id === selectedSubscriber);
|
||||
}, [licenses, selectedSubscriber]);
|
||||
|
||||
const handleSearch = (e?: React.FormEvent) => {
|
||||
@ -78,9 +84,36 @@ function App() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLicenses();
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_BASE}/auth/me`);
|
||||
setUser(res.data);
|
||||
fetchLicenses();
|
||||
} catch (err) {
|
||||
setUser(null);
|
||||
} finally {
|
||||
setAuthChecked(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await axios.post(`${API_BASE}/auth/logout`);
|
||||
setUser(null);
|
||||
} catch (err) {
|
||||
console.error('Logout failed', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchLicenses();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchLicenses = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@ -140,6 +173,12 @@ function App() {
|
||||
|
||||
|
||||
|
||||
if (!authChecked) return null; // Or a loader
|
||||
|
||||
if (!user) {
|
||||
return <Login onLoginSuccess={(userData) => setUser(userData)} apiBase={API_BASE} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col font-['Pretendard','sans-serif'] text-slate-800">
|
||||
<header className="bg-white border-b sticky top-0 z-20 shadow-sm">
|
||||
@ -153,9 +192,22 @@ function App() {
|
||||
<p className="text-[11px] text-slate-400 font-medium">플랫폼 라이선스 통합 관리</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={fetchLicenses} className="p-2 hover:bg-slate-100 rounded-full transition-all">
|
||||
<Clock className={`w-5 h-5 text-slate-400 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center font-bold text-slate-500 text-xs">
|
||||
{user?.name?.[0] || 'A'}
|
||||
</div>
|
||||
<span className="text-sm font-bold text-slate-700">{user?.name}님</span>
|
||||
</div>
|
||||
<button onClick={handleLogout} className="flex items-center gap-2 text-slate-400 hover:text-red-500 transition-all group">
|
||||
<span className="text-xs font-bold">로그아웃</span>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="w-px h-4 bg-slate-200" />
|
||||
<button onClick={fetchLicenses} className="p-2 hover:bg-slate-100 rounded-full transition-all">
|
||||
<Clock className={`w-5 h-5 text-slate-400 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -246,7 +298,7 @@ function App() {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{filteredSubscribers.length > 0 ? (
|
||||
filteredSubscribers.map((sub, idx) => (
|
||||
filteredSubscribers.map((sub: string, idx: number) => (
|
||||
<tr key={sub} className="hover:bg-[#F8FAFF] group transition-colors">
|
||||
<td className="px-8 py-5 text-xs font-mono text-slate-400 text-center">{idx + 1}</td>
|
||||
<td className="px-8 py-5 text-sm font-bold text-slate-800">{sub}</td>
|
||||
@ -297,7 +349,7 @@ function App() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{detailLicenses.map((lic, idx) => (
|
||||
{detailLicenses.map((lic: License, idx: number) => (
|
||||
<tr key={lic.id} className="hover:bg-[#F8FAFF] transition-colors">
|
||||
<td className="px-8 py-6 text-xs font-mono text-slate-400 text-center">{idx + 1}</td>
|
||||
<td className="px-8 py-6">{getStatusBadge(lic.status)}</td>
|
||||
|
||||
113
client/src/components/Login.tsx
Normal file
113
client/src/components/Login.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Key, Lock, User, AlertCircle, Loader2 } from 'lucide-react';
|
||||
|
||||
interface LoginProps {
|
||||
onLoginSuccess: (user: any) => void;
|
||||
apiBase: string;
|
||||
}
|
||||
|
||||
const Login: React.FC<LoginProps> = ({ onLoginSuccess, apiBase }) => {
|
||||
const [loginId, setLoginId] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await axios.post(`${apiBase}/auth/login`, { loginId, password });
|
||||
if (res.data.success) {
|
||||
onLoginSuccess(res.data.user);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || '로그인에 실패했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex items-center justify-center font-['Pretendard','sans-serif'] p-4">
|
||||
<div className="max-w-md w-full bg-white rounded-3xl shadow-xl border border-slate-200 overflow-hidden transform transition-all">
|
||||
<div className="p-8">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="bg-indigo-600 p-4 rounded-2xl shadow-lg shadow-indigo-200 mb-4">
|
||||
<Key className="text-white w-8 h-8" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-black text-slate-800 tracking-tight">License Manager</h1>
|
||||
<p className="text-sm text-slate-400 font-medium">관리자 계정으로 로그인해 주세요</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-bold text-slate-400 uppercase tracking-wider ml-1">아이디</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-300 group-focus-within:text-indigo-500 transition-colors">
|
||||
<User size={18} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={loginId}
|
||||
onChange={(e) => setLoginId(e.target.value)}
|
||||
placeholder="Admin ID"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-slate-50 border border-slate-100 rounded-2xl focus:bg-white focus:ring-4 focus:ring-indigo-50 focus:border-indigo-500 outline-none transition-all text-sm font-medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-bold text-slate-400 uppercase tracking-wider ml-1">비밀번호</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-300 group-focus-within:text-indigo-500 transition-colors">
|
||||
<Lock size={18} />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-slate-50 border border-slate-100 rounded-2xl focus:bg-white focus:ring-4 focus:ring-indigo-50 focus:border-indigo-500 outline-none transition-all text-sm font-medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3.5 bg-red-50 text-red-600 rounded-xl animate-shake">
|
||||
<AlertCircle size={16} className="shrink-0" />
|
||||
<span className="text-xs font-bold">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-slate-900 text-white font-black py-4 rounded-2xl hover:bg-indigo-600 active:scale-[0.98] transition-all shadow-lg shadow-slate-200 disabled:opacity-50 disabled:scale-100 flex items-center justify-center gap-2 mt-4"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<span>로그인 중...</span>
|
||||
</>
|
||||
) : (
|
||||
<span>시스템 접속</span>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-6 text-center border-t border-slate-100">
|
||||
<p className="text-[11px] text-slate-400 font-medium tracking-wide">
|
||||
© 2024 Smart IMS Platform. All Rights Reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@ -37,6 +37,7 @@ npm run build
|
||||
### 3. 서버 환경 설정
|
||||
`server/.env` 파일을 NAS 운영 환경에 맞게 수정합니다.
|
||||
- `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` 설정
|
||||
- `JWT_SECRET`: 보안을 위한 임의의 긴 문자열 입력
|
||||
- 포트 번호 확인 (기본 `3006`)
|
||||
|
||||
### 4. 의존성 설치 및 실행
|
||||
|
||||
@ -19,6 +19,10 @@
|
||||
- `/server`: Express 기반 백엔드 API 서버
|
||||
- `/config`: 암호화 키(`private_key.pem`, `public_key.pem`) 보관
|
||||
- `/docs`: 배포 및 운영 관련 문서
|
||||
- `overview.md`: 시스템 개요
|
||||
- `deployment.md`: NAS 배포 가이드 (PM2/작업스케줄러 포함)
|
||||
- `usage.md`: 웹 UI 사용법 및 CLI 명령어 가이드
|
||||
|
||||
## 시작하기
|
||||
상세한 설치 및 배포 방법은 [배포 가이드](./deployment.md)를 참조하세요.
|
||||
- 설치 및 배포: [배포 가이드](./deployment.md)
|
||||
- 시스템 사용 및 명령어: [사용 가이드](./usage.md)
|
||||
|
||||
81
docs/usage.md
Normal file
81
docs/usage.md
Normal file
@ -0,0 +1,81 @@
|
||||
# 라이선스 관리 시스템 사용 가이드 (Usage Guide)
|
||||
|
||||
이 문서는 라이선스 관리 시스템을 웹 UI 및 터미널(CLI)에서 사용하는 방법을 설명합니다.
|
||||
|
||||
## 1. 웹 관리 UI 사용법
|
||||
|
||||
시스템 배포 후 브라우저를 통해 접속하여 라이선스를 관리할 수 있습니다.
|
||||
|
||||
### 로그인
|
||||
- **주소**: `http://[NAS-IP 또는 도메인]:3006`
|
||||
- **초기 계정**: `admin` / `^Ocean1472bk`
|
||||
|
||||
### 라이선스 신규 발급
|
||||
1. 왼쪽 **'라이선스 신규 발급'** 패널에서 정보를 입력합니다.
|
||||
- **모듈 시스템**: 자산관리, 생산관리, 모니터링 중 선택
|
||||
- **유형**: SUB(구독형), DEMO(체험판), DEV(영구 프로젝트용)
|
||||
- **구독자 ID**: 고객사 식별 ID (예: `SOKUREE-2024-01`)
|
||||
- **만기 일자**: 라이선스 종료일 지정
|
||||
2. **'라이선스 발급'** 버튼을 클릭합니다.
|
||||
|
||||
### 라이선스 관리 및 복사
|
||||
- **검색**: 상단 검색바를 통해 특정 구독자 ID를 조회할 수 있습니다.
|
||||
- **상태 확인**: `등록 대기`(발급됨), `활성화됨`(Smart IMS 등록됨) 상태를 확인합니다.
|
||||
- **키 복사**: 발급된 라이선스 키의 아이콘을 클릭하여 클립보드에 복사할 수 있습니다.
|
||||
- **삭제**: 필요 없는 라이선스는 쓰레기통 아이콘으로 삭제 가능합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 터미널(CLI) 작업 가이드
|
||||
|
||||
개발자 또는 관리자가 NAS 터미널(SSH)에서 시스템을 점검하거나 제어할 때 사용합니다.
|
||||
|
||||
### 작업 위치
|
||||
모든 작업은 프로젝트 루트 디렉토리 및 서버 디렉토리에서 수행합니다.
|
||||
- **기본 위치**: `/volume1/[사용자폴더]/smart_ims_license`
|
||||
- **서버 소스**: `/volume1/[사용자폴더]/smart_ims_license/server`
|
||||
|
||||
### 주요 관리 명령어
|
||||
|
||||
#### 서비스 상태 확인 (PM2)
|
||||
```bash
|
||||
# 전체 서비스 리스트 확인
|
||||
pm2 list
|
||||
|
||||
# 실시간 로그 모니터링
|
||||
pm2 logs license-manager
|
||||
|
||||
# 서비스 재시작 (코드 수정 반영 등)
|
||||
pm2 restart license-manager
|
||||
```
|
||||
|
||||
#### DB 초기화 및 관리
|
||||
수동으로 DB를 초기화하거나 스키마를 업데이트해야 할 경우 사용합니다.
|
||||
(작업 위치: `server/`)
|
||||
```bash
|
||||
# DB 스키마 생성 및 초기 관리자 생성
|
||||
node init_db.js
|
||||
|
||||
# 테스트 데이터 초기화 (주의: 기존 데이터 삭제될 수 있음)
|
||||
node reset_test_data.js
|
||||
```
|
||||
|
||||
#### 환경 변수 수정
|
||||
DB 접속 정보나 포트를 변경해야 할 경우:
|
||||
```bash
|
||||
# vi 또는 nano 편집기로 .env 수정
|
||||
vi .env
|
||||
```
|
||||
*(수정 후 `pm2 restart license-manager` 필수)*
|
||||
|
||||
### 수동 라이선스 생성 (CLI 도구)
|
||||
웹 UI 없이 터미널에서 즉시 라이선스 키를 생성하고 싶을 때:
|
||||
`server/debug_license.js` 등의 도구를 활용할 수 있습니다 (추가 구현 시 제공).
|
||||
|
||||
---
|
||||
|
||||
## 3. 문제 해결 (Troubleshooting)
|
||||
|
||||
- **로그인 실패**: `server/.env`의 `JWT_SECRET`이 설정되어 있는지 확인하세요.
|
||||
- **DB 연결 오류**: `server/.env`의 DB 설정 정보와 Synology MariaDB 10의 정보가 일치하는지 확인하세요.
|
||||
- **웹 페이지 접속 안됨**: Synology 방화벽에서 3006 포트가 허용되어 있는지, PM2 서비스가 `online` 상태인지 확인하세요.
|
||||
BIN
server/.env
BIN
server/.env
Binary file not shown.
@ -40,29 +40,32 @@ async function init() {
|
||||
console.log('Skip issued_licenses migration:', e.message);
|
||||
}
|
||||
|
||||
// 2. license_history
|
||||
const createHistorySql = `
|
||||
CREATE TABLE IF NOT EXISTS license_history (
|
||||
// 3. users table
|
||||
const createUsersSql = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
module_code VARCHAR(50) NOT NULL,
|
||||
license_key TEXT,
|
||||
license_type VARCHAR(20),
|
||||
subscriber_id VARCHAR(50),
|
||||
activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
login_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(100),
|
||||
role VARCHAR(20) DEFAULT 'admin',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`;
|
||||
await connection.query(createHistorySql);
|
||||
await connection.query(createUsersSql);
|
||||
|
||||
try {
|
||||
await connection.query(`
|
||||
INSERT IGNORE INTO smart_ims_license_db.license_history
|
||||
(module_code, license_key, license_type, subscriber_id, activated_at)
|
||||
SELECT module_code, license_key, license_type, subscriber_id, activated_at
|
||||
FROM sokuree_platform_dev.license_history;
|
||||
`);
|
||||
console.log('✅ Migrated license_history');
|
||||
} catch (e) {
|
||||
console.log('Skip license_history migration:', e.message);
|
||||
// Seed Initial Admin
|
||||
const bcrypt = require('bcryptjs');
|
||||
const adminId = 'admin';
|
||||
const adminPw = '^Ocean1472bk';
|
||||
const [existing] = await connection.query('SELECT id FROM users WHERE login_id = ?', [adminId]);
|
||||
|
||||
if (existing.length === 0) {
|
||||
const hashedPw = await bcrypt.hash(adminPw, 10);
|
||||
await connection.query(
|
||||
'INSERT INTO users (login_id, password, name, role) VALUES (?, ?, ?, ?)',
|
||||
[adminId, hashedPw, 'Administrator', 'admin']
|
||||
);
|
||||
console.log('✅ Created initial admin account');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
|
||||
16
server/node_modules/.bin/bcrypt
generated
vendored
Normal file
16
server/node_modules/.bin/bcrypt
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../bcryptjs/bin/bcrypt" "$@"
|
||||
else
|
||||
exec node "$basedir/../bcryptjs/bin/bcrypt" "$@"
|
||||
fi
|
||||
17
server/node_modules/.bin/bcrypt.cmd
generated
vendored
Normal file
17
server/node_modules/.bin/bcrypt.cmd
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\bcryptjs\bin\bcrypt" %*
|
||||
28
server/node_modules/.bin/bcrypt.ps1
generated
vendored
Normal file
28
server/node_modules/.bin/bcrypt.ps1
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../bcryptjs/bin/bcrypt" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../bcryptjs/bin/bcrypt" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../bcryptjs/bin/bcrypt" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../bcryptjs/bin/bcrypt" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
9
server/node_modules/.package-lock.json
generated
vendored
9
server/node_modules/.package-lock.json
generated
vendored
@ -47,6 +47,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
|
||||
27
server/node_modules/bcryptjs/LICENSE
generated
vendored
Normal file
27
server/node_modules/bcryptjs/LICENSE
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
bcrypt.js
|
||||
---------
|
||||
Copyright (c) 2012 Nevins Bartolomeo <nevins.bartolomeo@gmail.com>
|
||||
Copyright (c) 2012 Shane Girish <shaneGirish@gmail.com>
|
||||
Copyright (c) 2025 Daniel Wirtz <dcode@dcode.io>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
201
server/node_modules/bcryptjs/README.md
generated
vendored
Normal file
201
server/node_modules/bcryptjs/README.md
generated
vendored
Normal file
@ -0,0 +1,201 @@
|
||||
# bcrypt.js
|
||||
|
||||
Optimized bcrypt in JavaScript with zero dependencies, with TypeScript support. Compatible to the C++
|
||||
[bcrypt](https://npmjs.org/package/bcrypt) binding on Node.js and also working in the browser.
|
||||
|
||||
[](https://github.com/dcodeIO/bcrypt.js/actions/workflows/test.yml) [](https://github.com/dcodeIO/bcrypt.js/actions/workflows/publish.yml) [](https://www.npmjs.com/package/bcryptjs)
|
||||
|
||||
## Security considerations
|
||||
|
||||
Besides incorporating a salt to protect against rainbow table attacks, bcrypt is an adaptive function: over time, the
|
||||
iteration count can be increased to make it slower, so it remains resistant to brute-force search attacks even with
|
||||
increasing computation power. ([see](http://en.wikipedia.org/wiki/Bcrypt))
|
||||
|
||||
While bcrypt.js is compatible to the C++ bcrypt binding, it is written in pure JavaScript and thus slower ([about 30%](https://github.com/dcodeIO/bcrypt.js/wiki/Benchmark)), effectively reducing the number of iterations that can be
|
||||
processed in an equal time span.
|
||||
|
||||
The maximum input length is 72 bytes (note that UTF-8 encoded characters use up to 4 bytes) and the length of generated
|
||||
hashes is 60 characters. Note that maximum input length is not implicitly checked by the library for compatibility with
|
||||
the C++ binding on Node.js, but should be checked with `bcrypt.truncates(password)` where necessary.
|
||||
|
||||
## Usage
|
||||
|
||||
The package exports an ECMAScript module with an UMD fallback.
|
||||
|
||||
```
|
||||
$> npm install bcryptjs
|
||||
```
|
||||
|
||||
```ts
|
||||
import bcrypt from "bcryptjs";
|
||||
```
|
||||
|
||||
### Usage with a CDN
|
||||
|
||||
- From GitHub via [jsDelivr](https://www.jsdelivr.com):<br />
|
||||
`https://cdn.jsdelivr.net/gh/dcodeIO/bcrypt.js@TAG/index.js` (ESM)
|
||||
- From npm via [jsDelivr](https://www.jsdelivr.com):<br />
|
||||
`https://cdn.jsdelivr.net/npm/bcryptjs@VERSION/index.js` (ESM)<br />
|
||||
`https://cdn.jsdelivr.net/npm/bcryptjs@VERSION/umd/index.js` (UMD)
|
||||
- From npm via [unpkg](https://unpkg.com):<br />
|
||||
`https://unpkg.com/bcryptjs@VERSION/index.js` (ESM)<br />
|
||||
`https://unpkg.com/bcryptjs@VERSION/umd/index.js` (UMD)
|
||||
|
||||
Replace `TAG` respectively `VERSION` with a [specific version](https://github.com/dcodeIO/bcrypt.js/releases) or omit it (not recommended in production) to use latest.
|
||||
|
||||
When using the ESM variant in a browser, the `crypto` import needs to be stubbed out, for example using an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap). Bundlers should omit it automatically.
|
||||
|
||||
### Usage - Sync
|
||||
|
||||
To hash a password:
|
||||
|
||||
```ts
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const hash = bcrypt.hashSync("B4c0/\/", salt);
|
||||
// Store hash in your password DB
|
||||
```
|
||||
|
||||
To check a password:
|
||||
|
||||
```ts
|
||||
// Load hash from your password DB
|
||||
bcrypt.compareSync("B4c0/\/", hash); // true
|
||||
bcrypt.compareSync("not_bacon", hash); // false
|
||||
```
|
||||
|
||||
Auto-gen a salt and hash:
|
||||
|
||||
```ts
|
||||
const hash = bcrypt.hashSync("bacon", 10);
|
||||
```
|
||||
|
||||
### Usage - Async
|
||||
|
||||
To hash a password:
|
||||
|
||||
```ts
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hash = await bcrypt.hash("B4c0/\/", salt);
|
||||
// Store hash in your password DB
|
||||
```
|
||||
|
||||
```ts
|
||||
bcrypt.genSalt(10, (err, salt) => {
|
||||
bcrypt.hash("B4c0/\/", salt, function (err, hash) {
|
||||
// Store hash in your password DB
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
To check a password:
|
||||
|
||||
```ts
|
||||
// Load hash from your password DB
|
||||
await bcrypt.compare("B4c0/\/", hash); // true
|
||||
await bcrypt.compare("not_bacon", hash); // false
|
||||
```
|
||||
|
||||
```ts
|
||||
// Load hash from your password DB
|
||||
bcrypt.compare("B4c0/\/", hash, (err, res) => {
|
||||
// res === true
|
||||
});
|
||||
bcrypt.compare("not_bacon", hash, (err, res) => {
|
||||
// res === false
|
||||
});
|
||||
```
|
||||
|
||||
Auto-gen a salt and hash:
|
||||
|
||||
```ts
|
||||
await bcrypt.hash("B4c0/\/", 10);
|
||||
// Store hash in your password DB
|
||||
```
|
||||
|
||||
```ts
|
||||
bcrypt.hash("B4c0/\/", 10, (err, hash) => {
|
||||
// Store hash in your password DB
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** Under the hood, asynchronous APIs split an operation into small chunks. After the completion of a chunk, the execution of the next chunk is placed on the back of the [JS event queue](https://developer.mozilla.org/en/docs/Web/JavaScript/EventLoop), efficiently yielding for other computation to execute.
|
||||
|
||||
### Usage - Command Line
|
||||
|
||||
```
|
||||
Usage: bcrypt <input> [rounds|salt]
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Callback types
|
||||
|
||||
- **Callback<`T`>**: `(err: Error | null, result?: T) => void`<br />
|
||||
Called with an error on failure or a value of type `T` upon success.
|
||||
|
||||
- **ProgressCallback**: `(percentage: number) => void`<br />
|
||||
Called with the percentage of rounds completed (0.0 - 1.0), maximally once per `MAX_EXECUTION_TIME = 100` ms.
|
||||
|
||||
- **RandomFallback**: `(length: number) => number[]`<br />
|
||||
Called to obtain random bytes when both [Web Crypto API](http://www.w3.org/TR/WebCryptoAPI/) and Node.js
|
||||
[crypto](http://nodejs.org/api/crypto.html) are not available.
|
||||
|
||||
### Functions
|
||||
|
||||
- bcrypt.**genSaltSync**(rounds?: `number`): `string`<br />
|
||||
Synchronously generates a salt. Number of rounds defaults to 10 when omitted.
|
||||
|
||||
- bcrypt.**genSalt**(rounds?: `number`): `Promise<string>`<br />
|
||||
Asynchronously generates a salt. Number of rounds defaults to 10 when omitted.
|
||||
|
||||
- bcrypt.**genSalt**([rounds: `number`, ]callback: `Callback<string>`): `void`<br />
|
||||
Asynchronously generates a salt. Number of rounds defaults to 10 when omitted.
|
||||
|
||||
- bcrypt.**truncates**(password: `string`): `boolean`<br />
|
||||
Tests if a password will be truncated when hashed, that is its length is greater than 72 bytes when converted to UTF-8.
|
||||
|
||||
- bcrypt.**hashSync**(password: `string`, salt?: `number | string`): `string`
|
||||
Synchronously generates a hash for the given password. Number of rounds defaults to 10 when omitted.
|
||||
|
||||
- bcrypt.**hash**(password: `string`, salt: `number | string`): `Promise<string>`<br />
|
||||
Asynchronously generates a hash for the given password.
|
||||
|
||||
- bcrypt.**hash**(password: `string`, salt: `number | string`, callback: `Callback<string>`, progressCallback?: `ProgressCallback`): `void`<br />
|
||||
Asynchronously generates a hash for the given password.
|
||||
|
||||
- bcrypt.**compareSync**(password: `string`, hash: `string`): `boolean`<br />
|
||||
Synchronously tests a password against a hash.
|
||||
|
||||
- bcrypt.**compare**(password: `string`, hash: `string`): `Promise<boolean>`<br />
|
||||
Asynchronously compares a password against a hash.
|
||||
|
||||
- bcrypt.**compare**(password: `string`, hash: `string`, callback: `Callback<boolean>`, progressCallback?: `ProgressCallback`)<br />
|
||||
Asynchronously compares a password against a hash.
|
||||
|
||||
- bcrypt.**getRounds**(hash: `string`): `number`<br />
|
||||
Gets the number of rounds used to encrypt the specified hash.
|
||||
|
||||
- bcrypt.**getSalt**(hash: `string`): `string`<br />
|
||||
Gets the salt portion from a hash. Does not validate the hash.
|
||||
|
||||
- bcrypt.**setRandomFallback**(random: `RandomFallback`): `void`<br />
|
||||
Sets the pseudo random number generator to use as a fallback if neither [Web Crypto API](http://www.w3.org/TR/WebCryptoAPI/) nor Node.js [crypto](http://nodejs.org/api/crypto.html) are available. Please note: It is highly important that the PRNG used is cryptographically secure and that it is seeded properly!
|
||||
|
||||
## Building
|
||||
|
||||
Building the UMD fallback:
|
||||
|
||||
```
|
||||
$> npm run build
|
||||
```
|
||||
|
||||
Running the [tests](./tests):
|
||||
|
||||
```
|
||||
$> npm test
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Based on work started by Shane Girish at [bcrypt-nodejs](https://github.com/shaneGirish/bcrypt-nodejs), which is itself
|
||||
based on [javascript-bcrypt](http://code.google.com/p/javascript-bcrypt/) (New BSD-licensed).
|
||||
23
server/node_modules/bcryptjs/bin/bcrypt
generated
vendored
Normal file
23
server/node_modules/bcryptjs/bin/bcrypt
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from "node:path";
|
||||
import bcrypt from "../index.js";
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.log(
|
||||
"Usage: " + path.basename(process.argv[1]) + " <input> [rounds|salt]",
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
var salt;
|
||||
if (process.argv.length > 3) {
|
||||
salt = process.argv[3];
|
||||
var rounds = parseInt(salt, 10);
|
||||
if (rounds == salt) {
|
||||
salt = bcrypt.genSaltSync(rounds);
|
||||
}
|
||||
} else {
|
||||
salt = bcrypt.genSaltSync();
|
||||
}
|
||||
console.log(bcrypt.hashSync(process.argv[2], salt));
|
||||
}
|
||||
3
server/node_modules/bcryptjs/index.d.ts
generated
vendored
Normal file
3
server/node_modules/bcryptjs/index.d.ts
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import * as bcrypt from "./types.js";
|
||||
export * from "./types.js";
|
||||
export default bcrypt;
|
||||
1159
server/node_modules/bcryptjs/index.js
generated
vendored
Normal file
1159
server/node_modules/bcryptjs/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
76
server/node_modules/bcryptjs/package.json
generated
vendored
Normal file
76
server/node_modules/bcryptjs/package.json
generated
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "bcryptjs",
|
||||
"description": "Optimized bcrypt in plain JavaScript with zero dependencies, with TypeScript support. Compatible to 'bcrypt'.",
|
||||
"version": "3.0.3",
|
||||
"author": "Daniel Wirtz <dcode@dcode.io>",
|
||||
"contributors": [
|
||||
"Shane Girish <shaneGirish@gmail.com> (https://github.com/shaneGirish)",
|
||||
"Alex Murray <> (https://github.com/alexmurray)",
|
||||
"Nicolas Pelletier <> (https://github.com/NicolasPelletier)",
|
||||
"Josh Rogers <> (https://github.com/geekymole)",
|
||||
"Noah Isaacson <noah@nisaacson.com> (https://github.com/nisaacson)"
|
||||
],
|
||||
"repository": {
|
||||
"type": "url",
|
||||
"url": "https://github.com/dcodeIO/bcrypt.js.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dcodeIO/bcrypt.js/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"bcrypt",
|
||||
"password",
|
||||
"auth",
|
||||
"authentication",
|
||||
"encryption",
|
||||
"crypt",
|
||||
"crypto"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "umd/index.js",
|
||||
"types": "umd/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./umd/index.d.ts",
|
||||
"default": "./umd/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"scripts": {
|
||||
"build": "node scripts/build.js",
|
||||
"lint": "prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"test": "npm run test:unit && npm run test:typescript",
|
||||
"test:unit": "node tests",
|
||||
"test:typescript": "tsc --project tests/typescript/tsconfig.esnext.json && tsc --project tests/typescript/tsconfig.nodenext.json && tsc --project tests/typescript/tsconfig.commonjs.json && tsc --project tests/typescript/tsconfig.global.json"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"types.d.ts",
|
||||
"umd/index.js",
|
||||
"umd/index.d.ts",
|
||||
"umd/types.d.ts",
|
||||
"umd/package.json",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"browser": {
|
||||
"crypto": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"esm2umd": "^0.3.1",
|
||||
"prettier": "^3.5.0",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
157
server/node_modules/bcryptjs/types.d.ts
generated
vendored
Normal file
157
server/node_modules/bcryptjs/types.d.ts
generated
vendored
Normal file
@ -0,0 +1,157 @@
|
||||
// Originally imported from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8b36dbdf95b624b8a7cd7f8416f06c15d274f9e6/types/bcryptjs/index.d.ts
|
||||
// MIT license.
|
||||
|
||||
/** Called with an error on failure or a value of type `T` upon success. */
|
||||
type Callback<T> = (err: Error | null, result?: T) => void;
|
||||
/** Called with the percentage of rounds completed (0.0 - 1.0), maximally once per `MAX_EXECUTION_TIME = 100` ms. */
|
||||
type ProgressCallback = (percentage: number) => void;
|
||||
/** Called to obtain random bytes when both Web Crypto API and Node.js crypto are not available. */
|
||||
type RandomFallback = (length: number) => number[];
|
||||
|
||||
/**
|
||||
* Sets the pseudo random number generator to use as a fallback if neither node's crypto module nor the Web Crypto API is available.
|
||||
* Please note: It is highly important that the PRNG used is cryptographically secure and that it is seeded properly!
|
||||
* @param random Function taking the number of bytes to generate as its sole argument, returning the corresponding array of cryptographically secure random byte values.
|
||||
*/
|
||||
export declare function setRandomFallback(random: RandomFallback): void;
|
||||
|
||||
/**
|
||||
* Synchronously generates a salt.
|
||||
* @param rounds Number of rounds to use, defaults to 10 if omitted
|
||||
* @return Resulting salt
|
||||
* @throws If a random fallback is required but not set
|
||||
*/
|
||||
export declare function genSaltSync(rounds?: number): string;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a salt.
|
||||
* @param rounds Number of rounds to use, defaults to 10 if omitted
|
||||
* @return Promise with resulting salt, if callback has been omitted
|
||||
*/
|
||||
export declare function genSalt(rounds?: number): Promise<string>;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a salt.
|
||||
* @param callback Callback receiving the error, if any, and the resulting salt
|
||||
*/
|
||||
export declare function genSalt(callback: Callback<string>): void;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a salt.
|
||||
* @param rounds Number of rounds to use, defaults to 10 if omitted
|
||||
* @param callback Callback receiving the error, if any, and the resulting salt
|
||||
*/
|
||||
export declare function genSalt(
|
||||
rounds: number,
|
||||
callback: Callback<string>,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Synchronously generates a hash for the given password.
|
||||
* @param password Password to hash
|
||||
* @param salt Salt length to generate or salt to use, default to 10
|
||||
* @return Resulting hash
|
||||
*/
|
||||
export declare function hashSync(
|
||||
password: string,
|
||||
salt?: number | string,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a hash for the given password.
|
||||
* @param password Password to hash
|
||||
* @param salt Salt length to generate or salt to use
|
||||
* @return Promise with resulting hash, if callback has been omitted
|
||||
*/
|
||||
export declare function hash(
|
||||
password: string,
|
||||
salt: number | string,
|
||||
): Promise<string>;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a hash for the given password.
|
||||
* @param password Password to hash
|
||||
* @param salt Salt length to generate or salt to use
|
||||
* @param callback Callback receiving the error, if any, and the resulting hash
|
||||
* @param progressCallback Callback successively called with the percentage of rounds completed (0.0 - 1.0), maximally once per MAX_EXECUTION_TIME = 100 ms.
|
||||
*/
|
||||
export declare function hash(
|
||||
password: string,
|
||||
salt: number | string,
|
||||
callback?: Callback<string>,
|
||||
progressCallback?: ProgressCallback,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Synchronously tests a password against a hash.
|
||||
* @param password Password to test
|
||||
* @param hash Hash to test against
|
||||
* @return true if matching, otherwise false
|
||||
*/
|
||||
export declare function compareSync(password: string, hash: string): boolean;
|
||||
|
||||
/**
|
||||
* Asynchronously tests a password against a hash.
|
||||
* @param password Password to test
|
||||
* @param hash Hash to test against
|
||||
* @return Promise, if callback has been omitted
|
||||
*/
|
||||
export declare function compare(
|
||||
password: string,
|
||||
hash: string,
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Asynchronously tests a password against a hash.
|
||||
* @param password Password to test
|
||||
* @param hash Hash to test against
|
||||
* @param callback Callback receiving the error, if any, otherwise the result
|
||||
* @param progressCallback Callback successively called with the percentage of rounds completed (0.0 - 1.0), maximally once per MAX_EXECUTION_TIME = 100 ms.
|
||||
*/
|
||||
export declare function compare(
|
||||
password: string,
|
||||
hash: string,
|
||||
callback?: Callback<boolean>,
|
||||
progressCallback?: ProgressCallback,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Gets the number of rounds used to encrypt the specified hash.
|
||||
* @param hash Hash to extract the used number of rounds from
|
||||
* @return Number of rounds used
|
||||
*/
|
||||
export declare function getRounds(hash: string): number;
|
||||
|
||||
/**
|
||||
* Gets the salt portion from a hash. Does not validate the hash.
|
||||
* @param hash Hash to extract the salt from
|
||||
* @return Extracted salt part
|
||||
*/
|
||||
export declare function getSalt(hash: string): string;
|
||||
|
||||
/**
|
||||
* Tests if a password will be truncated when hashed, that is its length is
|
||||
* greater than 72 bytes when converted to UTF-8.
|
||||
* @param password The password to test
|
||||
* @returns `true` if truncated, otherwise `false`
|
||||
*/
|
||||
export declare function truncates(password: string): boolean;
|
||||
|
||||
/**
|
||||
* Encodes a byte array to base64 with up to len bytes of input, using the custom bcrypt alphabet.
|
||||
* @function
|
||||
* @param b Byte array
|
||||
* @param len Maximum input length
|
||||
*/
|
||||
export declare function encodeBase64(
|
||||
b: Readonly<ArrayLike<number>>,
|
||||
len: number,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* Decodes a base64 encoded string to up to len bytes of output, using the custom bcrypt alphabet.
|
||||
* @function
|
||||
* @param s String to decode
|
||||
* @param len Maximum output length
|
||||
*/
|
||||
export declare function decodeBase64(s: string, len: number): number[];
|
||||
3
server/node_modules/bcryptjs/umd/index.d.ts
generated
vendored
Normal file
3
server/node_modules/bcryptjs/umd/index.d.ts
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import * as bcrypt from "./types.js";
|
||||
export = bcrypt;
|
||||
export as namespace bcrypt;
|
||||
1220
server/node_modules/bcryptjs/umd/index.js
generated
vendored
Normal file
1220
server/node_modules/bcryptjs/umd/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
server/node_modules/bcryptjs/umd/package.json
generated
vendored
Normal file
3
server/node_modules/bcryptjs/umd/package.json
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
157
server/node_modules/bcryptjs/umd/types.d.ts
generated
vendored
Normal file
157
server/node_modules/bcryptjs/umd/types.d.ts
generated
vendored
Normal file
@ -0,0 +1,157 @@
|
||||
// Originally imported from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8b36dbdf95b624b8a7cd7f8416f06c15d274f9e6/types/bcryptjs/index.d.ts
|
||||
// MIT license.
|
||||
|
||||
/** Called with an error on failure or a value of type `T` upon success. */
|
||||
type Callback<T> = (err: Error | null, result?: T) => void;
|
||||
/** Called with the percentage of rounds completed (0.0 - 1.0), maximally once per `MAX_EXECUTION_TIME = 100` ms. */
|
||||
type ProgressCallback = (percentage: number) => void;
|
||||
/** Called to obtain random bytes when both Web Crypto API and Node.js crypto are not available. */
|
||||
type RandomFallback = (length: number) => number[];
|
||||
|
||||
/**
|
||||
* Sets the pseudo random number generator to use as a fallback if neither node's crypto module nor the Web Crypto API is available.
|
||||
* Please note: It is highly important that the PRNG used is cryptographically secure and that it is seeded properly!
|
||||
* @param random Function taking the number of bytes to generate as its sole argument, returning the corresponding array of cryptographically secure random byte values.
|
||||
*/
|
||||
export declare function setRandomFallback(random: RandomFallback): void;
|
||||
|
||||
/**
|
||||
* Synchronously generates a salt.
|
||||
* @param rounds Number of rounds to use, defaults to 10 if omitted
|
||||
* @return Resulting salt
|
||||
* @throws If a random fallback is required but not set
|
||||
*/
|
||||
export declare function genSaltSync(rounds?: number): string;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a salt.
|
||||
* @param rounds Number of rounds to use, defaults to 10 if omitted
|
||||
* @return Promise with resulting salt, if callback has been omitted
|
||||
*/
|
||||
export declare function genSalt(rounds?: number): Promise<string>;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a salt.
|
||||
* @param callback Callback receiving the error, if any, and the resulting salt
|
||||
*/
|
||||
export declare function genSalt(callback: Callback<string>): void;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a salt.
|
||||
* @param rounds Number of rounds to use, defaults to 10 if omitted
|
||||
* @param callback Callback receiving the error, if any, and the resulting salt
|
||||
*/
|
||||
export declare function genSalt(
|
||||
rounds: number,
|
||||
callback: Callback<string>,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Synchronously generates a hash for the given password.
|
||||
* @param password Password to hash
|
||||
* @param salt Salt length to generate or salt to use, default to 10
|
||||
* @return Resulting hash
|
||||
*/
|
||||
export declare function hashSync(
|
||||
password: string,
|
||||
salt?: number | string,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a hash for the given password.
|
||||
* @param password Password to hash
|
||||
* @param salt Salt length to generate or salt to use
|
||||
* @return Promise with resulting hash, if callback has been omitted
|
||||
*/
|
||||
export declare function hash(
|
||||
password: string,
|
||||
salt: number | string,
|
||||
): Promise<string>;
|
||||
|
||||
/**
|
||||
* Asynchronously generates a hash for the given password.
|
||||
* @param password Password to hash
|
||||
* @param salt Salt length to generate or salt to use
|
||||
* @param callback Callback receiving the error, if any, and the resulting hash
|
||||
* @param progressCallback Callback successively called with the percentage of rounds completed (0.0 - 1.0), maximally once per MAX_EXECUTION_TIME = 100 ms.
|
||||
*/
|
||||
export declare function hash(
|
||||
password: string,
|
||||
salt: number | string,
|
||||
callback?: Callback<string>,
|
||||
progressCallback?: ProgressCallback,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Synchronously tests a password against a hash.
|
||||
* @param password Password to test
|
||||
* @param hash Hash to test against
|
||||
* @return true if matching, otherwise false
|
||||
*/
|
||||
export declare function compareSync(password: string, hash: string): boolean;
|
||||
|
||||
/**
|
||||
* Asynchronously tests a password against a hash.
|
||||
* @param password Password to test
|
||||
* @param hash Hash to test against
|
||||
* @return Promise, if callback has been omitted
|
||||
*/
|
||||
export declare function compare(
|
||||
password: string,
|
||||
hash: string,
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Asynchronously tests a password against a hash.
|
||||
* @param password Password to test
|
||||
* @param hash Hash to test against
|
||||
* @param callback Callback receiving the error, if any, otherwise the result
|
||||
* @param progressCallback Callback successively called with the percentage of rounds completed (0.0 - 1.0), maximally once per MAX_EXECUTION_TIME = 100 ms.
|
||||
*/
|
||||
export declare function compare(
|
||||
password: string,
|
||||
hash: string,
|
||||
callback?: Callback<boolean>,
|
||||
progressCallback?: ProgressCallback,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Gets the number of rounds used to encrypt the specified hash.
|
||||
* @param hash Hash to extract the used number of rounds from
|
||||
* @return Number of rounds used
|
||||
*/
|
||||
export declare function getRounds(hash: string): number;
|
||||
|
||||
/**
|
||||
* Gets the salt portion from a hash. Does not validate the hash.
|
||||
* @param hash Hash to extract the salt from
|
||||
* @return Extracted salt part
|
||||
*/
|
||||
export declare function getSalt(hash: string): string;
|
||||
|
||||
/**
|
||||
* Tests if a password will be truncated when hashed, that is its length is
|
||||
* greater than 72 bytes when converted to UTF-8.
|
||||
* @param password The password to test
|
||||
* @returns `true` if truncated, otherwise `false`
|
||||
*/
|
||||
export declare function truncates(password: string): boolean;
|
||||
|
||||
/**
|
||||
* Encodes a byte array to base64 with up to len bytes of input, using the custom bcrypt alphabet.
|
||||
* @function
|
||||
* @param b Byte array
|
||||
* @param len Maximum input length
|
||||
*/
|
||||
export declare function encodeBase64(
|
||||
b: Readonly<ArrayLike<number>>,
|
||||
len: number,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* Decodes a base64 encoded string to up to len bytes of output, using the custom bcrypt alphabet.
|
||||
* @function
|
||||
* @param s String to decode
|
||||
* @param len Maximum output length
|
||||
*/
|
||||
export declare function decodeBase64(s: string, len: number): number[];
|
||||
10
server/package-lock.json
generated
10
server/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.6",
|
||||
"crypto": "^1.0.1",
|
||||
@ -64,6 +65,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.6",
|
||||
"crypto": "^1.0.1",
|
||||
@ -24,4 +25,4 @@
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,32 @@ require('dotenv').config();
|
||||
|
||||
const db = require('./db');
|
||||
const { generateLicense } = require('./licenseManager');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const cookieParser = require('cookie-parser');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.LICENSE_SERVER_PORT || 3006;
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret';
|
||||
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
// Auth Middleware
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const token = req.cookies.token;
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized: No token provided' });
|
||||
|
||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||
if (err) return res.status(403).json({ error: 'Forbidden: Invalid token' });
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
// Load Private Key for Generation
|
||||
const privateKeyPath = path.join(__dirname, '../config/private_key.pem');
|
||||
@ -29,8 +46,44 @@ try {
|
||||
console.error('Error loading private key:', e);
|
||||
}
|
||||
|
||||
// --- Auth Routes ---
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
const { loginId, password } = req.body;
|
||||
try {
|
||||
const [users] = await db.query('SELECT * FROM users WHERE login_id = ?', [loginId]);
|
||||
if (users.length === 0) return res.status(401).json({ error: 'Invalid ID or Password' });
|
||||
|
||||
const user = users[0];
|
||||
const validPassword = await bcrypt.compare(password, user.password);
|
||||
if (!validPassword) return res.status(401).json({ error: 'Invalid ID or Password' });
|
||||
|
||||
const token = jwt.sign({ id: user.id, loginId: user.login_id, name: user.name }, JWT_SECRET, { expiresIn: '24h' });
|
||||
|
||||
res.cookie('token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
});
|
||||
|
||||
res.json({ success: true, user: { id: user.id, loginId: user.login_id, name: user.name } });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/auth/me', authenticateToken, (req, res) => {
|
||||
res.json(req.user);
|
||||
});
|
||||
|
||||
app.post('/api/auth/logout', (req, res) => {
|
||||
res.clearCookie('token');
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// --- License Routes ---
|
||||
// 1. Get All Issued Licenses
|
||||
app.get('/api/licenses', async (req, res) => {
|
||||
app.get('/api/licenses', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await db.query('SELECT * FROM issued_licenses ORDER BY created_at DESC');
|
||||
res.json(rows);
|
||||
@ -41,7 +94,7 @@ app.get('/api/licenses', async (req, res) => {
|
||||
});
|
||||
|
||||
// 1.1 Delete License
|
||||
app.delete('/api/licenses/:id', async (req, res) => {
|
||||
app.delete('/api/licenses/:id', authenticateToken, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
await db.query('DELETE FROM issued_licenses WHERE id = ?', [id]);
|
||||
@ -70,7 +123,7 @@ app.post('/api/licenses/activate', async (req, res) => {
|
||||
});
|
||||
|
||||
// 2. Generate New License
|
||||
app.post('/api/licenses/generate', async (req, res) => {
|
||||
app.post('/api/licenses/generate', authenticateToken, async (req, res) => {
|
||||
const { moduleCode, type, subscriberId, expiryDate } = req.body;
|
||||
|
||||
if (!moduleCode || !type || !subscriberId) {
|
||||
@ -113,7 +166,7 @@ app.post('/api/licenses/generate', async (req, res) => {
|
||||
const clientDistPath = path.join(__dirname, '../client/dist');
|
||||
if (fs.existsSync(clientDistPath)) {
|
||||
app.use(express.static(clientDistPath));
|
||||
|
||||
|
||||
// SPA Fallback: Any route not handled by API should return index.html
|
||||
app.get('*', (req, res) => {
|
||||
if (!req.path.startsWith('/api')) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user