Files
actual/packages/sync-server/src/account-db.js
lelemm fed1cd7d30 Added Global Synced Prefs (#6234)
* Added Global Synced Prefs

* [autofix.ci] apply automated fixes

* Add release notes for PR #6234

* typecheck

* lint fix

* Refactor global synced preferences to server preferences

- Removed global synced preferences implementation and related files.
- Introduced server preferences with a new slice and hooks for managing user settings.
- Updated components and hooks to utilize server preferences instead of global synced preferences.
- Adjusted Redux store and mock configurations to reflect the changes.
- Enhanced user settings consistency across devices with the new server preferences structure.

* Implement server preferences for feature flags and enhance admin permissions

- Updated the Experimental component to conditionally display based on user permissions and login method.
- Refactored feature flag handling to use 'flags.plugins' instead of 'plugins'.
- Introduced server-side checks to restrict access to server preferences for admin users only.
- Added comprehensive tests for server preferences management, ensuring proper handling of user roles and preferences.

* Enhance error handling in saveServerPrefs thunk

- Updated the saveServerPrefs async thunk to handle potential errors from the server response.
- Added a check for the presence of an error in the result and return it accordingly.
- Ensured that preferences are still dispatched to the store upon successful save.

* Feedback: strict "flags.plugins" typing

* Feedback: move state slice

* Feedback: localstorage pref

* Feedback: move serverPrefsSlide into prefsSlice

* Refactor: Remove duplicate import of PostError in app.ts

* Rename serverPrefs state slice property to server (#6596)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-01-09 08:17:36 +00:00

267 lines
6.9 KiB
JavaScript

import { join, resolve } from 'node:path';
import * as bcrypt from 'bcrypt';
import { bootstrapOpenId } from './accounts/openid';
import { bootstrapPassword, loginWithPassword } from './accounts/password';
import { openDatabase } from './db';
import { config } from './load-config';
let _accountDb;
export function getAccountDb() {
if (_accountDb === undefined) {
const dbPath = join(resolve(config.get('serverFiles')), 'account.sqlite');
_accountDb = openDatabase(dbPath);
}
return _accountDb;
}
export function needsBootstrap() {
const accountDb = getAccountDb();
const rows = accountDb.all('SELECT * FROM auth');
return rows.length === 0;
}
export function listLoginMethods() {
const accountDb = getAccountDb();
const rows = accountDb.all('SELECT method, display_name, active FROM auth');
return rows
.filter(f =>
rows.length > 1 && config.get('enforceOpenId')
? f.method === 'openid'
: true,
)
.map(r => ({
method: r.method,
active: r.active,
displayName: r.display_name,
}));
}
export function getActiveLoginMethod() {
const accountDb = getAccountDb();
const { method } =
accountDb.first('SELECT method FROM auth WHERE active = 1') || {};
return method;
}
/*
* Get the Login Method in the following order
* req (the frontend can say which method in the case it wants to resort to forcing password auth)
* config options
* fall back to using password
*/
export function getLoginMethod(req) {
if (
typeof req !== 'undefined' &&
(req.body || { loginMethod: null }).loginMethod &&
config.get('allowedLoginMethods').includes(req.body.loginMethod)
) {
return req.body.loginMethod;
}
//BY-PASS ANY OTHER CONFIGURATION TO ENSURE HEADER AUTH
if (
config.get('loginMethod') === 'header' &&
config.get('allowedLoginMethods').includes('header')
) {
return config.get('loginMethod');
}
const activeMethod = getActiveLoginMethod();
return activeMethod || config.get('loginMethod');
}
export async function bootstrap(loginSettings, forced = false) {
if (!loginSettings) {
return { error: 'invalid-login-settings' };
}
const passEnabled = 'password' in loginSettings;
const openIdEnabled = 'openId' in loginSettings;
const accountDb = getAccountDb();
accountDb.mutate('BEGIN TRANSACTION');
try {
const { countOfOwner } =
accountDb.first(
`SELECT count(*) as countOfOwner
FROM users
WHERE users.user_name <> '' and users.owner = 1`,
) || {};
if (!forced && (!openIdEnabled || countOfOwner > 0)) {
if (!needsBootstrap()) {
accountDb.mutate('ROLLBACK');
return { error: 'already-bootstrapped' };
}
}
if (!passEnabled && !openIdEnabled) {
accountDb.mutate('ROLLBACK');
return { error: 'no-auth-method-selected' };
}
if (passEnabled && openIdEnabled && !forced) {
accountDb.mutate('ROLLBACK');
return { error: 'max-one-method-allowed' };
}
if (passEnabled) {
const { error } = bootstrapPassword(loginSettings.password);
if (error) {
accountDb.mutate('ROLLBACK');
return { error };
}
}
if (openIdEnabled && forced) {
const { error } = await bootstrapOpenId(loginSettings.openId);
if (error) {
accountDb.mutate('ROLLBACK');
return { error };
}
}
accountDb.mutate('COMMIT');
return passEnabled ? loginWithPassword(loginSettings.password) : {};
} catch (error) {
accountDb.mutate('ROLLBACK');
throw error;
}
}
export function isAdmin(userId) {
return hasPermission(userId, 'ADMIN');
}
export function hasPermission(userId, permission) {
return getUserPermission(userId) === permission;
}
export async function enableOpenID(loginSettings) {
if (!loginSettings || !loginSettings.openId) {
return { error: 'invalid-login-settings' };
}
const { error } = (await bootstrapOpenId(loginSettings.openId)) || {};
if (error) {
return { error };
}
getAccountDb().mutate('DELETE FROM sessions');
}
export async function disableOpenID(loginSettings) {
if (!loginSettings || !loginSettings.password) {
return { error: 'invalid-login-settings' };
}
const accountDb = getAccountDb();
const { extra_data: passwordHash } =
accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [
'password',
]) || {};
if (!passwordHash) {
return { error: 'invalid-password' };
}
if (!loginSettings?.password) {
return { error: 'invalid-password' };
}
if (passwordHash) {
const confirmed = bcrypt.compareSync(loginSettings.password, passwordHash);
if (!confirmed) {
return { error: 'invalid-password' };
}
}
const { error } = (await bootstrapPassword(loginSettings.password)) || {};
if (error) {
return { error };
}
try {
accountDb.transaction(() => {
accountDb.mutate('DELETE FROM sessions');
accountDb.mutate(
`DELETE FROM user_access
WHERE user_access.user_id IN (
SELECT users.id
FROM users
WHERE users.user_name <> ?
);`,
[''],
);
accountDb.mutate('DELETE FROM users WHERE user_name <> ?', ['']);
accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']);
});
} catch (err) {
console.error('Error cleaning up openid information:', err);
return { error: 'database-error' };
}
}
export function getSession(token) {
const accountDb = getAccountDb();
return accountDb.first('SELECT * FROM sessions WHERE token = ?', [token]);
}
export function getUserInfo(userId) {
const accountDb = getAccountDb();
return accountDb.first('SELECT * FROM users WHERE id = ?', [userId]);
}
export function getUserPermission(userId) {
const accountDb = getAccountDb();
const { role } = accountDb.first(
`SELECT role FROM users
WHERE users.id = ?`,
[userId],
) || { role: '' };
return role;
}
export function getServerPrefs() {
const accountDb = getAccountDb();
const rows = accountDb.all('SELECT key, value FROM server_prefs') || [];
return rows.reduce((prefs, row) => {
prefs[row.key] = row.value;
return prefs;
}, {});
}
export function setServerPrefs(prefs) {
const accountDb = getAccountDb();
if (!prefs) {
return;
}
accountDb.transaction(() => {
Object.entries(prefs).forEach(([key, value]) => {
accountDb.mutate(
'INSERT INTO server_prefs (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value',
[key, value],
);
});
});
}
export function clearExpiredSessions() {
const clearThreshold = Math.floor(Date.now() / 1000) - 3600;
const deletedSessions = getAccountDb().mutate(
'DELETE FROM sessions WHERE expires_at <> -1 and expires_at < ?',
[clearThreshold],
).changes;
console.log(`Deleted ${deletedSessions} old sessions`);
}