+
+
+
+
diff --git a/public/scripts/loader.js b/public/scripts/loader.js
index 179a0437b..91e7df196 100644
--- a/public/scripts/loader.js
+++ b/public/scripts/loader.js
@@ -1,7 +1,5 @@
const ELEMENT_ID = 'loader';
-import { populateUserList } from './userManagement.js'
-
export function showLoader() {
const container = $('
').attr('id', 'load-spinner').addClass('fa-solid fa-gear fa-spin fa-3x');
@@ -10,7 +8,6 @@ export function showLoader() {
}
export async function hideLoader() {
-
//Sets up a 2-step animation. Spinner blurs/fades out, and then the loader shadow does the same.
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
//uncomment this as part of user selection enabling
@@ -35,8 +32,4 @@ export async function hideLoader() {
//uncomment to make user selection live
//await populateUserList()
-
-
}
-
-
diff --git a/public/scripts/login.js b/public/scripts/login.js
new file mode 100644
index 000000000..ef34b6747
--- /dev/null
+++ b/public/scripts/login.js
@@ -0,0 +1,142 @@
+async function getUserList() {
+ const response = await fetch('/api/users/list');
+ const userListObj = await response.json();
+ console.log(userListObj);
+ return userListObj;
+}
+
+async function sendRecoveryPart1(handle) {
+ const response = await fetch('/api/users/recover-step1', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ handle }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ return displayError(errorData.error || 'An error occurred');
+ }
+
+ showRecoveryBlock();
+}
+
+async function sendRecoveryPart2(handle, code, newPassword) {
+ const recoveryData = {
+ handle,
+ code,
+ newPassword,
+ };
+
+ const response = await fetch('/api/users/recover-step2', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(recoveryData),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ return displayError(errorData.error || 'An error occurred');
+ }
+
+ console.log(`Successfully recovered password for ${handle}!`);
+ await performLogin(handle, newPassword);
+}
+
+async function onUserSelected(user) {
+ // No password, just log in
+ if (!user.password) {
+ return await performLogin(user.handle, '');
+ }
+
+ $('#passwordRecoveryBlock').hide();
+ $('#passwordEntryBlock').show();
+ $('#loginButton').off('click').on('click', async () => {
+ const password = String($('#userPassword').val());
+ await performLogin(user.handle, password);
+ });
+
+ $('#recoverPassword').off('click').on('click', async () => {
+ await sendRecoveryPart1(user.handle);
+ });
+
+ $('#sendRecovery').off('click').on('click', async () => {
+ const code = String($('#recoveryCode').val());
+ const newPassword = String($('#newPassword').val());
+ await sendRecoveryPart2(user.handle, code, newPassword);
+ });
+
+ displayError('');
+}
+
+function displayError(message) {
+ $('#errorMessage').text(message);
+}
+
+async function performLogin(handle, password) {
+ const userInfo = {
+ handle: handle,
+ password: password,
+ };
+
+ try {
+ const response = await fetch('/api/users/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(userInfo),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ return displayError(errorData.error || 'An error occurred');
+ }
+
+ const data = await response.json();
+
+ if (data.handle) {
+ console.log(`Successfully logged in as ${handle}!`);
+ redirectToHome();
+ }
+ } catch (error) {
+ console.error('Error logging in:', error);
+ displayError(String(error));
+ }
+}
+
+function redirectToHome() {
+ window.location.href = '/';
+}
+
+function showRecoveryBlock() {
+ $('#passwordEntryBlock').hide();
+ $('#passwordRecoveryBlock').show();
+ displayError('');
+}
+
+function onCancelRecoveryClick() {
+ $('#passwordRecoveryBlock').hide();
+ $('#passwordEntryBlock').show();
+ displayError('');
+}
+
+(async function () {
+ const userList = await getUserList();
+ console.log(userList);
+ for (const user of userList) {
+ const userBlock = $('
').attr('src', user.avatar));
+ userBlock.append(avatarBlock);
+ userBlock.append($('
').text(user.handle));
+ userBlock.on('click', () => onUserSelected(user));
+ $('#userList').append(userBlock);
+ }
+ document.body.style.display = '';
+ $('#cancelRecovery').on('click', onCancelRecoveryClick);
+})();
diff --git a/public/scripts/userManagement.js b/public/scripts/userManagement.js
index eff0b02b3..e3cd9d34f 100644
--- a/public/scripts/userManagement.js
+++ b/public/scripts/userManagement.js
@@ -1,10 +1,3 @@
-async function getUserList() {
- const response = await fetch('/api/users/list');
- const userListObj = await response.json(); // Assuming the response is in JSON format
- console.log(userListObj)
- return userListObj;
-}
-
async function registerNewUser() {
let handle = String($("#newUserHandle").val());
let name = String($("#newUserName").val());
@@ -111,17 +104,6 @@ export async function populateUserList() {
`
const userSelectHTML = `
-
-
Select User
-
This is merely a test.
Click a user, and then click Login to proceed.
-
${newUserRegisterationHTML}
diff --git a/server.js b/server.js
index bf35c9f1e..2b8b2870e 100644
--- a/server.js
+++ b/server.js
@@ -18,6 +18,7 @@ const doubleCsrf = require('csrf-csrf').doubleCsrf;
const express = require('express');
const compression = require('compression');
const cookieParser = require('cookie-parser');
+const cookieSession = require('cookie-session');
const multer = require('multer');
const responseTime = require('response-time');
const helmet = require('helmet').default;
@@ -33,14 +34,7 @@ util.inspect.defaultOptions.maxStringLength = null;
util.inspect.defaultOptions.depth = 4;
// local library imports
-const {
- initUserStorage,
- ensurePublicDirectoriesExist,
- userDataMiddleware,
- migrateUserData,
- getCsrfSecret,
- getCookieSecret,
-} = require('./src/users');
+const userModule = require('./src/users');
const basicAuthMiddleware = require('./src/middleware/basicAuth');
const whitelistMiddleware = require('./src/middleware/whitelist');
const contentManager = require('./src/endpoints/content-manager');
@@ -122,6 +116,7 @@ const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTOR
const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
const basicAuthMode = getConfigValue('basicAuthMode', false);
+const enableAccounts = getConfigValue('enableUserAccounts', false);
const { UPLOADS_PATH } = require('./src/constants');
@@ -136,40 +131,6 @@ app.use(CORS);
if (listen && basicAuthMode) app.use(basicAuthMiddleware);
app.use(whitelistMiddleware(listen));
-app.use(userDataMiddleware(app));
-
-// CSRF Protection //
-if (!cliArguments.disableCsrf) {
- const COOKIES_SECRET = getCookieSecret();
-
- const { generateToken, doubleCsrfProtection } = doubleCsrf({
- getSecret: getCsrfSecret,
- cookieName: 'X-CSRF-Token',
- cookieOptions: {
- httpOnly: true,
- sameSite: 'strict',
- secure: false,
- },
- size: 64,
- getTokenFromRequest: (req) => req.headers['x-csrf-token'],
- });
-
- app.get('/csrf-token', (req, res) => {
- res.json({
- 'token': generateToken(res, req),
- });
- });
-
- app.use(cookieParser(COOKIES_SECRET));
- app.use(doubleCsrfProtection);
-} else {
- console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
- app.get('/csrf-token', (req, res) => {
- res.json({
- 'token': 'disabled',
- });
- });
-}
if (enableCorsProxy) {
const bodyParser = require('body-parser');
@@ -221,17 +182,85 @@ if (enableCorsProxy) {
});
}
-app.use(express.static(process.cwd() + '/public', {}));
-app.use('/', require('./src/users').router);
+app.use(cookieSession({
+ name: userModule.getCookieSessionName(),
+ sameSite: 'strict',
+ httpOnly: true,
+ maxAge: 24 * 60 * 60 * 1000, // 24 hours
+ secret: userModule.getCookieSecret(),
+}));
-app.use(multer({ dest: UPLOADS_PATH, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
-app.get('/', function (request, response) {
- response.sendFile(process.cwd() + '/public/index.html');
+app.use(userModule.setUserDataMiddleware);
+
+// Static files
+// Host index page
+app.get('/', (request, response) => {
+ if (userModule.shouldRedirectToLogin(request)) {
+ return response.redirect('/login');
+ }
+
+ return response.sendFile('index.html', { root: path.join(process.cwd(), 'public') });
});
// Host login page
-app.get('/login', (_request, response) => {
+app.get('/login', async (request, response) => {
+ if (!enableAccounts) {
+ console.log('User accounts are disabled. Redirecting to index page.');
+ return response.redirect('/');
+ }
+
+ const autoLogin = await userModule.tryAutoLogin(request);
+
+ if (autoLogin) {
+ return response.redirect('/');
+ }
+
return response.sendFile('login.html', { root: path.join(process.cwd(), 'public') });
});
+app.use(express.static(process.cwd() + '/public', {}));
+
+app.use('/api/users', userModule.publicEndpoints);
+
+app.use(userModule.requireLoginMiddleware);
+
+// CSRF Protection //
+if (!cliArguments.disableCsrf) {
+ const COOKIES_SECRET = userModule.getCookieSecret();
+
+ const { generateToken, doubleCsrfProtection } = doubleCsrf({
+ getSecret: userModule.getCsrfSecret,
+ cookieName: 'X-CSRF-Token',
+ cookieOptions: {
+ httpOnly: true,
+ sameSite: 'strict',
+ secure: false,
+ },
+ size: 64,
+ getTokenFromRequest: (req) => req.headers['x-csrf-token'],
+ });
+
+ app.get('/csrf-token', (req, res) => {
+ res.json({
+ 'token': generateToken(res, req),
+ });
+ });
+
+ app.use(cookieParser(COOKIES_SECRET));
+ app.use(doubleCsrfProtection);
+} else {
+ console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
+ app.get('/csrf-token', (req, res) => {
+ res.json({
+ 'token': 'disabled',
+ });
+ });
+}
+
+// User management
+app.use('/', userModule.router);
+app.use('/api/users', userModule.authenticatedEndpoints);
+app.use('/api/users', userModule.adminEndpoints);
+
+app.use(multer({ dest: UPLOADS_PATH, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
app.get('/version', async function (_, response) {
const data = await getVersion();
response.send(data);
@@ -481,10 +510,10 @@ const setupTasks = async function () {
// TODO: do endpoint init functions depend on certain directories existing or not existing? They should be callable
// in any order for encapsulation reasons, but right now it's unknown if that would break anything.
- await initUserStorage();
+ await userModule.initUserStorage();
await settingsEndpoint.init();
- const directories = await ensurePublicDirectoriesExist();
- await migrateUserData();
+ const directories = await userModule.ensurePublicDirectoriesExist();
+ await userModule.migrateUserData();
await contentManager.checkForNewContent(directories);
await ensureThumbnailCache();
cleanUploads();
diff --git a/src/users.js b/src/users.js
index 373c0ebbe..9c8ee7e2d 100644
--- a/src/users.js
+++ b/src/users.js
@@ -7,7 +7,6 @@ const os = require('os');
// Express and other dependencies
const storage = require('node-persist');
const express = require('express');
-const cookieSession = require('cookie-session');
const uuid = require('uuid');
const mime = require('mime-types');
const slugify = require('slugify').default;
@@ -400,7 +399,7 @@ function getCsrfSecret(request) {
* @returns {Promise} - The list of user handles
*/
async function getAllUserHandles() {
- const keys = await storage.keys(x=> x.key.startsWith(KEY_PREFIX));
+ const keys = await storage.keys(x => x.key.startsWith(KEY_PREFIX));
const handles = keys.map(x => x.replace(KEY_PREFIX, ''));
return handles;
}
@@ -455,87 +454,100 @@ function getUserAvatar(handle) {
}
/**
- * Middleware to add user data to the request object.
- * @param {import('express').Express} app Express app
- * @returns {import('express').RequestHandler}
+ * Checks if the user should be redirected to the login page.
+ * @param {import('express').Request} request Request object
+ * @returns {boolean} Whether the user should be redirected to the login page
*/
-function userDataMiddleware(app) {
- app.use(cookieSession({
- name: getCookieSessionName(),
- sameSite: 'strict',
- httpOnly: true,
- maxAge: 24 * 60 * 60 * 1000, // 24 hours
- secret: getCookieSecret(),
- }));
+function shouldRedirectToLogin(request) {
+ return ENABLE_ACCOUNTS && !request.user;
+}
- /**
- * Middleware to add user data to the request object.
- * @param {import('express').Request} req Request object
- * @param {import('express').Response} res Response object
- * @param {import('express').NextFunction} next Next function
- */
- return async (req, res, next) => {
- // Skip for login page
- if (req.path === '/login') {
- return next();
+/**
+ * Tries auto-login if there is only one user and it's not password protected.
+ * @param {import('express').Request} request Request object
+ * @returns {Promise} Whether auto-login was performed
+ */
+async function tryAutoLogin(request) {
+ if (!ENABLE_ACCOUNTS || request.user || !request.session) {
+ return false;
+ }
+
+ const userHandles = await getAllUserHandles();
+ if (userHandles.length === 1) {
+ const user = await storage.getItem(toKey(userHandles[0]));
+ if (!user.password) {
+ request.session.handle = userHandles[0];
+ return true;
}
+ }
- // If user accounts are disabled, use the default user
- if (!ENABLE_ACCOUNTS) {
- const handle = DEFAULT_USER.handle;
- const directories = getUserDirectories(handle);
- req.user = {
- profile: DEFAULT_USER,
- directories: directories,
- };
- return next();
- }
-
- if (!req.session) {
- console.error('Session not available');
- return res.sendStatus(500);
- }
-
- // If user accounts are enabled, get the user from the session
- let handle = req.session?.handle;
-
- // If we have the only user and it's not password protected, use it
- if (!handle) {
- const handles = await getAllUserHandles();
- if (handles.length === 1) {
- /** @type {User} */
- const user = await storage.getItem(toKey(handles[0]));
- if (!user.password) {
- handle = user.handle;
- req.session.handle = handle;
- }
- }
- }
-
- if (!handle) {
- return res.redirect('/login');
- }
-
- /** @type {User} */
- const user = await storage.getItem(toKey(handle));
-
- if (!user) {
- console.error('User not found:', handle);
- return res.redirect('/login');
- }
-
- if (!user.enabled) {
- console.error('User is disabled:', handle);
- return res.redirect('/login');
- }
+ return false;
+}
+/**
+ * Middleware to add user data to the request object.
+ * @param {import('express').Request} request Request object
+ * @param {import('express').Response} response Response object
+ * @param {import('express').NextFunction} next Next function
+ */
+async function setUserDataMiddleware(request, response, next) {
+ // If user accounts are disabled, use the default user
+ if (!ENABLE_ACCOUNTS) {
+ const handle = DEFAULT_USER.handle;
const directories = getUserDirectories(handle);
- req.user = {
- profile: user,
+ request.user = {
+ profile: DEFAULT_USER,
directories: directories,
};
return next();
+ }
+
+ if (!request.session) {
+ console.error('Session not available');
+ return response.sendStatus(500);
+ }
+
+ // If user accounts are enabled, get the user from the session
+ let handle = request.session?.handle;
+
+ // If we have the only user and it's not password protected, use it
+ if (!handle) {
+ return next();
+ }
+
+ /** @type {User} */
+ const user = await storage.getItem(toKey(handle));
+
+ if (!user) {
+ console.error('User not found:', handle);
+ return next();
+ }
+
+ if (!user.enabled) {
+ console.error('User is disabled:', handle);
+ return next();
+ }
+
+ const directories = getUserDirectories(handle);
+ request.user = {
+ profile: user,
+ directories: directories,
};
+ return next();
+}
+
+/**
+ * Middleware to add user data to the request object.
+ * @param {import('express').Request} request Request object
+ * @param {import('express').Response} response Response object
+ * @param {import('express').NextFunction} next Next function
+ */
+function requireLoginMiddleware(request, response, next) {
+ if (!request.user) {
+ return response.sendStatus(401);
+ }
+
+ return next();
}
/**
@@ -588,40 +600,60 @@ router.use('/user/images/*', createRouteHandler(req => req.user.directories.user
router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));
router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions));
-const endpoints = express.Router();
+const publicEndpoints = express.Router();
-endpoints.get('/list', async (_request, response) => {
+publicEndpoints.get('/list', async (_request, response) => {
/** @type {User[]} */
const users = await storage.values();
- const viewModels = users.filter(x => x.enabled).map(user => ({
- handle: user.handle,
- name: user.name,
- avatar: getUserAvatar(user.handle),
- admin: user.admin,
- password: !!user.password,
- }));
+ const viewModels = users
+ .filter(x => x.enabled)
+ .sort((x, y) => x.created - y.created)
+ .map(user => ({
+ handle: user.handle,
+ name: user.name,
+ avatar: getUserAvatar(user.handle),
+ admin: user.admin,
+ password: !!user.password,
+ }));
return response.json(viewModels);
});
-endpoints.get('/me', async (request, response) => {
- if (!request.user) {
- return response.sendStatus(401);
+publicEndpoints.post('/login', jsonParser, async (request, response) => {
+ if (!request.body.handle) {
+ console.log('Login failed: Missing required fields');
+ return response.status(400).json({ error: 'Missing required fields' });
}
- const user = request.user.profile;
- const viewModel = {
- handle: user.handle,
- name: user.name,
- avatar: getUserAvatar(user.handle),
- admin: user.admin,
- password: !!user.password,
- };
+ /** @type {User} */
+ const user = await storage.getItem(toKey(request.body.handle));
- return response.json(viewModel);
+ if (!user) {
+ console.log('Login failed: User not found');
+ return response.status(401).json({ error: 'User not found' });
+ }
+
+ if (!user.enabled) {
+ console.log('Login failed: User is disabled');
+ return response.status(403).json({ error: 'User is disabled' });
+ }
+
+ if (user.password && user.password !== getPasswordHash(request.body.password, user.salt)) {
+ console.log('Login failed: Incorrect password');
+ return response.status(401).json({ error: 'Incorrect password' });
+ }
+
+ if (!request.session) {
+ console.error('Session not available');
+ return response.sendStatus(500);
+ }
+
+ request.session.handle = user.handle;
+ console.log('Login successful:', user.handle, request.session);
+ return response.json({ handle: user.handle });
});
-endpoints.post('/recover-step1', jsonParser, async (request, response) => {
+publicEndpoints.post('/recover-step1', jsonParser, async (request, response) => {
if (!request.body.handle) {
console.log('Recover step 1 failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
@@ -641,13 +673,15 @@ endpoints.post('/recover-step1', jsonParser, async (request, response) => {
}
const mfaCode = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
- console.log(color.blue(`${user.name} YOUR PASSWORD RECOVERY CODE IS: `) + color.magenta(mfaCode));
+ console.log();
+ console.log(color.blue(`${user.name}, your password recovery code is: `) + color.magenta(mfaCode));
+ console.log();
MFA_CACHE.set(user.handle, mfaCode);
return response.sendStatus(204);
});
-endpoints.post('/recover-step2', jsonParser, async (request, response) => {
- if (!request.body.handle || !request.body.code || !request.body.password) {
+publicEndpoints.post('/recover-step2', jsonParser, async (request, response) => {
+ if (!request.body.handle || !request.body.code) {
console.log('Recover step 2 failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
}
@@ -672,101 +706,40 @@ endpoints.post('/recover-step2', jsonParser, async (request, response) => {
return response.status(401).json({ error: 'Incorrect code' });
}
+ const newPassword = request.body.newPassword || '';
const salt = getPasswordSalt();
- user.password = getPasswordHash(request.body.password, salt);
+ user.password = getPasswordHash(newPassword, salt);
user.salt = salt;
await storage.setItem(toKey(user.handle), user);
return response.sendStatus(204);
});
-endpoints.post('/login', jsonParser, async (request, response) => {
- if (!request.body.handle || !request.body.password) {
- console.log('Login failed: Missing required fields');
- return response.status(400).json({ error: 'Missing required fields' });
- }
+const authenticatedEndpoints = express.Router();
- /** @type {User} */
- const user = await storage.getItem(toKey(request.body.handle));
-
- if (!user) {
- console.log('Login failed: User not found');
- return response.status(401).json({ error: 'User not found' });
- }
-
- if (!user.enabled) {
- console.log('Login failed: User is disabled');
- return response.status(403).json({ error: 'User is disabled' });
- }
-
- if (user.password && user.password !== getPasswordHash(request.body.password, user.salt)) {
- console.log('Login failed: Incorrect password');
- return response.status(401).json({ error: 'Incorrect password' });
- }
-
- if (!request.session) {
- console.error('Login failed: Session not available');
- return response.status(500).json({ error: 'Session not available' });
- }
-
- // Regenerate session to prevent session fixation attacks
- await new Promise(resolve => request.session?.regenerate(resolve));
-
-
- request.session.handle = user.handle;
- console.log('Login successful:', user.handle, request.session);
- return response.json({ handle: user.handle });
-});
-
-endpoints.post('/logout', async (request, response) => {
+authenticatedEndpoints.post('/logout', async (request, response) => {
request.session?.destroy(() => {
return response.sendStatus(204);
});
});
-endpoints.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {
- if (!request.body.handle) {
- console.log('Disable user failed: Missing required fields');
- return response.status(400).json({ error: 'Missing required fields' });
+authenticatedEndpoints.get('/me', async (request, response) => {
+ if (!request.user) {
+ return response.sendStatus(401);
}
- if (request.body.handle === request.user.profile.handle) {
- console.log('Disable user failed: Cannot disable yourself');
- return response.status(400).json({ error: 'Cannot disable yourself' });
- }
+ const user = request.user.profile;
+ const viewModel = {
+ handle: user.handle,
+ name: user.name,
+ avatar: getUserAvatar(user.handle),
+ admin: user.admin,
+ password: !!user.password,
+ };
- /** @type {User} */
- const user = await storage.getItem(toKey(request.body.handle));
-
- if (!user) {
- console.log('Disable user failed: User not found');
- return response.status(404).json({ error: 'User not found' });
- }
-
- user.enabled = false;
- await storage.setItem(toKey(request.body.handle), user);
- return response.sendStatus(204);
+ return response.json(viewModel);
});
-endpoints.post('/enable', requireAdminMiddleware, jsonParser, async (request, response) => {
- if (!request.body.handle) {
- console.log('Enable user failed: Missing required fields');
- return response.status(400).json({ error: 'Missing required fields' });
- }
-
- /** @type {User} */
- const user = await storage.getItem(toKey(request.body.handle));
-
- if (!user) {
- console.log('Enable user failed: User not found');
- return response.status(404).json({ error: 'User not found' });
- }
-
- user.enabled = true;
- await storage.setItem(toKey(request.body.handle), user);
- return response.sendStatus(204);
-});
-
-endpoints.post('/change-password', jsonParser, async (request, response) => {
+authenticatedEndpoints.post('/change-password', jsonParser, async (request, response) => {
if (!request.body.handle || !request.body.oldPassword || !request.body.newPassword) {
console.log('Change password failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
@@ -797,7 +770,52 @@ endpoints.post('/change-password', jsonParser, async (request, response) => {
return response.sendStatus(204);
});
-endpoints.post('/create', requireAdminMiddleware, jsonParser, async (request, response) => {
+const adminEndpoints = express.Router();
+
+adminEndpoints.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {
+ if (!request.body.handle) {
+ console.log('Disable user failed: Missing required fields');
+ return response.status(400).json({ error: 'Missing required fields' });
+ }
+
+ if (request.body.handle === request.user.profile.handle) {
+ console.log('Disable user failed: Cannot disable yourself');
+ return response.status(400).json({ error: 'Cannot disable yourself' });
+ }
+
+ /** @type {User} */
+ const user = await storage.getItem(toKey(request.body.handle));
+
+ if (!user) {
+ console.log('Disable user failed: User not found');
+ return response.status(404).json({ error: 'User not found' });
+ }
+
+ user.enabled = false;
+ await storage.setItem(toKey(request.body.handle), user);
+ return response.sendStatus(204);
+});
+
+adminEndpoints.post('/enable', requireAdminMiddleware, jsonParser, async (request, response) => {
+ if (!request.body.handle) {
+ console.log('Enable user failed: Missing required fields');
+ return response.status(400).json({ error: 'Missing required fields' });
+ }
+
+ /** @type {User} */
+ const user = await storage.getItem(toKey(request.body.handle));
+
+ if (!user) {
+ console.log('Enable user failed: User not found');
+ return response.status(404).json({ error: 'User not found' });
+ }
+
+ user.enabled = true;
+ await storage.setItem(toKey(request.body.handle), user);
+ return response.sendStatus(204);
+});
+
+adminEndpoints.post('/create', requireAdminMiddleware, jsonParser, async (request, response) => {
if (!request.body.handle || !request.body.name) {
console.log('Create user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
@@ -834,16 +852,21 @@ endpoints.post('/create', requireAdminMiddleware, jsonParser, async (request, re
return response.json({ handle: newUser.handle });
});
-router.use('/api/users', endpoints);
-
module.exports = {
initUserStorage,
ensurePublicDirectoriesExist,
getAllUserHandles,
getUserDirectories,
- userDataMiddleware,
+ setUserDataMiddleware,
+ requireLoginMiddleware,
migrateUserData,
getCsrfSecret,
getCookieSecret,
+ getCookieSessionName,
router,
+ publicEndpoints,
+ authenticatedEndpoints,
+ adminEndpoints,
+ shouldRedirectToLogin,
+ tryAutoLogin,
};