mirror of https://github.com/requarks/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
8.0 KiB
324 lines
8.0 KiB
import bcrypt from 'bcryptjs'
|
|
import { userGroups, users as usersTable, userKeys } from '../db/schema.js'
|
|
import { eq } from 'drizzle-orm'
|
|
import { nanoid } from 'nanoid'
|
|
import { DateTime } from 'luxon'
|
|
import { flatten, uniq } from 'es-toolkit/array'
|
|
|
|
/**
|
|
* Users model
|
|
*/
|
|
class Users {
|
|
async getByEmail(email) {
|
|
const res = await WIKI.db.select().from(usersTable).where(eq(usersTable.email, email)).limit(1)
|
|
return res?.[0] ?? null
|
|
}
|
|
async init(ids) {
|
|
WIKI.logger.info('Inserting default users...')
|
|
|
|
await WIKI.db.insert(usersTable).values([
|
|
{
|
|
id: ids.userAdminId,
|
|
email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
|
|
auth: {
|
|
[ids.authModuleId]: {
|
|
password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12),
|
|
mustChangePwd: !process.env.ADMIN_PASS,
|
|
restrictLogin: false,
|
|
tfaIsActive: false,
|
|
tfaRequired: false,
|
|
tfaSecret: ''
|
|
}
|
|
},
|
|
name: 'Administrator',
|
|
isSystem: false,
|
|
isActive: true,
|
|
isVerified: true,
|
|
meta: {
|
|
location: '',
|
|
jobTitle: '',
|
|
pronouns: ''
|
|
},
|
|
prefs: {
|
|
timezone: 'America/New_York',
|
|
dateFormat: 'YYYY-MM-DD',
|
|
timeFormat: '12h',
|
|
appearance: 'site',
|
|
cvd: 'none'
|
|
}
|
|
},
|
|
{
|
|
id: ids.userGuestId,
|
|
email: 'guest@example.com',
|
|
auth: {},
|
|
name: 'Guest',
|
|
isSystem: true,
|
|
isActive: true,
|
|
isVerified: true,
|
|
meta: {},
|
|
prefs: {
|
|
timezone: 'America/New_York',
|
|
dateFormat: 'YYYY-MM-DD',
|
|
timeFormat: '12h',
|
|
appearance: 'site',
|
|
cvd: 'none'
|
|
}
|
|
}
|
|
])
|
|
|
|
await WIKI.db.insert(userGroups).values([
|
|
{
|
|
userId: ids.userAdminId,
|
|
groupId: ids.groupAdminId
|
|
},
|
|
{
|
|
userId: ids.userGuestId,
|
|
groupId: ids.groupGuestId
|
|
}
|
|
])
|
|
}
|
|
|
|
async login({ siteId, strategyId, username, password, ip }, req) {
|
|
if (strategyId in WIKI.auth.strategies) {
|
|
const str = WIKI.auth.strategies[strategyId]
|
|
const strInfo = WIKI.data.authentication.find((a) => a.key === str.module)
|
|
const context = {
|
|
ip,
|
|
siteId,
|
|
...(strInfo.useForm && {
|
|
username,
|
|
password
|
|
})
|
|
}
|
|
|
|
// Authenticate
|
|
const user = await str.authenticate(context)
|
|
|
|
// Perform post-login checks
|
|
return this.afterLoginChecks(
|
|
user,
|
|
strategyId,
|
|
context,
|
|
{
|
|
skipTFA: !strInfo.useForm,
|
|
skipChangePwd: !strInfo.useForm
|
|
},
|
|
req
|
|
)
|
|
} else {
|
|
throw new Error('Invalid Strategy ID')
|
|
}
|
|
}
|
|
|
|
async afterLoginChecks(
|
|
user,
|
|
strategyId,
|
|
context,
|
|
{ skipTFA, skipChangePwd } = { skipTFA: false, skipChangePwd: false },
|
|
req
|
|
) {
|
|
const str = WIKI.auth.strategies[strategyId]
|
|
if (!str) {
|
|
throw new Error('ERR_INVALID_STRATEGY')
|
|
}
|
|
|
|
// Get user groups
|
|
user.groups = await WIKI.db.query.users
|
|
.findFirst({
|
|
columns: {},
|
|
where: {
|
|
id: user.id
|
|
},
|
|
with: {
|
|
groups: {
|
|
columns: {
|
|
id: true,
|
|
permissions: true,
|
|
redirectOnLogin: true
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.then((r) => r?.groups || [])
|
|
|
|
// Get redirect target
|
|
let redirect = '/'
|
|
if (user.groups && user.groups.length > 0) {
|
|
for (const grp of user.groups) {
|
|
if (grp.redirectOnLogin && grp.redirectOnLogin !== '/') {
|
|
redirect = grp.redirectOnLogin
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get auth strategy flags
|
|
const authStr = user.auth[strategyId] || {}
|
|
|
|
// Is 2FA required?
|
|
if (!skipTFA) {
|
|
if (authStr.tfaIsActive && authStr.tfaSecret) {
|
|
try {
|
|
const tfaToken = await WIKI.db.userKeys.generateToken({
|
|
kind: 'tfa',
|
|
userId: user.id,
|
|
meta: {
|
|
strategyId
|
|
}
|
|
})
|
|
return {
|
|
nextAction: 'provideTfa',
|
|
continuationToken: tfaToken,
|
|
redirect
|
|
}
|
|
} catch (errc) {
|
|
WIKI.logger.warn(errc)
|
|
throw new WIKI.Error.AuthGenericError()
|
|
}
|
|
} else if (str.config?.enforceTfa || authStr.tfaRequired) {
|
|
try {
|
|
const tfaQRImage = await user.generateTFA(strategyId, context.siteId)
|
|
const tfaToken = await WIKI.db.userKeys.generateToken({
|
|
kind: 'tfaSetup',
|
|
userId: user.id,
|
|
meta: {
|
|
strategyId
|
|
}
|
|
})
|
|
return {
|
|
nextAction: 'setupTfa',
|
|
continuationToken: tfaToken,
|
|
tfaQRImage,
|
|
redirect
|
|
}
|
|
} catch (errc) {
|
|
WIKI.logger.warn(errc)
|
|
throw new WIKI.Error.AuthGenericError()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Must Change Password?
|
|
if (!skipChangePwd && authStr.mustChangePwd) {
|
|
try {
|
|
const pwdChangeToken = await this.generateToken({
|
|
kind: 'changePwd',
|
|
userId: user.id,
|
|
meta: {
|
|
strategyId
|
|
}
|
|
})
|
|
|
|
return {
|
|
nextAction: 'changePassword',
|
|
continuationToken: pwdChangeToken,
|
|
redirect
|
|
}
|
|
} catch (errc) {
|
|
WIKI.logger.warn(errc)
|
|
throw new WIKI.Error.AuthGenericError()
|
|
}
|
|
}
|
|
|
|
// Set Session Data
|
|
this.updateSession(user, req)
|
|
|
|
return {
|
|
authenticated: true,
|
|
nextAction: 'redirect',
|
|
redirect
|
|
}
|
|
}
|
|
|
|
async loginChangePassword({ strategyId, siteId, continuationToken, newPassword, ip }, req) {
|
|
if (!newPassword || newPassword.length < 8) {
|
|
throw new Error('ERR_PASSWORD_TOO_SHORT')
|
|
}
|
|
const { user, strategyId: expectedStrategyId } = await this.validateToken({
|
|
kind: 'changePwd',
|
|
token: continuationToken
|
|
})
|
|
|
|
if (strategyId !== expectedStrategyId) {
|
|
throw new Error('ERR_INVALID_STRATEGY')
|
|
}
|
|
|
|
if (user) {
|
|
user.auth[strategyId].password = await bcrypt.hash(newPassword, 12)
|
|
user.auth[strategyId].mustChangePwd = false
|
|
await WIKI.db.update(usersTable).set({ auth: user.auth }).where(eq(usersTable.id, user.id))
|
|
|
|
return this.afterLoginChecks(
|
|
user,
|
|
strategyId,
|
|
{ ip, siteId },
|
|
{ skipChangePwd: true, skipTFA: true },
|
|
req
|
|
)
|
|
} else {
|
|
throw new Error('ERR_INVALID_USER')
|
|
}
|
|
}
|
|
|
|
updateSession(user, req) {
|
|
req.session.authenticated = true
|
|
req.session.user = {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
hasAvatar: user.hasAvatar,
|
|
timezone: user.prefs?.timezone,
|
|
dateFormat: user.prefs?.dateFormat,
|
|
timeFormat: user.prefs?.timeFormat,
|
|
appearance: user.prefs?.appearance,
|
|
cvd: user.prefs?.cvd
|
|
}
|
|
req.session.permissions = uniq(flatten(user.groups?.map((g) => g.permissions)))
|
|
}
|
|
|
|
async generateToken({ userId, kind, meta = {} }) {
|
|
WIKI.logger.debug(`Generating ${kind} token for user ${userId}...`)
|
|
const token = await nanoid()
|
|
await WIKI.db.insert(userKeys).values({
|
|
kind,
|
|
token,
|
|
meta,
|
|
validUntil: DateTime.utc().plus({ days: 1 }).toISO(),
|
|
userId
|
|
})
|
|
return token
|
|
}
|
|
|
|
async validateToken({ kind, token, skipDelete }) {
|
|
const res = await WIKI.db.query.userKeys.findFirst({
|
|
where: {
|
|
kind,
|
|
token
|
|
},
|
|
with: {
|
|
user: true
|
|
}
|
|
})
|
|
if (res) {
|
|
if (skipDelete !== true) {
|
|
await WIKI.db.delete(userKeys).where(eq(userKeys.id, res.id))
|
|
}
|
|
if (DateTime.utc() > DateTime.fromISO(res.validUntil)) {
|
|
throw new Error('ERR_EXPIRED_VALIDATION_TOKEN')
|
|
}
|
|
return {
|
|
...res.meta,
|
|
user: res.user
|
|
}
|
|
} else {
|
|
throw new Error('ERR_INVALID_VALIDATION_TOKEN')
|
|
}
|
|
}
|
|
|
|
async destroyToken({ token }) {
|
|
return WIKI.db.delete(userKeys).where(eq(userKeys.token, token))
|
|
}
|
|
}
|
|
|
|
export const users = new Users()
|