Working login flow
This commit is contained in:
parent
af8627b999
commit
3f3e23420d
|
@ -0,0 +1,35 @@
|
||||||
|
body.login #shadow_popup {
|
||||||
|
opacity: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.login .logo {
|
||||||
|
max-width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.login #logoBlock {
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.login .userSelect {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
width: min-content;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px 0;
|
||||||
|
transition: background-color 0.15s ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.login .userSelect:hover {
|
||||||
|
background-color: var(--black30a);
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -0,0 +1,71 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<base href="/">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, viewport-fit=cover, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="apple-touch-icon" sizes="57x57" href="img/apple-icon-57x57.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="72x72" href="img/apple-icon-72x72.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="114x114" href="img/apple-icon-114x114.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="144x144" href="img/apple-icon-144x144.png" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/st-tailwind.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/login.css">
|
||||||
|
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
|
||||||
|
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
|
||||||
|
<!-- fontawesome webfonts-->
|
||||||
|
<link href="css/fontawesome.css" rel="stylesheet">
|
||||||
|
<link href="css/solid.css" rel="stylesheet">
|
||||||
|
<script src="lib/jquery-3.5.1.min.js"></script>
|
||||||
|
<script src="scripts/login.js"></script>
|
||||||
|
<title>SillyTavern</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="login" style="display: none;">
|
||||||
|
<div id="shadow_popup">
|
||||||
|
<div id="dialogue_popup">
|
||||||
|
<div id="dialogue_popup_holder">
|
||||||
|
<div id="dialogue_popup_text">
|
||||||
|
<div id="userSelectBlock" class="flex-container flexFlowColumn alignItemsCenter">
|
||||||
|
<h2 id="logoBlock" class="flex-container">
|
||||||
|
<img src="img/logo.png" alt="SillyTavern" class="logo">
|
||||||
|
<span>Welcome to SillyTavern</span>
|
||||||
|
</h2>
|
||||||
|
<h3>Select a User</h3>
|
||||||
|
<div id="userListBlock" class="wide100p">
|
||||||
|
<div id="userList" class="flex-container justifyCenter"></div>
|
||||||
|
<div id="passwordEntryBlock" style="display:none;"
|
||||||
|
class="flex-container flexFlowColumn alignItemsCenter">
|
||||||
|
<input id="userPassword" class="text_pole" type="password" placeholder="Enter a password...">
|
||||||
|
<div class="flex-container">
|
||||||
|
<div id="loginButton" class="menu_button">Login</div>
|
||||||
|
<div id="recoverPassword" class="menu_button">Recover</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="passwordRecoveryBlock" style="display:none;"
|
||||||
|
class="flex-container flexFlowColumn alignItemsCenter">
|
||||||
|
<div id="recoverMessage">
|
||||||
|
Recovery code has been posted to the server console.
|
||||||
|
</div>
|
||||||
|
<input id="recoveryCode" class="text_pole" type="text" placeholder="Recovery code">
|
||||||
|
<input id="newPassword" class="text_pole" type="password" placeholder="New password...">
|
||||||
|
<div class="flex-container">
|
||||||
|
<div id="sendRecovery" class="menu_button">Send</div>
|
||||||
|
<div id="cancelRecovery" class="menu_button">Cancel</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="neutral_warning" id="errorMessage">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -1,7 +1,5 @@
|
||||||
const ELEMENT_ID = 'loader';
|
const ELEMENT_ID = 'loader';
|
||||||
|
|
||||||
import { populateUserList } from './userManagement.js'
|
|
||||||
|
|
||||||
export function showLoader() {
|
export function showLoader() {
|
||||||
const container = $('<div></div>').attr('id', ELEMENT_ID);
|
const container = $('<div></div>').attr('id', ELEMENT_ID);
|
||||||
const loader = $('<div></div>').attr('id', 'load-spinner').addClass('fa-solid fa-gear fa-spin fa-3x');
|
const loader = $('<div></div>').attr('id', 'load-spinner').addClass('fa-solid fa-gear fa-spin fa-3x');
|
||||||
|
@ -10,7 +8,6 @@ export function showLoader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hideLoader() {
|
export async function hideLoader() {
|
||||||
|
|
||||||
//Sets up a 2-step animation. Spinner blurs/fades out, and then the loader shadow does the same.
|
//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 () {
|
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
|
||||||
//uncomment this as part of user selection enabling
|
//uncomment this as part of user selection enabling
|
||||||
|
@ -35,8 +32,4 @@ export async function hideLoader() {
|
||||||
|
|
||||||
//uncomment to make user selection live
|
//uncomment to make user selection live
|
||||||
//await populateUserList()
|
//await populateUserList()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 = $('<div></div>').addClass('userSelect');
|
||||||
|
const avatarBlock = $('<div></div>').addClass('avatar');
|
||||||
|
avatarBlock.append($('<img>').attr('src', user.avatar));
|
||||||
|
userBlock.append(avatarBlock);
|
||||||
|
userBlock.append($('<span></span>').text(user.name));
|
||||||
|
userBlock.append($('<small></small>').text(user.handle));
|
||||||
|
userBlock.on('click', () => onUserSelected(user));
|
||||||
|
$('#userList').append(userBlock);
|
||||||
|
}
|
||||||
|
document.body.style.display = '';
|
||||||
|
$('#cancelRecovery').on('click', onCancelRecoveryClick);
|
||||||
|
})();
|
|
@ -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() {
|
async function registerNewUser() {
|
||||||
let handle = String($("#newUserHandle").val());
|
let handle = String($("#newUserHandle").val());
|
||||||
let name = String($("#newUserName").val());
|
let name = String($("#newUserName").val());
|
||||||
|
@ -111,17 +104,6 @@ export async function populateUserList() {
|
||||||
`
|
`
|
||||||
|
|
||||||
const userSelectHTML = `
|
const userSelectHTML = `
|
||||||
<div id="userSelectBlock" class="flex-container flexFlowColumn alignItemsCenter">
|
|
||||||
<h3>Select User</h3>
|
|
||||||
<small>This is merely a test. <br> Click a user, and then click Login to proceed.</small>
|
|
||||||
<div id="userListBlock">
|
|
||||||
<div id="userList" class="flex-container justifyCenter"></div>
|
|
||||||
<div id="passwordEntryBlock" style="display:none;" class="flex-container flexFlowColumn alignItemsCenter">
|
|
||||||
<h4 id="passwordHeaderText"></h4>
|
|
||||||
<input id="userPassword" class="text_pole" type="password">
|
|
||||||
<div id="loginButton" class='menu_button'>Login</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="registerNewUserBlock" style="display:none;">
|
<div id="registerNewUserBlock" style="display:none;">
|
||||||
${newUserRegisterationHTML}
|
${newUserRegisterationHTML}
|
||||||
|
|
131
server.js
131
server.js
|
@ -18,6 +18,7 @@ const doubleCsrf = require('csrf-csrf').doubleCsrf;
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
|
const cookieSession = require('cookie-session');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const responseTime = require('response-time');
|
const responseTime = require('response-time');
|
||||||
const helmet = require('helmet').default;
|
const helmet = require('helmet').default;
|
||||||
|
@ -33,14 +34,7 @@ util.inspect.defaultOptions.maxStringLength = null;
|
||||||
util.inspect.defaultOptions.depth = 4;
|
util.inspect.defaultOptions.depth = 4;
|
||||||
|
|
||||||
// local library imports
|
// local library imports
|
||||||
const {
|
const userModule = require('./src/users');
|
||||||
initUserStorage,
|
|
||||||
ensurePublicDirectoriesExist,
|
|
||||||
userDataMiddleware,
|
|
||||||
migrateUserData,
|
|
||||||
getCsrfSecret,
|
|
||||||
getCookieSecret,
|
|
||||||
} = require('./src/users');
|
|
||||||
const basicAuthMiddleware = require('./src/middleware/basicAuth');
|
const basicAuthMiddleware = require('./src/middleware/basicAuth');
|
||||||
const whitelistMiddleware = require('./src/middleware/whitelist');
|
const whitelistMiddleware = require('./src/middleware/whitelist');
|
||||||
const contentManager = require('./src/endpoints/content-manager');
|
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 listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
|
||||||
const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
|
const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
|
||||||
const basicAuthMode = getConfigValue('basicAuthMode', false);
|
const basicAuthMode = getConfigValue('basicAuthMode', false);
|
||||||
|
const enableAccounts = getConfigValue('enableUserAccounts', false);
|
||||||
|
|
||||||
const { UPLOADS_PATH } = require('./src/constants');
|
const { UPLOADS_PATH } = require('./src/constants');
|
||||||
|
|
||||||
|
@ -136,40 +131,6 @@ app.use(CORS);
|
||||||
if (listen && basicAuthMode) app.use(basicAuthMiddleware);
|
if (listen && basicAuthMode) app.use(basicAuthMiddleware);
|
||||||
|
|
||||||
app.use(whitelistMiddleware(listen));
|
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) {
|
if (enableCorsProxy) {
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
|
@ -221,17 +182,85 @@ if (enableCorsProxy) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.static(process.cwd() + '/public', {}));
|
app.use(cookieSession({
|
||||||
app.use('/', require('./src/users').router);
|
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.use(userModule.setUserDataMiddleware);
|
||||||
app.get('/', function (request, response) {
|
|
||||||
response.sendFile(process.cwd() + '/public/index.html');
|
// 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
|
// 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') });
|
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) {
|
app.get('/version', async function (_, response) {
|
||||||
const data = await getVersion();
|
const data = await getVersion();
|
||||||
response.send(data);
|
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
|
// 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.
|
// 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();
|
await settingsEndpoint.init();
|
||||||
const directories = await ensurePublicDirectoriesExist();
|
const directories = await userModule.ensurePublicDirectoriesExist();
|
||||||
await migrateUserData();
|
await userModule.migrateUserData();
|
||||||
await contentManager.checkForNewContent(directories);
|
await contentManager.checkForNewContent(directories);
|
||||||
await ensureThumbnailCache();
|
await ensureThumbnailCache();
|
||||||
cleanUploads();
|
cleanUploads();
|
||||||
|
|
383
src/users.js
383
src/users.js
|
@ -7,7 +7,6 @@ const os = require('os');
|
||||||
// Express and other dependencies
|
// Express and other dependencies
|
||||||
const storage = require('node-persist');
|
const storage = require('node-persist');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cookieSession = require('cookie-session');
|
|
||||||
const uuid = require('uuid');
|
const uuid = require('uuid');
|
||||||
const mime = require('mime-types');
|
const mime = require('mime-types');
|
||||||
const slugify = require('slugify').default;
|
const slugify = require('slugify').default;
|
||||||
|
@ -400,7 +399,7 @@ function getCsrfSecret(request) {
|
||||||
* @returns {Promise<string[]>} - The list of user handles
|
* @returns {Promise<string[]>} - The list of user handles
|
||||||
*/
|
*/
|
||||||
async function getAllUserHandles() {
|
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, ''));
|
const handles = keys.map(x => x.replace(KEY_PREFIX, ''));
|
||||||
return handles;
|
return handles;
|
||||||
}
|
}
|
||||||
|
@ -455,87 +454,100 @@ function getUserAvatar(handle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware to add user data to the request object.
|
* Checks if the user should be redirected to the login page.
|
||||||
* @param {import('express').Express} app Express app
|
* @param {import('express').Request} request Request object
|
||||||
* @returns {import('express').RequestHandler}
|
* @returns {boolean} Whether the user should be redirected to the login page
|
||||||
*/
|
*/
|
||||||
function userDataMiddleware(app) {
|
function shouldRedirectToLogin(request) {
|
||||||
app.use(cookieSession({
|
return ENABLE_ACCOUNTS && !request.user;
|
||||||
name: getCookieSessionName(),
|
}
|
||||||
sameSite: 'strict',
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
||||||
secret: getCookieSecret(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware to add user data to the request object.
|
* Tries auto-login if there is only one user and it's not password protected.
|
||||||
* @param {import('express').Request} req Request object
|
* @param {import('express').Request} request Request object
|
||||||
* @param {import('express').Response} res Response object
|
* @returns {Promise<boolean>} Whether auto-login was performed
|
||||||
* @param {import('express').NextFunction} next Next function
|
*/
|
||||||
*/
|
async function tryAutoLogin(request) {
|
||||||
return async (req, res, next) => {
|
if (!ENABLE_ACCOUNTS || request.user || !request.session) {
|
||||||
// Skip for login page
|
return false;
|
||||||
if (req.path === '/login') {
|
}
|
||||||
return next();
|
|
||||||
|
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
|
return false;
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
const directories = getUserDirectories(handle);
|
||||||
req.user = {
|
request.user = {
|
||||||
profile: user,
|
profile: DEFAULT_USER,
|
||||||
directories: directories,
|
directories: directories,
|
||||||
};
|
};
|
||||||
return next();
|
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('/user/files/*', createRouteHandler(req => req.user.directories.files));
|
||||||
router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions));
|
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[]} */
|
/** @type {User[]} */
|
||||||
const users = await storage.values();
|
const users = await storage.values();
|
||||||
const viewModels = users.filter(x => x.enabled).map(user => ({
|
const viewModels = users
|
||||||
handle: user.handle,
|
.filter(x => x.enabled)
|
||||||
name: user.name,
|
.sort((x, y) => x.created - y.created)
|
||||||
avatar: getUserAvatar(user.handle),
|
.map(user => ({
|
||||||
admin: user.admin,
|
handle: user.handle,
|
||||||
password: !!user.password,
|
name: user.name,
|
||||||
}));
|
avatar: getUserAvatar(user.handle),
|
||||||
|
admin: user.admin,
|
||||||
|
password: !!user.password,
|
||||||
|
}));
|
||||||
|
|
||||||
return response.json(viewModels);
|
return response.json(viewModels);
|
||||||
});
|
});
|
||||||
|
|
||||||
endpoints.get('/me', async (request, response) => {
|
publicEndpoints.post('/login', jsonParser, async (request, response) => {
|
||||||
if (!request.user) {
|
if (!request.body.handle) {
|
||||||
return response.sendStatus(401);
|
console.log('Login failed: Missing required fields');
|
||||||
|
return response.status(400).json({ error: 'Missing required fields' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = request.user.profile;
|
/** @type {User} */
|
||||||
const viewModel = {
|
const user = await storage.getItem(toKey(request.body.handle));
|
||||||
handle: user.handle,
|
|
||||||
name: user.name,
|
|
||||||
avatar: getUserAvatar(user.handle),
|
|
||||||
admin: user.admin,
|
|
||||||
password: !!user.password,
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
if (!request.body.handle) {
|
||||||
console.log('Recover step 1 failed: Missing required fields');
|
console.log('Recover step 1 failed: Missing required fields');
|
||||||
return response.status(400).json({ error: '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');
|
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);
|
MFA_CACHE.set(user.handle, mfaCode);
|
||||||
return response.sendStatus(204);
|
return response.sendStatus(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
endpoints.post('/recover-step2', jsonParser, async (request, response) => {
|
publicEndpoints.post('/recover-step2', jsonParser, async (request, response) => {
|
||||||
if (!request.body.handle || !request.body.code || !request.body.password) {
|
if (!request.body.handle || !request.body.code) {
|
||||||
console.log('Recover step 2 failed: Missing required fields');
|
console.log('Recover step 2 failed: Missing required fields');
|
||||||
return response.status(400).json({ error: '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' });
|
return response.status(401).json({ error: 'Incorrect code' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newPassword = request.body.newPassword || '';
|
||||||
const salt = getPasswordSalt();
|
const salt = getPasswordSalt();
|
||||||
user.password = getPasswordHash(request.body.password, salt);
|
user.password = getPasswordHash(newPassword, salt);
|
||||||
user.salt = salt;
|
user.salt = salt;
|
||||||
await storage.setItem(toKey(user.handle), user);
|
await storage.setItem(toKey(user.handle), user);
|
||||||
return response.sendStatus(204);
|
return response.sendStatus(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
endpoints.post('/login', jsonParser, async (request, response) => {
|
const authenticatedEndpoints = express.Router();
|
||||||
if (!request.body.handle || !request.body.password) {
|
|
||||||
console.log('Login failed: Missing required fields');
|
|
||||||
return response.status(400).json({ error: 'Missing required fields' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {User} */
|
authenticatedEndpoints.post('/logout', async (request, response) => {
|
||||||
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) => {
|
|
||||||
request.session?.destroy(() => {
|
request.session?.destroy(() => {
|
||||||
return response.sendStatus(204);
|
return response.sendStatus(204);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
endpoints.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {
|
authenticatedEndpoints.get('/me', async (request, response) => {
|
||||||
if (!request.body.handle) {
|
if (!request.user) {
|
||||||
console.log('Disable user failed: Missing required fields');
|
return response.sendStatus(401);
|
||||||
return response.status(400).json({ error: 'Missing required fields' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.body.handle === request.user.profile.handle) {
|
const user = request.user.profile;
|
||||||
console.log('Disable user failed: Cannot disable yourself');
|
const viewModel = {
|
||||||
return response.status(400).json({ error: 'Cannot disable yourself' });
|
handle: user.handle,
|
||||||
}
|
name: user.name,
|
||||||
|
avatar: getUserAvatar(user.handle),
|
||||||
|
admin: user.admin,
|
||||||
|
password: !!user.password,
|
||||||
|
};
|
||||||
|
|
||||||
/** @type {User} */
|
return response.json(viewModel);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
endpoints.post('/enable', requireAdminMiddleware, jsonParser, async (request, response) => {
|
authenticatedEndpoints.post('/change-password', 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) => {
|
|
||||||
if (!request.body.handle || !request.body.oldPassword || !request.body.newPassword) {
|
if (!request.body.handle || !request.body.oldPassword || !request.body.newPassword) {
|
||||||
console.log('Change password failed: Missing required fields');
|
console.log('Change password failed: Missing required fields');
|
||||||
return response.status(400).json({ error: '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);
|
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) {
|
if (!request.body.handle || !request.body.name) {
|
||||||
console.log('Create user failed: Missing required fields');
|
console.log('Create user failed: Missing required fields');
|
||||||
return response.status(400).json({ error: '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 });
|
return response.json({ handle: newUser.handle });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use('/api/users', endpoints);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initUserStorage,
|
initUserStorage,
|
||||||
ensurePublicDirectoriesExist,
|
ensurePublicDirectoriesExist,
|
||||||
getAllUserHandles,
|
getAllUserHandles,
|
||||||
getUserDirectories,
|
getUserDirectories,
|
||||||
userDataMiddleware,
|
setUserDataMiddleware,
|
||||||
|
requireLoginMiddleware,
|
||||||
migrateUserData,
|
migrateUserData,
|
||||||
getCsrfSecret,
|
getCsrfSecret,
|
||||||
getCookieSecret,
|
getCookieSecret,
|
||||||
|
getCookieSessionName,
|
||||||
router,
|
router,
|
||||||
|
publicEndpoints,
|
||||||
|
authenticatedEndpoints,
|
||||||
|
adminEndpoints,
|
||||||
|
shouldRedirectToLogin,
|
||||||
|
tryAutoLogin,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue