mirror of
https://github.com/standardnotes/server
synced 2026-07-14 00:01:54 -04:00
324 lines
9.4 KiB
TypeScript
324 lines
9.4 KiB
TypeScript
import { Request, Response } from 'express'
|
|
import {
|
|
BaseHttpController,
|
|
controller,
|
|
httpGet,
|
|
httpPost,
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
results,
|
|
} from 'inversify-express-utils'
|
|
|
|
import TYPES from '../../Bootstrap/Types'
|
|
import { SessionServiceInterface } from '../../Domain/Session/SessionServiceInterface'
|
|
import { SignIn } from '../../Domain/UseCase/SignIn'
|
|
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
|
|
import { VerifyMFA } from '../../Domain/UseCase/VerifyMFA'
|
|
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
|
|
import { Logger } from 'winston'
|
|
import { GetUserKeyParams } from '../../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
|
|
import { ErrorTag } from '@standardnotes/common'
|
|
import { inject } from 'inversify'
|
|
import { AuthController } from '../../Controller/AuthController'
|
|
|
|
@controller('/auth')
|
|
export class InversifyExpressAuthController extends BaseHttpController {
|
|
constructor(
|
|
@inject(TYPES.SessionService) private sessionService: SessionServiceInterface,
|
|
@inject(TYPES.VerifyMFA) private verifyMFA: VerifyMFA,
|
|
@inject(TYPES.SignIn) private signInUseCase: SignIn,
|
|
@inject(TYPES.GetUserKeyParams) private getUserKeyParams: GetUserKeyParams,
|
|
@inject(TYPES.ClearLoginAttempts) private clearLoginAttempts: ClearLoginAttempts,
|
|
@inject(TYPES.IncreaseLoginAttempts) private increaseLoginAttempts: IncreaseLoginAttempts,
|
|
@inject(TYPES.Logger) private logger: Logger,
|
|
@inject(TYPES.AuthController) private authController: AuthController,
|
|
) {
|
|
super()
|
|
}
|
|
|
|
@httpGet('/params', TYPES.AuthMiddlewareWithoutResponse)
|
|
async params(request: Request, response: Response): Promise<results.JsonResult> {
|
|
if (response.locals.session) {
|
|
const result = await this.getUserKeyParams.execute({
|
|
email: response.locals.user.email,
|
|
authenticated: true,
|
|
authenticatedUser: response.locals.user,
|
|
})
|
|
|
|
return this.json(result.keyParams)
|
|
}
|
|
|
|
if (!request.query.email) {
|
|
return this.json(
|
|
{
|
|
error: {
|
|
message: 'Please provide an email address.',
|
|
},
|
|
},
|
|
400,
|
|
)
|
|
}
|
|
|
|
const verifyMFAResponse = await this.verifyMFA.execute({
|
|
email: <string>request.query.email,
|
|
requestParams: request.query,
|
|
preventOTPFromFurtherUsage: false,
|
|
})
|
|
|
|
if (!verifyMFAResponse.success) {
|
|
return this.json(
|
|
{
|
|
error: {
|
|
tag: verifyMFAResponse.errorTag,
|
|
message: verifyMFAResponse.errorMessage,
|
|
payload: verifyMFAResponse.errorPayload,
|
|
},
|
|
},
|
|
401,
|
|
)
|
|
}
|
|
|
|
const result = await this.getUserKeyParams.execute({
|
|
email: <string>request.query.email,
|
|
authenticated: false,
|
|
})
|
|
|
|
return this.json(result.keyParams)
|
|
}
|
|
|
|
@httpPost('/sign_in', TYPES.LockMiddleware)
|
|
async signIn(request: Request): Promise<results.JsonResult> {
|
|
if (!request.body.email || !request.body.password) {
|
|
this.logger.debug('/auth/sign_in request missing credentials: %O', request.body)
|
|
|
|
return this.json(
|
|
{
|
|
error: {
|
|
tag: 'invalid-auth',
|
|
message: 'Invalid login credentials.',
|
|
},
|
|
},
|
|
401,
|
|
)
|
|
}
|
|
|
|
const verifyMFAResponse = await this.verifyMFA.execute({
|
|
email: request.body.email,
|
|
requestParams: request.body,
|
|
preventOTPFromFurtherUsage: true,
|
|
})
|
|
|
|
if (!verifyMFAResponse.success) {
|
|
return this.json(
|
|
{
|
|
error: {
|
|
tag: verifyMFAResponse.errorTag,
|
|
message: verifyMFAResponse.errorMessage,
|
|
payload: verifyMFAResponse.errorPayload,
|
|
},
|
|
},
|
|
401,
|
|
)
|
|
}
|
|
|
|
const signInResult = await this.signInUseCase.execute({
|
|
apiVersion: request.body.api,
|
|
userAgent: <string>request.headers['user-agent'],
|
|
email: request.body.email,
|
|
password: request.body.password,
|
|
ephemeralSession: request.body.ephemeral ?? false,
|
|
})
|
|
|
|
if (!signInResult.success) {
|
|
await this.increaseLoginAttempts.execute({ email: request.body.email })
|
|
|
|
return this.json(
|
|
{
|
|
error: {
|
|
message: signInResult.errorMessage,
|
|
},
|
|
},
|
|
signInResult.errorCode ?? 401,
|
|
)
|
|
}
|
|
|
|
await this.clearLoginAttempts.execute({ email: request.body.email })
|
|
|
|
return this.json(signInResult.authResponse)
|
|
}
|
|
|
|
@httpPost('/pkce_params', TYPES.AuthMiddlewareWithoutResponse)
|
|
async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
|
|
if (!request.body.code_challenge) {
|
|
return this.json(
|
|
{
|
|
error: {
|
|
message: 'Please provide the code challenge parameter.',
|
|
},
|
|
},
|
|
400,
|
|
)
|
|
}
|
|
|
|
if (response.locals.session) {
|
|
const result = await this.getUserKeyParams.execute({
|
|
email: response.locals.user.email,
|
|
authenticated: true,
|
|
authenticatedUser: response.locals.user,
|
|
codeChallenge: request.body.code_challenge as string,
|
|
})
|
|
|
|
return this.json(result.keyParams)
|
|
}
|
|
|
|
if (!request.body.email) {
|
|
return this.json(
|
|
{
|
|
error: {
|
|
message: 'Please provide an email address.',
|
|
},
|
|
},
|
|
400,
|
|
)
|
|
}
|
|
|
|
const verifyMFAResponse = await this.verifyMFA.execute({
|
|
email: <string>request.body.email,
|
|
requestParams: request.body,
|
|
preventOTPFromFurtherUsage: true,
|
|
})
|
|
|
|
if (!verifyMFAResponse.success) {
|
|
return this.json(
|
|
{
|
|
error: {
|
|
tag: verifyMFAResponse.errorTag,
|
|
message: verifyMFAResponse.errorMessage,
|
|
payload: verifyMFAResponse.errorPayload,
|
|
},
|
|
},
|
|
401,
|
|
)
|
|
}
|
|
|
|
const result = await this.getUserKeyParams.execute({
|
|
email: <string>request.body.email,
|
|
authenticated: false,
|
|
codeChallenge: request.body.code_challenge as string,
|
|
})
|
|
|
|
return this.json(result.keyParams)
|
|
}
|
|
|
|
@httpPost('/pkce_sign_in', TYPES.LockMiddleware)
|
|
async pkceSignIn(request: Request): Promise<results.JsonResult> {
|
|
if (!request.body.email || !request.body.password || !request.body.code_verifier) {
|
|
this.logger.debug('/auth/sign_in request missing credentials: %O', request.body)
|
|
|
|
return this.json(
|
|
{
|
|
error: {
|
|
tag: 'invalid-auth',
|
|
message: 'Invalid login credentials.',
|
|
},
|
|
},
|
|
401,
|
|
)
|
|
}
|
|
|
|
const signInResult = await this.signInUseCase.execute({
|
|
apiVersion: request.body.api,
|
|
userAgent: <string>request.headers['user-agent'],
|
|
email: request.body.email,
|
|
password: request.body.password,
|
|
ephemeralSession: request.body.ephemeral ?? false,
|
|
codeVerifier: request.body.code_verifier,
|
|
})
|
|
|
|
if (!signInResult.success) {
|
|
await this.increaseLoginAttempts.execute({ email: request.body.email })
|
|
|
|
return this.json(
|
|
{
|
|
error: {
|
|
message: signInResult.errorMessage,
|
|
},
|
|
},
|
|
401,
|
|
)
|
|
}
|
|
|
|
await this.clearLoginAttempts.execute({ email: request.body.email })
|
|
|
|
return this.json(signInResult.authResponse)
|
|
}
|
|
|
|
@httpPost('/recovery/codes', TYPES.ApiGatewayAuthMiddleware)
|
|
async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
|
|
const result = await this.authController.generateRecoveryCodes({
|
|
userUuid: response.locals.user.uuid,
|
|
})
|
|
|
|
return this.json(result.data, result.status)
|
|
}
|
|
|
|
@httpPost('/recovery/login', TYPES.LockMiddleware)
|
|
async recoveryLogin(request: Request): Promise<results.JsonResult> {
|
|
const result = await this.authController.signInWithRecoveryCodes({
|
|
apiVersion: request.body.api_version,
|
|
userAgent: <string>request.headers['user-agent'],
|
|
codeVerifier: request.body.code_verifier,
|
|
username: request.body.username,
|
|
recoveryCodes: request.body.recovery_codes,
|
|
password: request.body.password,
|
|
})
|
|
|
|
return this.json(result.data, result.status)
|
|
}
|
|
|
|
@httpPost('/recovery/params')
|
|
async recoveryParams(request: Request): Promise<results.JsonResult> {
|
|
const result = await this.authController.recoveryKeyParams({
|
|
apiVersion: request.body.api_version,
|
|
username: request.body.username,
|
|
codeChallenge: request.body.code_challenge,
|
|
recoveryCodes: request.body.recovery_codes,
|
|
})
|
|
|
|
return this.json(result.data, result.status)
|
|
}
|
|
|
|
@httpPost('/sign_out', TYPES.AuthMiddlewareWithoutResponse)
|
|
async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
|
|
if (response.locals.readOnlyAccess) {
|
|
return this.json(
|
|
{
|
|
error: {
|
|
tag: ErrorTag.ReadOnlyAccess,
|
|
message: 'Session has read-only access.',
|
|
},
|
|
},
|
|
401,
|
|
)
|
|
}
|
|
|
|
const authorizationHeader = <string>request.headers.authorization
|
|
|
|
const userUuid = await this.sessionService.deleteSessionByToken(authorizationHeader.replace('Bearer ', ''))
|
|
|
|
if (userUuid !== null) {
|
|
response.setHeader('x-invalidate-cache', userUuid)
|
|
}
|
|
response.status(204).send()
|
|
}
|
|
|
|
@httpPost('/')
|
|
async register(request: Request): Promise<results.JsonResult> {
|
|
const response = await this.authController.register({
|
|
...request.body,
|
|
userAgent: <string>request.headers['user-agent'],
|
|
})
|
|
|
|
return this.json(response.data, response.status)
|
|
}
|
|
}
|