๐ user.ts โข 9403 bytes
/**
* CmdCode V0.5 - ็จๆท่ฎค่ฏไธๅทฅไฝๅบ็ฎก็
*
* ๅ่ฝ๏ผ
* 1. ๆณจๅ/็ปๅฝ โ ่ฟ็จPHP API + MySQL
* 2. Tokenๆฌๅฐๅ ๅฏ็ผๅญ โ ไธๆฌกๅฏๅจ่ชๅจ็ปๅฝ
* 3. ๆญ็น็ปญไผ โ ๅทฅไฝๅบๅฟซ็
งไฟๅญ/ๆขๅค
* 4. ็จๆทไธๅฑๆไปถๅคน โ ้็ฆป+1GB้
้ข
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
import { join } from 'node:path'
import { homedir } from 'node:os'
import { encrypt, decrypt } from './crypto-util.js'
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ้
็ฝฎ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const API_BASE = 'https://cmdcode.cn/xiaoc/cmdcode_api.php'
const CMD_DIR = join(homedir(), '.cmdcode')
const USER_CACHE_FILE = join(CMD_DIR, 'user.enc')
const WORKSPACE_BASE = join(CMD_DIR, 'workspaces')
// ้
้ข่ฎก็ฎ๏ผadmin 1GB๏ผๆฎ้็จๆท100MB
const SUPER_USERS = ['admin', 'root', 'administrator']
function getUserQuotaMb(username: string): number {
return SUPER_USERS.includes(username) ? 1024 : 100
}
export { getUserQuotaMb }
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ็ฑปๅ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export interface UserInfo {
username: string
token: string
expiresAt: string
workspaceDir: string
}
export interface QuotaInfo {
usedMb: number
quotaMb: number
remainingMb: number
usagePercent: number
}
export interface SaveSnapshotResult {
success: boolean
message: string
}
/** ็จๆท่ชๅฎไนๆจกๅ้
็ฝฎ */
export interface UserModel {
id: string
name: string
model: string
baseUrl: string
apiKey: string
note1?: string
note2?: string
createdAt: string
isDefault: boolean
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ่ฟ็จAPI่ฐ็จ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function apiCall(action: string, data?: Record<string, string>, token?: string): Promise<any> {
const url = `${API_BASE}?action=${action}`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const response = await fetch(url, {
method: 'POST',
headers,
body: data ? JSON.stringify(data) : undefined,
signal: AbortSignal.timeout(10_000), // 10็ง่ถ
ๆถ
})
return await response.json()
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ็จๆทๆณจๅ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export async function register(username: string, password: string): Promise<UserInfo> {
const result = await apiCall('register', { username, password })
if (result.error) {
throw new Error(result.error)
}
const userInfo: UserInfo = {
username: result.username,
token: result.token,
expiresAt: result.expires_at,
workspaceDir: join(WORKSPACE_BASE, result.username),
}
// ๅๅปบ็จๆทไธๅฑๆไปถๅคน
if (!existsSync(userInfo.workspaceDir)) {
mkdirSync(userInfo.workspaceDir, { recursive: true })
}
// ๆฌๅฐๅ ๅฏ็ผๅญtoken
saveUserCache(userInfo)
return userInfo
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ็จๆท็ปๅฝ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export async function login(username: string, password: string): Promise<UserInfo> {
const result = await apiCall('login', { username, password })
if (result.error) {
throw new Error(result.error)
}
const userInfo: UserInfo = {
username: result.username,
token: result.token,
expiresAt: result.expires_at,
workspaceDir: join(WORKSPACE_BASE, result.username),
}
// ๅๅปบ็จๆทไธๅฑๆไปถๅคน
if (!existsSync(userInfo.workspaceDir)) {
mkdirSync(userInfo.workspaceDir, { recursive: true })
}
// ๆฌๅฐๅ ๅฏ็ผๅญtoken
saveUserCache(userInfo)
return userInfo
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆญ็น็ปญไผ ๏ผไฟๅญๅทฅไฝๅบๅฟซ็
ง
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export async function saveWorkspaceSnapshot(
userInfo: UserInfo,
snapshot: string
): Promise<SaveSnapshotResult> {
const result = await apiCall('save', { snapshot }, userInfo.token)
if (result.error) {
return { success: false, message: result.error }
}
return { success: true, message: result.message }
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆญ็น็ปญไผ ๏ผๆขๅคๅทฅไฝๅบๅฟซ็
ง
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export async function restoreWorkspaceSnapshot(userInfo: UserInfo): Promise<string | null> {
const result = await apiCall('restore', undefined, userInfo.token)
if (result.error) {
return null
}
return result.snapshot || null
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ้
้ขๆฅ่ฏข
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export async function getQuota(userInfo: UserInfo): Promise<QuotaInfo> {
const result = await apiCall('quota', undefined, userInfo.token)
if (result.error) {
return { usedMb: 0, quotaMb: getUserQuotaMb(userInfo.username), remainingMb: getUserQuotaMb(userInfo.username), usagePercent: 0 }
}
return {
usedMb: result.used_mb,
quotaMb: result.quota_mb,
remainingMb: result.remaining_mb,
usagePercent: result.usage_percent,
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆดๆฐ่ฟ็จ้
้ข
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export async function updateRemoteQuota(userInfo: UserInfo, usedMb: number): Promise<void> {
await apiCall('update_quota', { used_mb: String(usedMb) }, userInfo.token)
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆฌๅฐToken็ผๅญ๏ผๅ ๅฏๅญๅจ๏ผ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function saveUserCache(userInfo: UserInfo): void {
if (!existsSync(CMD_DIR)) mkdirSync(CMD_DIR, { recursive: true })
const data = {
username: userInfo.username,
token: userInfo.token,
expiresAt: userInfo.expiresAt,
}
writeFileSync(USER_CACHE_FILE, encrypt(data), 'utf-8')
try { require('node:fs').chmodSync(USER_CACHE_FILE, 0o600) } catch { /* ignore */ }
}
export function loadUserCache(): UserInfo | null {
if (!existsSync(USER_CACHE_FILE)) return null
try {
const data = decrypt<{ username: string; token: string; expiresAt: string }>(
readFileSync(USER_CACHE_FILE, 'utf-8')
)
return {
username: data.username,
token: data.token,
expiresAt: data.expiresAt,
workspaceDir: join(WORKSPACE_BASE, data.username),
}
} catch {
return null
}
}
export function clearUserCache(): void {
if (existsSync(USER_CACHE_FILE)) {
require('node:fs').unlinkSync(USER_CACHE_FILE)
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Tokenๆๆๆง้ช่ฏ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export async function validateToken(userInfo: UserInfo): Promise<boolean> {
try {
const result = await apiCall('quota', undefined, userInfo.token)
return result.success === true
} catch {
return false
}
}
/** ไป
ๅญ token ๅญ็ฌฆไธฒ้ช่ฏๆๆๆง๏ผๆ ้ UserInfo๏ผ็จไบ Web ๅค็จๆทๅบๆฏ๏ผ */
export async function verifyAuthToken(token: string): Promise<UserInfo | null> {
try {
const result = await apiCall('quota', undefined, token)
if (result.success === true && result.username) {
return {
username: result.username,
token,
expiresAt: result.expires_at || '',
workspaceDir: getWorkspaceDir(result.username),
}
}
return null
} catch {
return null
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ่ทๅ็จๆทๅทฅไฝๅบ่ทฏๅพ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export function getWorkspaceDir(username: string): string {
return join(WORKSPACE_BASE, username)
}