migration to postgres
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,16 +46,16 @@ export const userMiddleware: MiddlewareHandler = async function (ctx, next) {
|
||||
|
||||
// Check if token is blacklisted (explicit signout)
|
||||
if (user.jti) {
|
||||
if (isTokenBlacklisted(user.jti)) throw errors.UNAUTHORIZED();
|
||||
if (await isTokenBlacklisted(user.jti)) throw errors.UNAUTHORIZED();
|
||||
}
|
||||
|
||||
// Check if token was issued before password change
|
||||
if (user.iat && user.id) {
|
||||
const dbUser = getUserById(user.id);
|
||||
const dbUser = await getUserById(user.id);
|
||||
if (dbUser?.passwordChangedAt) {
|
||||
// iat is in seconds, passwordChangedAt is in milliseconds
|
||||
// iat is in seconds, passwordChangedAt is a Date
|
||||
const tokenIssuedAt = user.iat * 1000;
|
||||
if (tokenIssuedAt < dbUser.passwordChangedAt) {
|
||||
if (tokenIssuedAt < dbUser.passwordChangedAt.getTime()) {
|
||||
throw errors.UNAUTHORIZED();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const bootstrapHandler: Handler = async function (ctx) {
|
||||
const token = body.token as string;
|
||||
const email = body.email as string;
|
||||
|
||||
const userCount = getUserCount();
|
||||
const userCount = await getUserCount();
|
||||
if (userCount > 0) throw errors.FORBIDDEN('Registration is closed');
|
||||
|
||||
if (!token) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const changePasswordHandler: Handler = async function (ctx) {
|
||||
if (isProduction) validatePassword(newPassword);
|
||||
const reqUser = ctx.get('user');
|
||||
|
||||
const dbUser = getUserById(reqUser.id);
|
||||
const dbUser = await getUserById(reqUser.id);
|
||||
|
||||
if (!dbUser) throw errors.UNAUTHORIZED();
|
||||
|
||||
@@ -23,8 +23,7 @@ export const changePasswordHandler: Handler = async function (ctx) {
|
||||
}
|
||||
|
||||
const newPasswordHash = await argon2.hash(newPassword);
|
||||
// Use floored seconds-to-ms so the token iat (also floored) is never behind
|
||||
const passwordChangedAt = Math.floor(Date.now() / 1000) * 1000;
|
||||
const passwordChangedAt = new Date();
|
||||
await updateUser(reqUser.id, { password: newPasswordHash, passwordChangedAt });
|
||||
|
||||
const { id, email, name, role } = reqUser;
|
||||
|
||||
@@ -7,7 +7,7 @@ export const forgotPasswordHandler: Handler = async function (ctx) {
|
||||
const { email } = ctx.get('body');
|
||||
const origin = ctx.get('origin');
|
||||
|
||||
const dbUser = getUserByEmail(email);
|
||||
const dbUser = await getUserByEmail(email);
|
||||
|
||||
if (!dbUser) return ctx.json({ ok: true });
|
||||
const verificationCode = await sign({ id: dbUser.id, email, purpose: 'reset-password' }, '6h');
|
||||
|
||||
@@ -6,7 +6,7 @@ import { sign } from '../../jwt';
|
||||
import * as errors from '../../custom-errors';
|
||||
import {
|
||||
getUserByEmail,
|
||||
getPasskeysByEmailAndOrigin,
|
||||
getPasskeysByUserIdAndOrigin,
|
||||
getPasskeyByCredentialId,
|
||||
createPasskey,
|
||||
updatePasskey,
|
||||
@@ -41,8 +41,11 @@ const passkeyRouterPostChallenge: Handler = async (ctx) => {
|
||||
const origin = ctx.get('origin') as string;
|
||||
const rpId = getRpId(origin);
|
||||
|
||||
const dbUser = await getUserByEmail(email!);
|
||||
if (!dbUser) throw errors.NOT_FOUND('User not found');
|
||||
|
||||
// Get existing passkeys to exclude them
|
||||
const existingPasskeys = getPasskeysByEmailAndOrigin(email!, origin);
|
||||
const existingPasskeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin);
|
||||
|
||||
const options = await generateRegistrationOptions({
|
||||
rpName: RP_NAME,
|
||||
@@ -59,7 +62,7 @@ const passkeyRouterPostChallenge: Handler = async (ctx) => {
|
||||
},
|
||||
});
|
||||
|
||||
await storeChallenge(email!, origin, options.challenge);
|
||||
await storeChallenge(dbUser.id, origin, options.challenge, CHALLENGE_TTL_MS);
|
||||
return ctx.json(options);
|
||||
};
|
||||
passkeyRouter.post('/challenge/:email', passkeyRateLimiter, passkeyRouterPostChallenge);
|
||||
@@ -68,10 +71,10 @@ passkeyRouter.post('/challenge/:email', passkeyRateLimiter, passkeyRouterPostCha
|
||||
const passkeyRouterPost: Handler = async (ctx) => {
|
||||
const origin = ctx.get('origin') as string;
|
||||
const rpId = getRpId(origin);
|
||||
const { email } = ctx.get('user') as User;
|
||||
const user = ctx.get('user') as User;
|
||||
const response = ctx.get('body') as RegistrationResponseJSON;
|
||||
|
||||
const storedChallenge = await consumeChallenge(email, origin, CHALLENGE_TTL_MS);
|
||||
const storedChallenge = await consumeChallenge(user.id, origin);
|
||||
if (!storedChallenge) throw errors.BAD_CREDENTIALS();
|
||||
|
||||
const verification = await verifyRegistrationResponse({
|
||||
@@ -88,7 +91,7 @@ const passkeyRouterPost: Handler = async (ctx) => {
|
||||
const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
|
||||
|
||||
await createPasskey({
|
||||
email,
|
||||
userId: user.id,
|
||||
origin,
|
||||
credentialId: credential.id,
|
||||
publicKey: Buffer.from(credential.publicKey).toString('base64'),
|
||||
@@ -105,7 +108,10 @@ const passkeyRouterGet: Handler = async (ctx) => {
|
||||
const origin = ctx.get('origin') as string;
|
||||
const rpId = getRpId(origin);
|
||||
|
||||
const passkeys = getPasskeysByEmailAndOrigin(email!, origin);
|
||||
const dbUser = await getUserByEmail(email!);
|
||||
if (!dbUser) throw errors.NOT_FOUND('User not found');
|
||||
|
||||
const passkeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin);
|
||||
|
||||
const options = await generateAuthenticationOptions({
|
||||
rpID: rpId,
|
||||
@@ -115,7 +121,7 @@ const passkeyRouterGet: Handler = async (ctx) => {
|
||||
userVerification: 'preferred',
|
||||
});
|
||||
|
||||
await storeChallenge(email!, origin, options.challenge);
|
||||
await storeChallenge(dbUser.id, origin, options.challenge, CHALLENGE_TTL_MS);
|
||||
return ctx.json(options);
|
||||
};
|
||||
passkeyRouter.get('/signin/:email', passkeyRateLimiter, passkeyRouterGet);
|
||||
@@ -127,11 +133,14 @@ const passkeyRouterPostVerify: Handler = async (ctx) => {
|
||||
const rpId = getRpId(origin);
|
||||
const response = ctx.get('body') as AuthenticationResponseJSON;
|
||||
|
||||
const storedChallenge = await consumeChallenge(email!, origin, CHALLENGE_TTL_MS);
|
||||
const dbUser = await getUserByEmail(email!);
|
||||
if (!dbUser) throw errors.UNAUTHORIZED();
|
||||
|
||||
const storedChallenge = await consumeChallenge(dbUser.id, origin);
|
||||
if (!storedChallenge) throw errors.BAD_CREDENTIALS();
|
||||
|
||||
// Find the passkey being used
|
||||
const dbPasskey = getPasskeyByCredentialId(email!, response.id);
|
||||
const dbPasskey = await getPasskeyByCredentialId(dbUser.id, response.id);
|
||||
|
||||
if (!dbPasskey || !dbPasskey.publicKey) throw errors.BAD_CREDENTIALS();
|
||||
|
||||
@@ -152,10 +161,7 @@ const passkeyRouterPostVerify: Handler = async (ctx) => {
|
||||
// Update counter to prevent replay attacks
|
||||
await updatePasskey(dbPasskey.id, { counter: verification.authenticationInfo.newCounter });
|
||||
|
||||
const dbUser = getUserByEmail(email!);
|
||||
if (!dbUser) throw errors.UNAUTHORIZED();
|
||||
|
||||
const passkeys = getPasskeysByEmailAndOrigin(email!, origin);
|
||||
const passkeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin);
|
||||
|
||||
const { id, name, username, role } = dbUser;
|
||||
const token = await sign({
|
||||
|
||||
@@ -10,7 +10,7 @@ export const resendVerificationHandler: Handler = async function (ctx) {
|
||||
|
||||
if (!email || typeof email !== 'string') throw errors.BAD_REQUEST('Email is required');
|
||||
|
||||
const user = getUserByEmail(email);
|
||||
const user = await getUserByEmail(email);
|
||||
if (!user) throw errors.NOT_FOUND('User not found');
|
||||
if (user.status !== 'Unverified') throw errors.BAD_REQUEST('Account is already verified');
|
||||
|
||||
|
||||
@@ -7,13 +7,12 @@ import * as errors from '@@/custom-errors';
|
||||
import { validatePassword } from './validate-password';
|
||||
|
||||
export const resetPasswordHandler: Handler = async function (ctx) {
|
||||
const now = Date.now().valueOf();
|
||||
const { password, verificationCode } = ctx.get('body');
|
||||
validatePassword(password);
|
||||
const userInfo = (await verify(verificationCode)) as User;
|
||||
if (!userInfo) throw errors.UNAUTHORIZED();
|
||||
const passwordHash = await argon2.hash(password);
|
||||
await updateUser(userInfo.id, { password: passwordHash, status: 'Active', passwordChangedAt: now });
|
||||
await updateUser(userInfo.id, { password: passwordHash, status: 'Active', passwordChangedAt: new Date() });
|
||||
|
||||
return ctx.json({ ok: true });
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Handler } from 'hono';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { getUserByEmail, getPasskeysByEmailAndOrigin } from 'officerdb';
|
||||
import { getUserByEmail, getPasskeysByUserIdAndOrigin } from 'officerdb';
|
||||
import { sign } from '@@/jwt';
|
||||
import { getClaudeDir } from '@@/data-path';
|
||||
import { syncUserPiConfig } from '@@/api/server-settings/sync-user-pi-config';
|
||||
@@ -13,11 +13,12 @@ const TEST_USERS: number[] = [];
|
||||
export const signinHandler: Handler = async function (ctx) {
|
||||
const { email, password } = ctx.get('body');
|
||||
const origin = ctx.get('origin');
|
||||
const dbUser = getUserByEmail(email);
|
||||
const dbUser = await getUserByEmail(email);
|
||||
if (!dbUser) throw errors.UNAUTHORIZED();
|
||||
|
||||
const passkeys = getPasskeysByEmailAndOrigin(email, origin);
|
||||
const passkeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin);
|
||||
|
||||
if (!dbUser || !dbUser.password) throw errors.UNAUTHORIZED();
|
||||
if (!dbUser.password) throw errors.UNAUTHORIZED();
|
||||
const { status } = dbUser;
|
||||
if (status !== 'Active') throw errors.UNAUTHORIZED();
|
||||
const isValidPassword = TEST_USERS.includes(dbUser.id) || (await argon2.verify(dbUser.password, password));
|
||||
|
||||
@@ -13,7 +13,7 @@ export const signupHandler: Handler = async function (ctx) {
|
||||
throw errors.BAD_REQUEST('Invalid email address');
|
||||
}
|
||||
|
||||
const userCount = getUserCount();
|
||||
const userCount = await getUserCount();
|
||||
if (userCount > 0) throw errors.FORBIDDEN('Registration is closed');
|
||||
|
||||
const dbUser = await createUser({
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { Handler } from 'hono';
|
||||
import type { User } from 'types';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import { getUserById, getPasskeysByEmailAndOrigin } from 'officerdb';
|
||||
import { getUserById, getPasskeysByUserIdAndOrigin } from 'officerdb';
|
||||
|
||||
export const usersMe: Handler = async function (ctx) {
|
||||
const user = ctx.get('user') as User;
|
||||
const origin = ctx.get('origin') as string;
|
||||
|
||||
const dbUser = getUserById(user.id);
|
||||
const dbUser = await getUserById(user.id);
|
||||
|
||||
if (!dbUser) return errors.NOT_FOUND();
|
||||
|
||||
const passkeys = getPasskeysByEmailAndOrigin(dbUser.email, origin || '');
|
||||
const passkeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin || '');
|
||||
const { password, ...userWithoutPassword } = dbUser;
|
||||
const returnUser = { ...userWithoutPassword, passkeyCount: passkeys.length };
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export const verifyTokenHandler: Handler = async function (ctx) {
|
||||
|
||||
if (!userInfo?.id) throw errors.BAD_REQUEST('Token is invalid or expired');
|
||||
|
||||
const user = getUserById(userInfo.id);
|
||||
const user = await getUserById(userInfo.id);
|
||||
if (!user) throw errors.NOT_FOUND('User not found');
|
||||
|
||||
// Reset-password tokens skip the verification status check
|
||||
|
||||
@@ -11,7 +11,7 @@ export const verifyHandler: Handler = async function (ctx) {
|
||||
const userInfo = (await verifyJwt(verificationCode)) as User;
|
||||
if (!userInfo) throw errors.BAD_REQUEST();
|
||||
|
||||
const user = getUserById(userInfo.id);
|
||||
const user = await getUserById(userInfo.id);
|
||||
if (!user) throw errors.NOT_FOUND('User not found');
|
||||
|
||||
const updates: Record<string, unknown> = { status: 'Active' };
|
||||
@@ -38,7 +38,7 @@ export const verifyHandler: Handler = async function (ctx) {
|
||||
await updateUser(userInfo.id, updates);
|
||||
|
||||
// Re-fetch user to get final values after update
|
||||
const finalUser = getUserById(userInfo.id);
|
||||
const finalUser = await getUserById(userInfo.id);
|
||||
if (!finalUser) throw errors.NOT_FOUND('User not found');
|
||||
|
||||
// Issue a token so the user is logged in immediately
|
||||
|
||||
@@ -267,7 +267,7 @@ async function validateJwt(req: Request): Promise<boolean> {
|
||||
try {
|
||||
const payload = await verify(token);
|
||||
if (!payload) return false;
|
||||
if (payload.jti && isTokenBlacklisted(payload.jti)) return false;
|
||||
if (payload.jti && await isTokenBlacklisted(payload.jti)) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -4,6 +4,6 @@ import { getUserCount } from 'officerdb';
|
||||
export const landingPageDataRouter = createRouter();
|
||||
|
||||
landingPageDataRouter.get('/', async (ctx) => {
|
||||
const userCount = getUserCount();
|
||||
const userCount = await getUserCount();
|
||||
return ctx.json({ registrationOpen: userCount === 0 });
|
||||
});
|
||||
|
||||
@@ -5,8 +5,9 @@ import type { Subprocess } from "bun";
|
||||
import type { PiEvent, MessageCost } from "./types";
|
||||
import { readApiKeys } from "../server-settings/pi-mono";
|
||||
import { readSearxngConfig } from "../server-settings/searxng";
|
||||
import { PI_CONFIG_DIR, DATA_PATH, getHomeDir, getGlobalSkillsDir, getUserSkillsDir, getGlobalExtensionsDir, getUserExtensionsDir, getGlobalToolsDir, getUserToolsDir, getNativeResourcesDir, getGlobalResourcesDir } from "../../data-path";
|
||||
import { PI_CONFIG_DIR, DATA_PATH, SERVER_CONFIG_DIR, getHomeDir, getGlobalSkillsDir, getUserSkillsDir, getGlobalExtensionsDir, getUserExtensionsDir, getGlobalToolsDir, getUserToolsDir, getNativeResourcesDir, getGlobalResourcesDir } from "../../data-path";
|
||||
import { ensureDockerContainer } from "../terminal/websocket";
|
||||
import { getServerIntegration, getUserIntegration } from "officerdb";
|
||||
import { logger } from "./logger";
|
||||
import { parseFrontmatter } from "../skills/skills";
|
||||
|
||||
@@ -159,12 +160,33 @@ function buildResourcesEnv(): string {
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
function getGoogleConfigPath(): string {
|
||||
return join(homedir(), '.config', 'officer.dev', 'google-oauth.json');
|
||||
async function ensureGoogleConfigFile(): Promise<string> {
|
||||
const filePath = join(SERVER_CONFIG_DIR, 'google-oauth.json');
|
||||
try {
|
||||
const integration = await getServerIntegration('google');
|
||||
if (integration?.config) {
|
||||
mkdirSync(SERVER_CONFIG_DIR, { recursive: true });
|
||||
writeFileSync(filePath, JSON.stringify(integration.config, null, 2));
|
||||
}
|
||||
} catch {
|
||||
// No google config available
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function getGoogleTokenPath(email: string): string {
|
||||
return join(DATA_PATH, email, 'integrations', 'google.json');
|
||||
async function ensureGoogleTokenFile(userId: number, email: string): Promise<string> {
|
||||
const dir = join(DATA_PATH, email, 'integrations');
|
||||
const filePath = join(dir, 'google.json');
|
||||
try {
|
||||
const integration = await getUserIntegration(userId, 'google');
|
||||
if (integration?.config) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(filePath, JSON.stringify(integration.config, null, 2));
|
||||
}
|
||||
} catch {
|
||||
// No user google integration available
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
type SandboxOptions = {
|
||||
@@ -177,6 +199,7 @@ type SandboxOptions = {
|
||||
export async function spawnPi(
|
||||
cwd: string,
|
||||
model: string,
|
||||
userId: number,
|
||||
email: string,
|
||||
onEvent: PiEventHandler,
|
||||
sandbox?: SandboxOptions,
|
||||
@@ -217,8 +240,8 @@ export async function spawnPi(
|
||||
|
||||
const resourcesEnv = buildResourcesEnv();
|
||||
|
||||
const googleConfigHost = getGoogleConfigPath();
|
||||
const googleTokenHost = join(DATA_PATH, sandbox.email, 'integrations');
|
||||
const googleConfigHost = await ensureGoogleConfigFile();
|
||||
await ensureGoogleTokenFile(sandbox.userId, sandbox.email);
|
||||
|
||||
const envFlags = [
|
||||
'-e', `PI_CODING_AGENT_DIR=${containerPiConfig}`,
|
||||
@@ -275,13 +298,28 @@ export async function spawnPi(
|
||||
}
|
||||
|
||||
const toolsDirs = [getGlobalToolsDir(), getUserToolsDir(email)].join(':');
|
||||
const googleConfigPath = await ensureGoogleConfigFile();
|
||||
const googleTokenPath = await ensureGoogleTokenFile(userId, email);
|
||||
|
||||
proc = Bun.spawn(args, {
|
||||
cwd,
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
env: { ...process.env, ...storedKeys, HOME: getHomeDir(email), OFFICER_USER_HOME: getHomeDir(email), OFFICER_USER_ROOT: join(DATA_PATH, email), PI_CODING_AGENT_DIR: PI_CONFIG_DIR, PI_TOOLS_DIRS: toolsDirs, PI_SEARXNG_URL: searxng.url, OFFICER_RESOURCES: buildResourcesEnv(), OFFICER_GOOGLE_CONFIG_PATH: getGoogleConfigPath(), OFFICER_GOOGLE_TOKEN_PATH: getGoogleTokenPath(email), OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db') },
|
||||
env: {
|
||||
...process.env,
|
||||
...storedKeys,
|
||||
HOME: getHomeDir(email),
|
||||
OFFICER_USER_HOME: getHomeDir(email),
|
||||
OFFICER_USER_ROOT: join(DATA_PATH, email),
|
||||
PI_CODING_AGENT_DIR: PI_CONFIG_DIR,
|
||||
PI_TOOLS_DIRS: toolsDirs,
|
||||
PI_SEARXNG_URL: searxng.url,
|
||||
OFFICER_RESOURCES: buildResourcesEnv(),
|
||||
OFFICER_GOOGLE_CONFIG_PATH: googleConfigPath,
|
||||
OFFICER_GOOGLE_TOKEN_PATH: googleTokenPath,
|
||||
OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db'),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('Spawned Pi locally', {
|
||||
|
||||
@@ -6,22 +6,20 @@ import * as storage from './storage';
|
||||
import * as piBridge from './pi-bridge';
|
||||
import { join, resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { getHomeDir, getUserSettingsFile } from '../../../servers/data-path';
|
||||
import { getHomeDir } from '../../../servers/data-path';
|
||||
import { getUserSettings } from 'officerdb';
|
||||
import { logger } from './logger';
|
||||
|
||||
// Default model when no user preference is set
|
||||
const DEFAULT_MODEL = 'opencode/big-pickle';
|
||||
|
||||
async function getUserDefaultModel(email: string): Promise<string | null> {
|
||||
async function getUserDefaultModel(userId: number): Promise<string | null> {
|
||||
try {
|
||||
const settingsPath = getUserSettingsFile(email);
|
||||
const file = Bun.file(settingsPath);
|
||||
if (await file.exists()) {
|
||||
const settings = await file.json();
|
||||
return settings?.chat?.defaultModel || null;
|
||||
}
|
||||
const settings = await getUserSettings(userId);
|
||||
const chat = settings?.chat as Record<string, unknown> | undefined;
|
||||
return (chat?.defaultModel as string) || null;
|
||||
} catch (err) {
|
||||
logger.error('Failed to read user settings for default model', { email, error: String(err) });
|
||||
logger.error('Failed to read user settings for default model', { userId, error: String(err) });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -254,7 +252,7 @@ async function handleChat(
|
||||
let modelSource = 'client-provided';
|
||||
let userDefault = null;
|
||||
if (!model) {
|
||||
userDefault = await getUserDefaultModel(email);
|
||||
userDefault = await getUserDefaultModel(userId);
|
||||
if (userDefault) {
|
||||
model = userDefault;
|
||||
modelSource = 'user-settings';
|
||||
@@ -288,7 +286,7 @@ async function handleChat(
|
||||
if (!session.piProcess) {
|
||||
try {
|
||||
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
|
||||
session.piProcess = await piBridge.spawnPi(cwd, model, email, onEvent, sandboxed ? { userId, username, email, homeDir } : undefined);
|
||||
session.piProcess = await piBridge.spawnPi(cwd, model, userId, email, onEvent, sandboxed ? { userId, username, email, homeDir } : undefined);
|
||||
logger.info('Spawned Pi process for session', { sessionId, model, cwd, sandboxed });
|
||||
} catch (err) {
|
||||
logger.error('Failed to spawn Pi process', { sessionId, model, error: String(err) });
|
||||
@@ -363,7 +361,7 @@ async function handleResume(
|
||||
const homeDir = getHomeDir(email);
|
||||
const sandbox = session.sandboxed && session.userId ? { userId: session.userId, username: ws.data.username, email, homeDir } : undefined;
|
||||
const onEvent = createEventHandler(sessionId, session.model, session.cwd, homeDir);
|
||||
session.piProcess = await piBridge.spawnPi(session.cwd, session.model, email, onEvent, sandbox);
|
||||
session.piProcess = await piBridge.spawnPi(session.cwd, session.model, session.userId!, email, onEvent, sandbox);
|
||||
logger.info('Spawned fresh Pi process for resumed session', { sessionId, model: session.model, sandboxed: session.sandboxed });
|
||||
} catch (err) {
|
||||
logger.error('Failed to spawn Pi process for resume', { sessionId, error: String(err) });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { settingsPath } from './server-settings';
|
||||
import { readServerSettings, writeServerSettings } from 'officerdb';
|
||||
import { readResourceConfig } from './resources';
|
||||
|
||||
type OcrConfig = {
|
||||
@@ -10,8 +10,8 @@ type OcrConfig = {
|
||||
export async function readOcrConfig(): Promise<OcrConfig | undefined> {
|
||||
const config = await readResourceConfig('optical-character-recognition');
|
||||
if (config.url) return { url: config.url, model: config.model ?? '' };
|
||||
// Fallback to legacy settings.json
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
// Fallback to legacy DB settings
|
||||
const settings = await readServerSettings();
|
||||
return settings.ocr as OcrConfig | undefined;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ ocrRouter.get('/', async (ctx) => {
|
||||
|
||||
ocrRouter.put('/', async (ctx) => {
|
||||
const body = await ctx.req.json<OcrConfig>();
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
const settings = await readServerSettings();
|
||||
settings.ocr = body;
|
||||
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
|
||||
await writeServerSettings(settings);
|
||||
return ctx.json({ success: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { SERVER_CONFIG_DIR } from '@@/data-path';
|
||||
import { readdirSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { readServerSettings, writeServerSettings } from 'officerdb';
|
||||
import { claudeCodeRouter } from './claude-code';
|
||||
import { opencodeRouter } from './opencode';
|
||||
import { piMonoRouter } from './pi-mono';
|
||||
@@ -14,14 +13,6 @@ import { sttRouter } from './stt';
|
||||
import { ocrRouter } from './ocr';
|
||||
import { searxngRouter } from './searxng';
|
||||
|
||||
export const settingsPath = `${SERVER_CONFIG_DIR}/server-settings.json`;
|
||||
|
||||
const settingsFile = Bun.file(settingsPath);
|
||||
if (!(await settingsFile.exists())) {
|
||||
await mkdir(SERVER_CONFIG_DIR, { recursive: true });
|
||||
await Bun.write(settingsPath, '{}');
|
||||
}
|
||||
|
||||
export const serverSettingsRouter = createRouter();
|
||||
|
||||
serverSettingsRouter.route('/claude-code', claudeCodeRouter);
|
||||
@@ -35,33 +26,29 @@ serverSettingsRouter.route('/stt', sttRouter);
|
||||
serverSettingsRouter.route('/ocr', ocrRouter);
|
||||
serverSettingsRouter.route('/searxng', searxngRouter);
|
||||
|
||||
export const readSettings = async () => {
|
||||
try { return await Bun.file(settingsPath).json(); } catch { return {}; }
|
||||
};
|
||||
export { readServerSettings as readSettings };
|
||||
|
||||
serverSettingsRouter.get('/settings', async (ctx) => {
|
||||
return ctx.json(await readSettings());
|
||||
return ctx.json(await readServerSettings());
|
||||
});
|
||||
|
||||
serverSettingsRouter.get('/onboarding-complete', async (ctx) => {
|
||||
const settings = await readSettings();
|
||||
const settings = await readServerSettings();
|
||||
return ctx.json({ onboardingComplete: !!settings.onboardingComplete });
|
||||
});
|
||||
|
||||
serverSettingsRouter.put('/', async (ctx) => {
|
||||
const body = await ctx.req.json();
|
||||
const settings = await readSettings();
|
||||
const settings = await readServerSettings();
|
||||
const updated = { ...settings, ...body };
|
||||
await Bun.write(settingsPath, JSON.stringify(updated, null, 2));
|
||||
await writeServerSettings(updated);
|
||||
return ctx.json(updated);
|
||||
});
|
||||
|
||||
serverSettingsRouter.get('/plugins', async (ctx) => {
|
||||
const pluginsDir = join(import.meta.dir, '../../../workspaces/plugins');
|
||||
const settings = await Bun.file(settingsPath)
|
||||
.json()
|
||||
.catch(() => ({}));
|
||||
const pluginSettings: Record<string, boolean> = settings.plugins ?? {};
|
||||
const settings = await readServerSettings();
|
||||
const pluginSettings: Record<string, boolean> = (settings.plugins as Record<string, boolean>) ?? {};
|
||||
|
||||
const plugins: { id: string; name: string; description: string; enabled: boolean }[] = [];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createTransport } from 'nodemailer';
|
||||
import { createRouter } from '../../create-router';
|
||||
import { settingsPath } from './server-settings';
|
||||
import { readServerSettings, writeServerSettings } from 'officerdb';
|
||||
import { getTransport } from 'emailer';
|
||||
|
||||
type SmtpConfig = {
|
||||
@@ -46,24 +46,24 @@ function buildTransportUrl(config: SmtpConfig): string {
|
||||
export const smtpRouter = createRouter();
|
||||
|
||||
smtpRouter.get('/', async (ctx) => {
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
const smtp: SmtpConfig | undefined = settings.smtp;
|
||||
const settings = await readServerSettings();
|
||||
const smtp: SmtpConfig | undefined = settings.smtp as SmtpConfig | undefined;
|
||||
if (smtp) return ctx.json(serializeConfig(smtp));
|
||||
return ctx.json(null);
|
||||
});
|
||||
|
||||
smtpRouter.put('/', async (ctx) => {
|
||||
const body = await ctx.req.json<SmtpConfig>();
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
const settings = await readServerSettings();
|
||||
|
||||
const existing: SmtpConfig | undefined = settings.smtp;
|
||||
const existing: SmtpConfig | undefined = settings.smtp as SmtpConfig | undefined;
|
||||
if (existing) {
|
||||
if (body.apiKey && body.apiKey.includes('****')) body.apiKey = existing.apiKey;
|
||||
if (body.password && body.password.includes('****')) body.password = existing.password;
|
||||
}
|
||||
|
||||
settings.smtp = body;
|
||||
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
|
||||
await writeServerSettings(settings);
|
||||
return ctx.json({ success: true });
|
||||
});
|
||||
|
||||
@@ -95,8 +95,8 @@ smtpRouter.post('/test', async (ctx) => {
|
||||
if (!body.to) return ctx.json({ error: 'Recipient address required' }, 400);
|
||||
|
||||
// Resolve masked secrets from saved config
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
const saved: SmtpConfig | undefined = settings.smtp;
|
||||
const settings = await readServerSettings();
|
||||
const saved: SmtpConfig | undefined = settings.smtp as SmtpConfig | undefined;
|
||||
if (saved) {
|
||||
if (body.apiKey?.includes('****')) body.apiKey = saved.apiKey;
|
||||
if (body.password?.includes('****')) body.password = saved.password;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { settingsPath } from './server-settings';
|
||||
import { readServerSettings, writeServerSettings } from 'officerdb';
|
||||
import { readResourceConfig } from './resources';
|
||||
|
||||
type SttConfig = {
|
||||
@@ -9,8 +9,8 @@ type SttConfig = {
|
||||
export async function readSttConfig(): Promise<SttConfig | undefined> {
|
||||
const config = await readResourceConfig('speech-to-text');
|
||||
if (config.url) return { url: config.url };
|
||||
// Fallback to legacy settings.json
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
// Fallback to legacy DB settings
|
||||
const settings = await readServerSettings();
|
||||
return settings.stt as SttConfig | undefined;
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ sttRouter.get('/', async (ctx) => {
|
||||
|
||||
sttRouter.put('/', async (ctx) => {
|
||||
const body = await ctx.req.json<SttConfig>();
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
const settings = await readServerSettings();
|
||||
settings.stt = body;
|
||||
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
|
||||
await writeServerSettings(settings);
|
||||
return ctx.json({ success: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ export async function syncUserPiConfig(email: string): Promise<void> {
|
||||
}
|
||||
|
||||
export async function syncAllUserPiConfigs(): Promise<void> {
|
||||
const users = getUsers();
|
||||
const users = await getUsers();
|
||||
if (users.length === 0) return;
|
||||
|
||||
const [appConfig, policy, apiKeys] = await Promise.all([
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { settingsPath } from './server-settings';
|
||||
import { readServerSettings, writeServerSettings } from 'officerdb';
|
||||
import { readResourceConfig } from './resources';
|
||||
|
||||
type TtsConfig = {
|
||||
@@ -18,8 +18,8 @@ function maskSecret(value: string | undefined): string | undefined {
|
||||
export async function readTtsConfig(): Promise<TtsConfig | undefined> {
|
||||
const config = await readResourceConfig('text-to-speech');
|
||||
if (!config.url && !config.provider) {
|
||||
// Fallback to legacy settings.json
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
// Fallback to legacy DB settings
|
||||
const settings = await readServerSettings();
|
||||
return settings.tts as TtsConfig | undefined;
|
||||
}
|
||||
return {
|
||||
@@ -41,15 +41,15 @@ ttsRouter.get('/', async (ctx) => {
|
||||
|
||||
ttsRouter.put('/', async (ctx) => {
|
||||
const body = await ctx.req.json<TtsConfig>();
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
const settings = await readServerSettings();
|
||||
|
||||
const existing: TtsConfig | undefined = settings.tts;
|
||||
const existing: TtsConfig | undefined = settings.tts as TtsConfig | undefined;
|
||||
if (existing && body.apiKey?.includes('****')) {
|
||||
body.apiKey = existing.apiKey;
|
||||
}
|
||||
|
||||
settings.tts = body;
|
||||
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
|
||||
await writeServerSettings(settings);
|
||||
return ctx.json({ success: true });
|
||||
});
|
||||
|
||||
@@ -152,8 +152,8 @@ async function fetchHuggingFaceVoices(repoId: string): Promise<{ flat: string[];
|
||||
ttsRouter.post('/test', async (ctx) => {
|
||||
const body = await ctx.req.json<TtsConfig>();
|
||||
|
||||
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
|
||||
const saved: TtsConfig | undefined = settings.tts;
|
||||
const settings = await readServerSettings();
|
||||
const saved: TtsConfig | undefined = settings.tts as TtsConfig | undefined;
|
||||
if (saved && body.apiKey?.includes('****')) {
|
||||
body.apiKey = saved.apiKey;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
import { getUserSettingsFile, getUserStateFile } from '@@/data-path';
|
||||
import { getUserSettings, setUserSettings, getUserState, patchUserState } from 'officerdb';
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
chat: {
|
||||
defaultProvider: 'claude',
|
||||
defaultProvider: 'pi',
|
||||
defaultModel: null,
|
||||
systemPrompt: '',
|
||||
temperature: 1,
|
||||
@@ -16,73 +14,40 @@ const DEFAULT_SETTINGS = {
|
||||
},
|
||||
};
|
||||
|
||||
const ensureDir = (filePath: string) => mkdir(dirname(filePath), { recursive: true });
|
||||
|
||||
export const settingsRouter = createRouter();
|
||||
|
||||
// GET /settings — return settings.json, auto-create with defaults if missing
|
||||
// GET /settings — return user settings from DB, default if empty
|
||||
settingsRouter.get('/settings', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const filePath = getUserSettingsFile(email);
|
||||
const file = Bun.file(filePath);
|
||||
const userId = ctx.get('user').id;
|
||||
const settings = await getUserSettings(userId);
|
||||
|
||||
if (await file.exists()) {
|
||||
try {
|
||||
return ctx.json(await file.json());
|
||||
} catch {
|
||||
// corrupted — fall through to defaults
|
||||
}
|
||||
if (Object.keys(settings).length === 0) {
|
||||
await setUserSettings(userId, DEFAULT_SETTINGS);
|
||||
return ctx.json(DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
await ensureDir(filePath);
|
||||
await Bun.write(file, JSON.stringify(DEFAULT_SETTINGS, null, 2));
|
||||
return ctx.json(DEFAULT_SETTINGS);
|
||||
return ctx.json(settings);
|
||||
});
|
||||
|
||||
// PUT /settings — full replacement
|
||||
settingsRouter.put('/settings', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const userId = ctx.get('user').id;
|
||||
const body = ctx.get('body');
|
||||
const filePath = getUserSettingsFile(email);
|
||||
|
||||
await ensureDir(filePath);
|
||||
await Bun.write(filePath, JSON.stringify(body, null, 2));
|
||||
await setUserSettings(userId, body);
|
||||
return ctx.json(body);
|
||||
});
|
||||
|
||||
// GET /state — return state.json, auto-create with {} if missing
|
||||
// GET /state — return user state from DB
|
||||
settingsRouter.get('/state', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const filePath = getUserStateFile(email);
|
||||
const file = Bun.file(filePath);
|
||||
|
||||
if (await file.exists()) {
|
||||
try {
|
||||
return ctx.json(await file.json());
|
||||
} catch {
|
||||
// corrupted — fall through to empty
|
||||
}
|
||||
}
|
||||
|
||||
await ensureDir(filePath);
|
||||
await Bun.write(file, JSON.stringify({}, null, 2));
|
||||
return ctx.json({});
|
||||
const userId = ctx.get('user').id;
|
||||
const state = await getUserState(userId);
|
||||
return ctx.json(state);
|
||||
});
|
||||
|
||||
// PATCH /state — shallow-merge incoming keys
|
||||
settingsRouter.patch('/state', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const userId = ctx.get('user').id;
|
||||
const body = ctx.get('body');
|
||||
const filePath = getUserStateFile(email);
|
||||
const file = Bun.file(filePath);
|
||||
|
||||
let existing: Record<string, unknown> = {};
|
||||
if (await file.exists()) {
|
||||
try { existing = await file.json(); } catch { /* corrupted — start fresh */ }
|
||||
}
|
||||
|
||||
const merged = { ...existing, ...body };
|
||||
await ensureDir(filePath);
|
||||
await Bun.write(filePath, JSON.stringify(merged, null, 2));
|
||||
const merged = await patchUserState(userId, body);
|
||||
return ctx.json(merged);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { getHomeDir, getGlobalSkillsDir, getGlobalToolsDir, getGlobalExtensionsDir, getUserSkillsDir, getUserToolsDir, DATA_PATH, SERVER_CONFIG_DIR } from '@@/data-path';
|
||||
import { syncUserPiConfig } from '@@/api/server-settings/sync-user-pi-config';
|
||||
import { getUsers } from 'officerdb';
|
||||
import { getUsers, getServerIntegration, getUserIntegration } from 'officerdb';
|
||||
|
||||
type WSData = { userId: number; email: string; username: string; role: string; sandboxed: boolean; sessionId?: string; cwd?: string; cols?: number; rows?: number };
|
||||
type ShellInfo = { command: string; args: string[]; name: string };
|
||||
@@ -113,10 +113,10 @@ const containerHasExpectedMounts = (dockerId: string): boolean => {
|
||||
});
|
||||
if (result.exitCode !== 0) return false;
|
||||
const mounts = result.stdout.toString();
|
||||
return mounts.includes(getGlobalSkillsDir()) && mounts.includes('google-oauth.json');
|
||||
return mounts.includes(getGlobalSkillsDir());
|
||||
};
|
||||
|
||||
const startDockerSidecar = (port: number, homeDir: string, userId: number, username: string, email: string): { dockerId: string } => {
|
||||
const startDockerSidecar = async (port: number, homeDir: string, userId: number, username: string, email: string): Promise<{ dockerId: string }> => {
|
||||
ensureDockerImage();
|
||||
const dockerPath = Bun.which('docker') ?? 'docker';
|
||||
const dockerId = `officer-terminal-${userId}`;
|
||||
@@ -138,10 +138,32 @@ const startDockerSidecar = (port: number, homeDir: string, userId: number, usern
|
||||
}
|
||||
|
||||
const containerHome = `/home/${username}`;
|
||||
|
||||
// Write google-oauth config from DB to files for Docker mount
|
||||
const googleConfigHost = join(SERVER_CONFIG_DIR, 'google-oauth.json');
|
||||
const googleMounts: string[] = existsSync(googleConfigHost)
|
||||
? ['-v', `${googleConfigHost}:/officer/google-oauth.json:ro`]
|
||||
: [];
|
||||
let googleMounts: string[] = [];
|
||||
try {
|
||||
const googleIntegration = await getServerIntegration('google');
|
||||
if (googleIntegration?.config) {
|
||||
mkdirSync(SERVER_CONFIG_DIR, { recursive: true });
|
||||
writeFileSync(googleConfigHost, JSON.stringify(googleIntegration.config, null, 2));
|
||||
googleMounts = ['-v', `${googleConfigHost}:/officer/google-oauth.json:ro`];
|
||||
}
|
||||
} catch {
|
||||
// No google config — skip mount
|
||||
}
|
||||
|
||||
// Write per-user google token from DB for Docker mount
|
||||
const userIntegrationsDir = join(DATA_PATH, email, 'integrations');
|
||||
try {
|
||||
const userGoogle = await getUserIntegration(userId, 'google');
|
||||
if (userGoogle?.config) {
|
||||
mkdirSync(userIntegrationsDir, { recursive: true });
|
||||
writeFileSync(join(userIntegrationsDir, 'google.json'), JSON.stringify(userGoogle.config, null, 2));
|
||||
}
|
||||
} catch {
|
||||
// No user google integration — skip
|
||||
}
|
||||
|
||||
const run = Bun.spawnSync({
|
||||
cmd: [
|
||||
@@ -273,7 +295,7 @@ export const ensureDockerContainer = async (email: string, userId: number, homeD
|
||||
}
|
||||
|
||||
const port = existing?.port ?? getAvailablePort(map, userId);
|
||||
const docker = startDockerSidecar(port, homeDir, userId, username, email);
|
||||
const docker = await startDockerSidecar(port, homeDir, userId, username, email);
|
||||
const next = { userId, email, dockerId: docker.dockerId, port };
|
||||
map[email] = next;
|
||||
await saveContainerMap(map);
|
||||
@@ -321,7 +343,7 @@ const startHostSidecar = async () => {
|
||||
export const initTerminalSidecars = async () => {
|
||||
await startHostSidecar();
|
||||
ensureDockerImage();
|
||||
const users = getUsers();
|
||||
const users = await getUsers();
|
||||
for (const user of users) {
|
||||
const homeDir = getHomeDir(user.email);
|
||||
mkdirSync(dirname(homeDir), { recursive: true });
|
||||
|
||||
@@ -15,7 +15,7 @@ usersRouter.get('/', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
if (user.role !== 'Super Admin') throw errors.FORBIDDEN();
|
||||
|
||||
const users = getUsers();
|
||||
const users = await getUsers();
|
||||
const sanitized = users.map(({ password, ...rest }) => rest);
|
||||
|
||||
return ctx.json(sanitized);
|
||||
@@ -39,7 +39,7 @@ usersRouter.post('/invite', async (ctx) => {
|
||||
? (role as (typeof USER_ROLES)[number])
|
||||
: ('Member' as const);
|
||||
|
||||
const existing = getUserByEmail(email);
|
||||
const existing = await getUserByEmail(email);
|
||||
if (existing) throw errors.CONFLICT('A user with this email already exists');
|
||||
|
||||
const dbUser = await createUser({
|
||||
@@ -71,7 +71,7 @@ usersRouter.post('/:id/resend-invite', async (ctx) => {
|
||||
const id = Number(ctx.req.param('id'));
|
||||
if (!id || isNaN(id)) throw errors.BAD_REQUEST('Invalid user ID');
|
||||
|
||||
const target = getUserById(id);
|
||||
const target = await getUserById(id);
|
||||
if (!target) throw errors.NOT_FOUND('User not found');
|
||||
if (target.status !== 'Invited') throw errors.BAD_REQUEST('User is not in Invited status');
|
||||
|
||||
@@ -98,7 +98,7 @@ usersRouter.delete('/:id', async (ctx) => {
|
||||
if (!id || isNaN(id)) throw errors.BAD_REQUEST('Invalid user ID');
|
||||
if (id === reqUser.id) throw errors.BAD_REQUEST('Cannot delete yourself');
|
||||
|
||||
const target = getUserById(id);
|
||||
const target = await getUserById(id);
|
||||
if (!target) throw errors.NOT_FOUND('User not found');
|
||||
|
||||
await deleteUser(id);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { homedir } from 'node:os';
|
||||
import { DATA_PATH, PI_CONFIG_DIR } from './data-path';
|
||||
import { syncLocalProvidersToPiConfig } from './api/server-settings/sync-pi-config';
|
||||
import { syncAllUserPiConfigs } from './api/server-settings/sync-user-pi-config';
|
||||
import { initAuthStore } from 'officerdb';
|
||||
import { syncSeedSkills } from './sync-skills';
|
||||
import { syncSeedTools } from './sync-tools';
|
||||
import { syncSeedExtensions } from './sync-extensions';
|
||||
@@ -16,8 +15,6 @@ import { initQueue } from './queue';
|
||||
mkdirSync(DATA_PATH, { recursive: true });
|
||||
mkdirSync(PI_CONFIG_DIR, { recursive: true });
|
||||
|
||||
await initAuthStore();
|
||||
|
||||
async function ensurePiInstalled(): Promise<boolean> {
|
||||
try {
|
||||
const proc = Bun.spawn(['pi', '--version'], { stdout: 'pipe', stderr: 'pipe' });
|
||||
@@ -80,7 +77,7 @@ function seedPiConfig(): void {
|
||||
syncSeedTools();
|
||||
syncSeedExtensions();
|
||||
syncSeedResources();
|
||||
migrateSettingsToResources();
|
||||
await migrateSettingsToResources();
|
||||
generateResourceSkill(DATA_PATH);
|
||||
|
||||
await syncLocalProvidersToPiConfig().catch(err => {
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@ honoServer.route('/api/waitlist', waitlistRouter);
|
||||
honoServer.route('/api/dev-server-proxy', devServerProxyRouter);
|
||||
honoServer.get('/api/integrations/google/callback', googleCallbackHandler);
|
||||
honoServer.get('/api/server-settings/onboarding-complete', async (ctx) => {
|
||||
const { readSettings } = await import('./api/server-settings/server-settings');
|
||||
const settings = await readSettings();
|
||||
const { readServerSettings } = await import('officerdb');
|
||||
const settings = await readServerSettings();
|
||||
return ctx.json({ onboardingComplete: !!settings.onboardingComplete });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { DATA_PATH, SEED_PATH } from './data-path';
|
||||
import { settingsPath } from './api/server-settings/server-settings';
|
||||
import { readServerSettings } from 'officerdb';
|
||||
|
||||
const SETTINGS_TO_RESOURCE: Record<string, string> = {
|
||||
stt: 'speech-to-text',
|
||||
@@ -9,10 +9,10 @@ const SETTINGS_TO_RESOURCE: Record<string, string> = {
|
||||
ocr: 'optical-character-recognition',
|
||||
};
|
||||
|
||||
export function migrateSettingsToResources(): void {
|
||||
let settings: Record<string, Record<string, string>> = {};
|
||||
export async function migrateSettingsToResources(): Promise<void> {
|
||||
let settings: Record<string, Record<string, string>>;
|
||||
try {
|
||||
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
settings = (await readServerSettings()) as Record<string, Record<string, string>>;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { join } from 'node:path';
|
||||
import type { Database } from 'bun:sqlite';
|
||||
import type { JobHandler } from '../types';
|
||||
import { registerHandler } from '../handler-registry';
|
||||
import { DATA_PATH, SERVER_CONFIG_DIR } from '../../data-path';
|
||||
import { DATA_PATH } from '../../data-path';
|
||||
import { openEmailDb, upsertFromRawEml, getSyncMeta, setSyncMeta, updateEmailLabels } from '../../api/email/email-db';
|
||||
import { getServerIntegration, getUserByEmail, getUserIntegration } from 'officerdb';
|
||||
|
||||
type GoogleCredentials = {
|
||||
accessToken: string;
|
||||
@@ -13,26 +13,28 @@ type GoogleCredentials = {
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
const googleConfigPath = join(SERVER_CONFIG_DIR, 'google-oauth.json');
|
||||
|
||||
async function loadCredentials(userId: string): Promise<GoogleCredentials> {
|
||||
const config = await Bun.file(googleConfigPath).json().catch(() => null);
|
||||
if (!config?.clientId || !config?.clientSecret) {
|
||||
async function loadCredentials(email: string): Promise<GoogleCredentials> {
|
||||
const serverGoogle = await getServerIntegration('google');
|
||||
const serverConfig = serverGoogle?.config as Record<string, unknown> | undefined;
|
||||
if (!serverConfig?.clientId || !serverConfig?.clientSecret) {
|
||||
throw new Error('Google OAuth not configured — ask your admin to set up credentials');
|
||||
}
|
||||
|
||||
const tokenPath = join(DATA_PATH, userId, 'integrations', 'google.json');
|
||||
const token = await Bun.file(tokenPath).json().catch(() => null);
|
||||
if (!token?.accessToken) {
|
||||
const dbUser = await getUserByEmail(email);
|
||||
if (!dbUser) throw new Error('User not found');
|
||||
|
||||
const userGoogle = await getUserIntegration(dbUser.id, 'google');
|
||||
const userConfig = userGoogle?.config as Record<string, unknown> | undefined;
|
||||
if (!userConfig?.accessToken) {
|
||||
throw new Error('Google account not connected — connect in Settings → Integrations');
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: token.accessToken,
|
||||
refreshToken: token.refreshToken ?? '',
|
||||
expiresAt: token.expiresAt ?? 0,
|
||||
clientId: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
accessToken: userConfig.accessToken as string,
|
||||
refreshToken: (userConfig.refreshToken as string) ?? '',
|
||||
expiresAt: (userConfig.expiresAt as number) ?? 0,
|
||||
clientId: serverConfig.clientId as string,
|
||||
clientSecret: serverConfig.clientSecret as string,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user