feat: add PostgreSQL + JWT backend, fix 4 critical issues (auth/role persistence/edit/library)
This commit is contained in:
parent
9f4ee690db
commit
6234c27138
251
app.js
251
app.js
|
|
@ -1,6 +1,38 @@
|
||||||
(() => {
|
(() => {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// --- API helper ---
|
||||||
|
const API_BASE = '/api';
|
||||||
|
const TOKEN_KEY = 'eternal_ai_token';
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem(TOKEN_KEY) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToken(token) {
|
||||||
|
if (token) localStorage.setItem(TOKEN_KEY, token);
|
||||||
|
else localStorage.removeItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(path, options = {}) {
|
||||||
|
const token = getToken();
|
||||||
|
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
|
||||||
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.error || `请求失败 (${res.status})`);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message === 'Failed to fetch') {
|
||||||
|
throw new Error('无法连接服务器,请检查网络');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- U9: Unified state management ---
|
// --- U9: Unified state management ---
|
||||||
const STORAGE_KEY = 'eternal_ai_state';
|
const STORAGE_KEY = 'eternal_ai_state';
|
||||||
|
|
||||||
|
|
@ -8,6 +40,7 @@
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
isCreator: false,
|
isCreator: false,
|
||||||
account: null,
|
account: null,
|
||||||
|
userId: null,
|
||||||
boundCreator: null,
|
boundCreator: null,
|
||||||
libraryName: '我的 [XXX]',
|
libraryName: '我的 [XXX]',
|
||||||
creatorName: '',
|
creatorName: '',
|
||||||
|
|
@ -93,6 +126,7 @@
|
||||||
let activeAuthTab = 'login';
|
let activeAuthTab = 'login';
|
||||||
let activeCenterTab = 'roles';
|
let activeCenterTab = 'roles';
|
||||||
let currentRole = null;
|
let currentRole = null;
|
||||||
|
let editingRoleId = null;
|
||||||
let viewHistory = ['landing'];
|
let viewHistory = ['landing'];
|
||||||
|
|
||||||
// --- U9: Unified showView with history and tab-bar sync ---
|
// --- U9: Unified showView with history and tab-bar sync ---
|
||||||
|
|
@ -231,10 +265,13 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function login(account) {
|
function applyUserData(user) {
|
||||||
state.isLoggedIn = true;
|
state.isLoggedIn = true;
|
||||||
state.account = account;
|
state.userId = user.id;
|
||||||
state.boundCreator = state.boundCreator || { name: '云朵', roles: mockRoles };
|
state.account = user.account;
|
||||||
|
state.isCreator = user.isCreator || false;
|
||||||
|
state.creatorName = user.creatorName || '';
|
||||||
|
state.libraryName = user.libraryName || '我的角色库';
|
||||||
saveState();
|
saveState();
|
||||||
updateLandingCard();
|
updateLandingCard();
|
||||||
}
|
}
|
||||||
|
|
@ -243,55 +280,69 @@
|
||||||
state.isLoggedIn = false;
|
state.isLoggedIn = false;
|
||||||
state.isCreator = false;
|
state.isCreator = false;
|
||||||
state.account = null;
|
state.account = null;
|
||||||
|
state.userId = null;
|
||||||
|
state.boundCreator = null;
|
||||||
|
state.roles = [];
|
||||||
|
state.income = { balance: 0, records: [] };
|
||||||
|
setToken('');
|
||||||
saveState();
|
saveState();
|
||||||
updateLandingCard();
|
updateLandingCard();
|
||||||
showView('landing');
|
showView('landing');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- U2: Role Library ---
|
// --- U2: Role Library ---
|
||||||
function renderRoleLibrary() {
|
async function renderRoleLibrary() {
|
||||||
const listEl = document.getElementById('role-list');
|
const listEl = document.getElementById('role-list');
|
||||||
const emptyEl = document.getElementById('library-empty');
|
const emptyEl = document.getElementById('library-empty');
|
||||||
const titleEl = document.getElementById('library-title');
|
const titleEl = document.getElementById('library-title');
|
||||||
|
|
||||||
titleEl.textContent = state.libraryName || '我的角色库';
|
titleEl.textContent = state.libraryName || '我的角色库';
|
||||||
|
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">加载中…</p>';
|
||||||
|
emptyEl.hidden = true;
|
||||||
|
|
||||||
if (!state.boundCreator) {
|
try {
|
||||||
|
const { roles } = await api('/roles');
|
||||||
|
if (roles.length === 0) {
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
emptyEl.hidden = false;
|
emptyEl.hidden = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
listEl.innerHTML = roles
|
||||||
emptyEl.hidden = true;
|
|
||||||
listEl.innerHTML = mockRoles
|
|
||||||
.map(
|
.map(
|
||||||
(role) => `
|
(role) => `
|
||||||
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.name},${role.desc},每月${role.price}元">
|
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.displayName},${role.desc || ''},每月${role.price}元">
|
||||||
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
<img class="role-card__avatar" src="${role.avatar || 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20character%20portrait&image_size=square'}" alt="${role.displayName}" />
|
||||||
<div class="role-card__info">
|
<div class="role-card__info">
|
||||||
<h3 class="role-card__name">${role.name}</h3>
|
<h3 class="role-card__name">${role.displayName}</h3>
|
||||||
<p class="role-card__desc">${role.desc}</p>
|
<p class="role-card__desc">${role.desc || ''}</p>
|
||||||
<span class="role-card__price">¥${role.price}</span>
|
<span class="role-card__price">¥${role.price}</span>
|
||||||
</div>
|
</div>
|
||||||
</article>`
|
</article>`
|
||||||
)
|
)
|
||||||
.join('');
|
.join('');
|
||||||
|
} catch (err) {
|
||||||
|
listEl.innerHTML = `<p style="text-align:center;color:var(--text-muted)">加载失败:${err.message}</p>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- U3: Role Detail ---
|
// --- U3: Role Detail ---
|
||||||
function renderRoleDetail(roleId) {
|
async function renderRoleDetail(roleId) {
|
||||||
const role = mockRoles.find((r) => r.id === roleId);
|
try {
|
||||||
if (!role) return;
|
const { role } = await api(`/roles/${roleId}`);
|
||||||
currentRole = role;
|
currentRole = role;
|
||||||
|
|
||||||
document.getElementById('detail-name').textContent = role.name;
|
document.getElementById('detail-name').textContent = role.displayName;
|
||||||
document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar})`;
|
document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar || ''})`;
|
||||||
document.getElementById('detail-role-name').textContent = role.name;
|
document.getElementById('detail-role-name').textContent = role.displayName;
|
||||||
document.getElementById('detail-role-desc').textContent = role.desc;
|
document.getElementById('detail-role-desc').textContent = role.desc || role.personality || '';
|
||||||
document.getElementById('detail-price').innerHTML = `<span class="detail-price__value">¥${role.price}</span><span class="detail-price__unit">/ 月</span>`;
|
document.getElementById('detail-price').innerHTML = `<span class="detail-price__value">¥${role.price}</span><span class="detail-price__unit">/ 月</span>`;
|
||||||
|
|
||||||
document.getElementById('detail-actions-pre').hidden = false;
|
document.getElementById('detail-actions-pre').hidden = false;
|
||||||
document.getElementById('detail-paid').hidden = true;
|
document.getElementById('detail-paid').hidden = true;
|
||||||
|
} catch (err) {
|
||||||
|
alert('加载角色详情失败:' + err.message);
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function payRole() {
|
function payRole() {
|
||||||
|
|
@ -348,22 +399,33 @@
|
||||||
renderSettings();
|
renderSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCreatorRoles() {
|
async function renderCreatorRoles() {
|
||||||
const listEl = document.getElementById('creator-role-list');
|
const listEl = document.getElementById('creator-role-list');
|
||||||
const roles = state.roles.length > 0 ? state.roles : mockRoles;
|
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">加载中…</p>';
|
||||||
|
try {
|
||||||
|
const { roles } = await api('/roles/my/roles');
|
||||||
|
state.roles = roles;
|
||||||
|
saveState();
|
||||||
|
if (roles.length === 0) {
|
||||||
|
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">还没有创建角色,点击「新建角色」开始</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
listEl.innerHTML = roles
|
listEl.innerHTML = roles
|
||||||
.map(
|
.map(
|
||||||
(role) => `
|
(role) => `
|
||||||
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.name},${role.status === 'running' ? '运行中' : '已停止'}">
|
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.displayName},${role.status === 'running' ? '运行中' : '已停止'}">
|
||||||
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
<img class="role-card__avatar" src="${role.avatar || 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20character%20portrait&image_size=square'}" alt="${role.displayName}" />
|
||||||
<div class="role-card__info">
|
<div class="role-card__info">
|
||||||
<h3 class="role-card__name">${role.name}</h3>
|
<h3 class="role-card__name">${role.displayName}</h3>
|
||||||
<span class="role-card__status role-card__status--${role.status}">${role.status === 'running' ? '运行中' : '已停止'}</span>
|
<span class="role-card__status role-card__status--${role.status}">${role.status === 'running' ? '运行中' : '已停止'}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn--small btn--outline" type="button" data-action="edit-role" data-role-id="${role.id}">编辑</button>
|
<button class="btn btn--small btn--outline" type="button" data-action="edit-role" data-role-id="${role.id}">编辑</button>
|
||||||
</article>`
|
</article>`
|
||||||
)
|
)
|
||||||
.join('');
|
.join('');
|
||||||
|
} catch (err) {
|
||||||
|
listEl.innerHTML = `<p style="text-align:center;color:var(--text-muted)">加载失败:${err.message}</p>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderIncome() {
|
function renderIncome() {
|
||||||
|
|
@ -588,7 +650,7 @@ ${data.secrets || '无'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function publish() {
|
async function publish() {
|
||||||
if (!validateStep(currentStep)) return;
|
if (!validateStep(currentStep)) return;
|
||||||
|
|
||||||
const data = getFormData();
|
const data = getFormData();
|
||||||
|
|
@ -599,9 +661,37 @@ ${data.secrets || '无'}
|
||||||
generatedSoul = generateSoulMd(data);
|
generatedSoul = generateSoulMd(data);
|
||||||
generatedConfig = generateConfigYaml(data);
|
generatedConfig = generateConfigYaml(data);
|
||||||
|
|
||||||
|
// 持久化到数据库
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...data,
|
||||||
|
soulMd: generatedSoul,
|
||||||
|
configYaml: generatedConfig,
|
||||||
|
desc: data.personality.split(/[,,]/).slice(0, 2).join(','),
|
||||||
|
price: parseFloat(data.price) || 29.9,
|
||||||
|
avatar: data.avatar || `https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20${encodeURIComponent(data.displayName)}%20portrait&image_size=square`,
|
||||||
|
temperature: parseFloat(data.temperature) || 0.8,
|
||||||
|
maxTokens: parseInt(data.maxTokens) || 2048,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editingRoleId) {
|
||||||
|
await api(`/roles/${editingRoleId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await api('/roles', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
form.hidden = true;
|
form.hidden = true;
|
||||||
resultPanel.hidden = false;
|
resultPanel.hidden = false;
|
||||||
renderPreview();
|
renderPreview();
|
||||||
|
} catch (err) {
|
||||||
|
alert('保存失败:' + err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPreview() {
|
function renderPreview() {
|
||||||
|
|
@ -627,8 +717,53 @@ ${data.secrets || '无'}
|
||||||
generatedSoul = '';
|
generatedSoul = '';
|
||||||
generatedConfig = '';
|
generatedConfig = '';
|
||||||
activePreview = 'soul';
|
activePreview = 'soul';
|
||||||
|
editingRoleId = null;
|
||||||
updateStep(0);
|
updateStep(0);
|
||||||
updateTabs();
|
updateTabs();
|
||||||
|
updateSystemPromptPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载已有角色数据到表单(编辑模式)
|
||||||
|
async function loadRoleForEdit(roleId) {
|
||||||
|
try {
|
||||||
|
const { role } = await api(`/roles/${roleId}/full`);
|
||||||
|
editingRoleId = roleId;
|
||||||
|
form.hidden = false;
|
||||||
|
resultPanel.hidden = true;
|
||||||
|
|
||||||
|
// 填充表单字段
|
||||||
|
const fields = {
|
||||||
|
displayName: role.displayName,
|
||||||
|
gender: role.gender,
|
||||||
|
age: role.age || '',
|
||||||
|
relationship: role.relationship || '',
|
||||||
|
personality: role.personality,
|
||||||
|
background: role.background,
|
||||||
|
speechStyle: role.speechStyle,
|
||||||
|
likes: role.likes || '',
|
||||||
|
dislikes: role.dislikes || '',
|
||||||
|
memories: role.memories || '',
|
||||||
|
secrets: role.secrets || '',
|
||||||
|
greeting: role.greeting,
|
||||||
|
systemPrompt: role.systemPrompt || '',
|
||||||
|
model: role.model,
|
||||||
|
temperature: String(role.temperature),
|
||||||
|
maxTokens: String(role.maxTokens),
|
||||||
|
price: String(role.price),
|
||||||
|
};
|
||||||
|
Object.entries(fields).forEach(([name, value]) => {
|
||||||
|
const el = form.elements[name];
|
||||||
|
if (el) el.value = value;
|
||||||
|
});
|
||||||
|
if (form.elements.enableMemory) form.elements.enableMemory.checked = role.enableMemory;
|
||||||
|
if (form.elements.enableTools) form.elements.enableTools.checked = role.enableTools;
|
||||||
|
|
||||||
|
updateStep(0);
|
||||||
|
updateTabs();
|
||||||
|
updateSystemPromptPreview();
|
||||||
|
} catch (err) {
|
||||||
|
alert('加载角色数据失败:' + err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTabs() {
|
function updateTabs() {
|
||||||
|
|
@ -652,7 +787,7 @@ ${data.secrets || '无'}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', async (e) => {
|
||||||
// FAQ toggle (U4)
|
// FAQ toggle (U4)
|
||||||
const faqBtn = e.target.closest('.faq-q');
|
const faqBtn = e.target.closest('.faq-q');
|
||||||
if (faqBtn) {
|
if (faqBtn) {
|
||||||
|
|
@ -772,7 +907,8 @@ ${data.secrets || '无'}
|
||||||
|
|
||||||
if (action === 'edit-role') {
|
if (action === 'edit-role') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
resetCreator();
|
const roleId = target.dataset.roleId;
|
||||||
|
await loadRoleForEdit(roleId);
|
||||||
showView('creator');
|
showView('creator');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -848,14 +984,36 @@ ${data.secrets || '无'}
|
||||||
|
|
||||||
// --- Auth form submissions ---
|
// --- Auth form submissions ---
|
||||||
document.querySelectorAll('.auth-form').forEach((authForm) => {
|
document.querySelectorAll('.auth-form').forEach((authForm) => {
|
||||||
authForm.addEventListener('submit', (e) => {
|
authForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!validatePasswordMatch(authForm)) return;
|
if (!validatePasswordMatch(authForm)) return;
|
||||||
const formData = new FormData(authForm);
|
const formData = new FormData(authForm);
|
||||||
const data = Object.fromEntries(formData.entries());
|
const data = Object.fromEntries(formData.entries());
|
||||||
login(data.account);
|
const isRegister = authForm.dataset.form === 'register';
|
||||||
|
|
||||||
// After login, go to role library or creator center
|
try {
|
||||||
|
const endpoint = isRegister ? '/auth/register' : '/auth/login';
|
||||||
|
const result = await api(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ account: data.account, password: data.password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
setToken(result.token);
|
||||||
|
applyUserData(result.user);
|
||||||
|
|
||||||
|
// 注册成功后自动成为创作者
|
||||||
|
if (isRegister) {
|
||||||
|
await api('/auth/settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ isCreator: true, creatorName: data.account }),
|
||||||
|
});
|
||||||
|
state.isCreator = true;
|
||||||
|
state.creatorName = data.account;
|
||||||
|
saveState();
|
||||||
|
updateLandingCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录后跳转
|
||||||
if (state.isCreator) {
|
if (state.isCreator) {
|
||||||
showView('creator-center');
|
showView('creator-center');
|
||||||
renderCreatorCenter();
|
renderCreatorCenter();
|
||||||
|
|
@ -863,19 +1021,33 @@ ${data.secrets || '无'}
|
||||||
renderRoleLibrary();
|
renderRoleLibrary();
|
||||||
showView('role-library');
|
showView('role-library');
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Settings form (U7) ---
|
// --- Settings form (U7) ---
|
||||||
document.getElementById('settings-form').addEventListener('submit', (e) => {
|
document.getElementById('settings-form').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(e.target);
|
const formData = new FormData(e.target);
|
||||||
const data = Object.fromEntries(formData.entries());
|
const data = Object.fromEntries(formData.entries());
|
||||||
state.creatorName = data.creatorName || '';
|
try {
|
||||||
state.libraryName = data.libraryName || '我的 [XXX]';
|
const { user } = await api('/auth/settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
creatorName: data.creatorName,
|
||||||
|
libraryName: data.libraryName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
state.creatorName = user.creatorName || '';
|
||||||
|
state.libraryName = user.libraryName || '我的 [XXX]';
|
||||||
saveState();
|
saveState();
|
||||||
updateLandingCard();
|
updateLandingCard();
|
||||||
alert('设置已保存');
|
alert('设置已保存');
|
||||||
|
} catch (err) {
|
||||||
|
alert('保存失败:' + err.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Withdraw form (U7) ---
|
// --- Withdraw form (U7) ---
|
||||||
|
|
@ -904,4 +1076,17 @@ ${data.secrets || '无'}
|
||||||
updateTabBar('landing');
|
updateTabBar('landing');
|
||||||
initFaqA11y();
|
initFaqA11y();
|
||||||
initViewA11y();
|
initViewA11y();
|
||||||
|
|
||||||
|
// 页面加载时验证 token,恢复登录态
|
||||||
|
(async () => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) return;
|
||||||
|
try {
|
||||||
|
const { user } = await api('/auth/me');
|
||||||
|
applyUserData(user);
|
||||||
|
} catch {
|
||||||
|
// token 过期,清除
|
||||||
|
setToken('');
|
||||||
|
}
|
||||||
|
})();
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,76 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2.1"
|
"@prisma/client": "^5.22.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"prisma": "^5.22.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/client": {
|
||||||
|
"version": "5.22.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@prisma/client/-/client-5.22.0.tgz",
|
||||||
|
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.13"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prisma": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"prisma": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/debug": {
|
||||||
|
"version": "5.22.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@prisma/debug/-/debug-5.22.0.tgz",
|
||||||
|
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/engines": {
|
||||||
|
"version": "5.22.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@prisma/engines/-/engines-5.22.0.tgz",
|
||||||
|
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "5.22.0",
|
||||||
|
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||||
|
"@prisma/fetch-engine": "5.22.0",
|
||||||
|
"@prisma/get-platform": "5.22.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/engines-version": {
|
||||||
|
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
|
||||||
|
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/fetch-engine": {
|
||||||
|
"version": "5.22.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
|
||||||
|
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "5.22.0",
|
||||||
|
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||||
|
"@prisma/get-platform": "5.22.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/get-platform": {
|
||||||
|
"version": "5.22.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-5.22.0.tgz",
|
||||||
|
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "5.22.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
|
|
@ -25,6 +94,15 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.3.0.tgz",
|
||||||
|
|
@ -62,6 +140,12 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
|
@ -140,6 +224,23 @@
|
||||||
"node": ">=6.6.0"
|
"node": ">=6.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cors": {
|
||||||
|
"version": "2.8.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz",
|
||||||
|
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": "^4",
|
||||||
|
"vary": "^1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||||
|
|
@ -166,6 +267,18 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.4.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz",
|
||||||
|
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -180,6 +293,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
|
@ -322,6 +444,20 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|
@ -461,6 +597,91 @@
|
||||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -531,6 +752,15 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|
@ -583,6 +813,25 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prisma": {
|
||||||
|
"version": "5.22.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/prisma/-/prisma-5.22.0.tgz",
|
||||||
|
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/engines": "5.22.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prisma": "build/index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.13"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|
@ -651,12 +900,44 @@
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
|
||||||
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz",
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -5,6 +5,9 @@
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js",
|
||||||
|
"db:push": "prisma db push",
|
||||||
|
"db:studio": "prisma studio",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"keywords": ["ai", "eternal", "agent", "hermes"],
|
"keywords": ["ai", "eternal", "agent", "hermes"],
|
||||||
|
|
@ -15,6 +18,12 @@
|
||||||
"url": "http://gitea.fischerai.cn/chigulong/eternalai.git"
|
"url": "http://gitea.fischerai.cn/chigulong/eternalai.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2.1"
|
"@prisma/client": "^5.22.0",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.0",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"prisma": "^5.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
account String @unique
|
||||||
|
password String
|
||||||
|
isCreator Boolean @default(false)
|
||||||
|
creatorName String?
|
||||||
|
libraryName String?
|
||||||
|
boundCreator String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
roles Role[]
|
||||||
|
orders Order[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Role {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
creatorId String
|
||||||
|
displayName String
|
||||||
|
gender String @default("unknown")
|
||||||
|
age String?
|
||||||
|
relationship String?
|
||||||
|
personality String
|
||||||
|
background String
|
||||||
|
speechStyle String
|
||||||
|
likes String?
|
||||||
|
dislikes String?
|
||||||
|
memories String?
|
||||||
|
secrets String?
|
||||||
|
greeting String
|
||||||
|
systemPrompt String?
|
||||||
|
model String @default("gpt-4o")
|
||||||
|
temperature Float @default(0.8)
|
||||||
|
maxTokens Int @default(2048)
|
||||||
|
enableMemory Boolean @default(true)
|
||||||
|
enableTools Boolean @default(false)
|
||||||
|
agentId String?
|
||||||
|
soulMd String?
|
||||||
|
configYaml String?
|
||||||
|
avatar String?
|
||||||
|
desc String?
|
||||||
|
price Float @default(0)
|
||||||
|
status String @default("running")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
creator User @relation(fields: [creatorId], references: [id])
|
||||||
|
orders Order[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Order {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
roleId String
|
||||||
|
amount Float
|
||||||
|
status String @default("paid")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
|
}
|
||||||
22
server.js
22
server.js
|
|
@ -1,17 +1,27 @@
|
||||||
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const app = express();
|
const cors = require('cors');
|
||||||
const PORT = 3001;
|
|
||||||
|
|
||||||
// Serve static files from the root directory
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
// 中间件
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// API 路由
|
||||||
|
app.use('/api/auth', require('./src/routes/auth'));
|
||||||
|
app.use('/api/roles', require('./src/routes/roles'));
|
||||||
|
|
||||||
|
// 静态文件
|
||||||
app.use(express.static('.'));
|
app.use(express.static('.'));
|
||||||
|
|
||||||
// Route for the main page
|
// 主页
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'index.html'));
|
res.sendFile(path.join(__dirname, 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the server
|
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`EternalAI server is running on http://0.0.0.0:${PORT}`);
|
console.log(`EternalAI server running on http://0.0.0.0:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'eternalai_jwt_secret_2026_change_in_prod';
|
||||||
|
const JWT_EXPIRES_IN = '7d';
|
||||||
|
|
||||||
|
// 哈希密码
|
||||||
|
function hashPassword(password) {
|
||||||
|
return bcrypt.hashSync(password, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
function verifyPassword(password, hash) {
|
||||||
|
return bcrypt.compareSync(password, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 JWT
|
||||||
|
function signToken(userId) {
|
||||||
|
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 JWT 并提取 userId
|
||||||
|
function verifyToken(token) {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET);
|
||||||
|
return decoded.userId;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Express 中间件:验证 JWT
|
||||||
|
function authMiddleware(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({ error: '未登录' });
|
||||||
|
}
|
||||||
|
const token = authHeader.slice(7);
|
||||||
|
const userId = verifyToken(token);
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(401).json({ error: '登录已过期,请重新登录' });
|
||||||
|
}
|
||||||
|
req.userId = userId;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hashPassword,
|
||||||
|
verifyPassword,
|
||||||
|
signToken,
|
||||||
|
verifyToken,
|
||||||
|
authMiddleware,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
module.exports = prisma;
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
const express = require('express');
|
||||||
|
const prisma = require('../lib/prisma');
|
||||||
|
const { hashPassword, verifyPassword, signToken, authMiddleware } = require('../lib/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
router.post('/register', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { account, password } = req.body;
|
||||||
|
if (!account || !password) {
|
||||||
|
return res.status(400).json({ error: '账号和密码不能为空' });
|
||||||
|
}
|
||||||
|
if (password.length < 6) {
|
||||||
|
return res.status(400).json({ error: '密码至少 6 位' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await prisma.user.findUnique({ where: { account } });
|
||||||
|
if (existing) {
|
||||||
|
return res.status(409).json({ error: '该账号已注册' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
account,
|
||||||
|
password: hashPassword(password),
|
||||||
|
libraryName: '我的角色库',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = signToken(user.id);
|
||||||
|
res.json({
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
account: user.account,
|
||||||
|
isCreator: user.isCreator,
|
||||||
|
creatorName: user.creatorName,
|
||||||
|
libraryName: user.libraryName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('注册失败:', err);
|
||||||
|
res.status(500).json({ error: '注册失败,请稍后重试' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { account, password } = req.body;
|
||||||
|
if (!account || !password) {
|
||||||
|
return res.status(400).json({ error: '账号和密码不能为空' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({ where: { account } });
|
||||||
|
if (!user || !verifyPassword(password, user.password)) {
|
||||||
|
return res.status(401).json({ error: '账号或密码错误' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = signToken(user.id);
|
||||||
|
res.json({
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
account: user.account,
|
||||||
|
isCreator: user.isCreator,
|
||||||
|
creatorName: user.creatorName,
|
||||||
|
libraryName: user.libraryName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('登录失败:', err);
|
||||||
|
res.status(500).json({ error: '登录失败,请稍后重试' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
router.get('/me', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: req.userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
account: true,
|
||||||
|
isCreator: true,
|
||||||
|
creatorName: true,
|
||||||
|
libraryName: true,
|
||||||
|
boundCreator: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: '用户不存在' });
|
||||||
|
}
|
||||||
|
res.json({ user });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取用户信息失败:', err);
|
||||||
|
res.status(500).json({ error: '服务器错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新用户设置
|
||||||
|
router.put('/settings', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { creatorName, libraryName, isCreator } = req.body;
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: { id: req.userId },
|
||||||
|
data: {
|
||||||
|
...(creatorName !== undefined && { creatorName }),
|
||||||
|
...(libraryName !== undefined && { libraryName }),
|
||||||
|
...(isCreator !== undefined && { isCreator }),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
account: true,
|
||||||
|
isCreator: true,
|
||||||
|
creatorName: true,
|
||||||
|
libraryName: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.json({ user });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('更新设置失败:', err);
|
||||||
|
res.status(500).json({ error: '更新失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
const express = require('express');
|
||||||
|
const prisma = require('../lib/prisma');
|
||||||
|
const { authMiddleware } = require('../lib/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// 获取角色库(所有已上架角色)
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const roles = await prisma.role.findMany({
|
||||||
|
where: { status: 'running' },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
displayName: true,
|
||||||
|
avatar: true,
|
||||||
|
desc: true,
|
||||||
|
price: true,
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.json({ roles });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取角色列表失败:', err);
|
||||||
|
res.status(500).json({ error: '服务器错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取角色详情
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const role = await prisma.role.findUnique({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
displayName: true,
|
||||||
|
avatar: true,
|
||||||
|
desc: true,
|
||||||
|
price: true,
|
||||||
|
status: true,
|
||||||
|
gender: true,
|
||||||
|
age: true,
|
||||||
|
relationship: true,
|
||||||
|
personality: true,
|
||||||
|
background: true,
|
||||||
|
speechStyle: true,
|
||||||
|
greeting: true,
|
||||||
|
creatorId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!role) {
|
||||||
|
return res.status(404).json({ error: '角色不存在' });
|
||||||
|
}
|
||||||
|
res.json({ role });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取角色详情失败:', err);
|
||||||
|
res.status(500).json({ error: '服务器错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取当前用户创建的角色
|
||||||
|
router.get('/my/roles', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const roles = await prisma.role.findMany({
|
||||||
|
where: { creatorId: req.userId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
res.json({ roles });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取我的角色失败:', err);
|
||||||
|
res.status(500).json({ error: '服务器错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发布新角色
|
||||||
|
router.post('/', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = req.body;
|
||||||
|
if (!data.displayName || !data.greeting || !data.personality || !data.background || !data.speechStyle) {
|
||||||
|
return res.status(400).json({ error: '必填字段缺失' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await prisma.role.create({
|
||||||
|
data: {
|
||||||
|
creatorId: req.userId,
|
||||||
|
displayName: data.displayName,
|
||||||
|
gender: data.gender || 'unknown',
|
||||||
|
age: data.age || null,
|
||||||
|
relationship: data.relationship || null,
|
||||||
|
personality: data.personality,
|
||||||
|
background: data.background,
|
||||||
|
speechStyle: data.speechStyle,
|
||||||
|
likes: data.likes || null,
|
||||||
|
dislikes: data.dislikes || null,
|
||||||
|
memories: data.memories || null,
|
||||||
|
secrets: data.secrets || null,
|
||||||
|
greeting: data.greeting,
|
||||||
|
systemPrompt: data.systemPrompt || null,
|
||||||
|
model: data.model || 'gpt-4o',
|
||||||
|
temperature: parseFloat(data.temperature) || 0.8,
|
||||||
|
maxTokens: parseInt(data.maxTokens) || 2048,
|
||||||
|
enableMemory: data.enableMemory ?? true,
|
||||||
|
enableTools: data.enableTools ?? false,
|
||||||
|
agentId: data.agentId || null,
|
||||||
|
soulMd: data.soulMd || null,
|
||||||
|
configYaml: data.configYaml || null,
|
||||||
|
avatar: data.avatar || null,
|
||||||
|
desc: data.desc || data.personality.slice(0, 50),
|
||||||
|
price: parseFloat(data.price) || 0,
|
||||||
|
status: 'running',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ role });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('发布角色失败:', err);
|
||||||
|
res.status(500).json({ error: '发布失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 编辑角色
|
||||||
|
router.put('/:id', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const existing = await prisma.role.findUnique({ where: { id: req.params.id } });
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: '角色不存在' });
|
||||||
|
}
|
||||||
|
if (existing.creatorId !== req.userId) {
|
||||||
|
return res.status(403).json({ error: '无权编辑他人角色' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
const role = await prisma.role.update({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
data: {
|
||||||
|
displayName: data.displayName ?? existing.displayName,
|
||||||
|
gender: data.gender ?? existing.gender,
|
||||||
|
age: data.age ?? existing.age,
|
||||||
|
relationship: data.relationship ?? existing.relationship,
|
||||||
|
personality: data.personality ?? existing.personality,
|
||||||
|
background: data.background ?? existing.background,
|
||||||
|
speechStyle: data.speechStyle ?? existing.speechStyle,
|
||||||
|
likes: data.likes ?? existing.likes,
|
||||||
|
dislikes: data.dislikes ?? existing.dislikes,
|
||||||
|
memories: data.memories ?? existing.memories,
|
||||||
|
secrets: data.secrets ?? existing.secrets,
|
||||||
|
greeting: data.greeting ?? existing.greeting,
|
||||||
|
systemPrompt: data.systemPrompt ?? existing.systemPrompt,
|
||||||
|
model: data.model ?? existing.model,
|
||||||
|
temperature: parseFloat(data.temperature) || existing.temperature,
|
||||||
|
maxTokens: parseInt(data.maxTokens) || existing.maxTokens,
|
||||||
|
enableMemory: data.enableMemory ?? existing.enableMemory,
|
||||||
|
enableTools: data.enableTools ?? existing.enableTools,
|
||||||
|
soulMd: data.soulMd ?? existing.soulMd,
|
||||||
|
configYaml: data.configYaml ?? existing.configYaml,
|
||||||
|
avatar: data.avatar ?? existing.avatar,
|
||||||
|
desc: data.desc ?? existing.desc,
|
||||||
|
price: parseFloat(data.price) || existing.price,
|
||||||
|
status: data.status ?? existing.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ role });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('编辑角色失败:', err);
|
||||||
|
res.status(500).json({ error: '编辑失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取角色详情(含 Soul.md 和 config.yaml,仅创建者可访问)
|
||||||
|
router.get('/:id/full', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const role = await prisma.role.findUnique({ where: { id: req.params.id } });
|
||||||
|
if (!role) {
|
||||||
|
return res.status(404).json({ error: '角色不存在' });
|
||||||
|
}
|
||||||
|
if (role.creatorId !== req.userId) {
|
||||||
|
return res.status(403).json({ error: '无权查看' });
|
||||||
|
}
|
||||||
|
res.json({ role });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取角色完整信息失败:', err);
|
||||||
|
res.status(500).json({ error: '服务器错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Loading…
Reference in New Issue