mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 18:20:24 -05:00
* 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>
267 lines
6.9 KiB
JavaScript
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`);
|
|
}
|