-
Notifications
You must be signed in to change notification settings - Fork 11.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
741 additions
and
698 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
packages/cli/src/UserManagement/controllers/AuthController.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* eslint-disable import/no-cycle */ | ||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ | ||
import type { Request, Response } from 'express'; | ||
import type { IDataObject } from 'n8n-workflow'; | ||
import { Db } from '../..'; | ||
import { issueCookie, resolveJwt } from '../auth/jwt'; | ||
import { AUTH_COOKIE_NAME } from '../../constants'; | ||
import type { User } from '../../databases/entities/User'; | ||
import { Get, Post, RestController } from '../decorators'; | ||
import type { PublicUser } from '../Interfaces'; | ||
import type { LoginRequest } from '../../requests'; | ||
import { compareHash, sanitizeUser } from '../UserManagementHelper'; | ||
import * as config from '../../../config'; | ||
|
||
@RestController() | ||
export class AuthController { | ||
/** | ||
* Log in a user. | ||
* | ||
* Authless endpoint. | ||
*/ | ||
@Post('/login') | ||
async login(req: LoginRequest, res: Response): Promise<PublicUser> { | ||
const { email, password } = req.body; | ||
if (!email) { | ||
throw new Error('Email is required to log in'); | ||
} | ||
|
||
if (!password) { | ||
throw new Error('Password is required to log in'); | ||
} | ||
|
||
let user: User | undefined; | ||
try { | ||
user = await Db.collections.User.findOne( | ||
{ email }, | ||
{ | ||
relations: ['globalRole'], | ||
}, | ||
); | ||
} catch (error) { | ||
throw new Error('Unable to access database.'); | ||
} | ||
|
||
if (!user || !user.password || !(await compareHash(password, user.password))) { | ||
// password is empty until user signs up | ||
const error = new Error('Wrong username or password. Do you have caps lock on?'); | ||
// @ts-ignore | ||
error.httpStatusCode = 401; | ||
throw error; | ||
} | ||
|
||
await issueCookie(res, user); | ||
|
||
return sanitizeUser(user); | ||
} | ||
|
||
/** | ||
* Manually check the `n8n-auth` cookie. | ||
*/ | ||
@Get('/login') | ||
async loginCurrentUser(req: Request, res: Response): Promise<PublicUser> { | ||
// Manually check the existing cookie. | ||
const cookieContents = req.cookies?.[AUTH_COOKIE_NAME] as string | undefined; | ||
|
||
let user: User; | ||
if (cookieContents) { | ||
// If logged in, return user | ||
try { | ||
user = await resolveJwt(cookieContents); | ||
|
||
if (!config.get('userManagement.isInstanceOwnerSetUp')) { | ||
res.cookie(AUTH_COOKIE_NAME, cookieContents); | ||
} | ||
|
||
return sanitizeUser(user); | ||
} catch (error) { | ||
res.clearCookie(AUTH_COOKIE_NAME); | ||
} | ||
} | ||
|
||
if (config.get('userManagement.isInstanceOwnerSetUp')) { | ||
const error = new Error('Not logged in'); | ||
// @ts-ignore | ||
error.httpStatusCode = 401; | ||
throw error; | ||
} | ||
|
||
try { | ||
user = await Db.collections.User.findOneOrFail({ relations: ['globalRole'] }); | ||
} catch (error) { | ||
throw new Error( | ||
'No users found in database - did you wipe the users table? Create at least one user.', | ||
); | ||
} | ||
|
||
if (user.email || user.password) { | ||
throw new Error('Invalid database state - user has password set.'); | ||
} | ||
|
||
await issueCookie(res, user); | ||
|
||
return sanitizeUser(user); | ||
} | ||
|
||
/** | ||
* Log out a user. | ||
* | ||
* Authless endpoint. | ||
*/ | ||
@Post('/logout') | ||
logout(req: Request, res: Response): IDataObject { | ||
res.clearCookie(AUTH_COOKIE_NAME); | ||
return { loggedOut: true }; | ||
} | ||
} |
185 changes: 185 additions & 0 deletions
185
packages/cli/src/UserManagement/controllers/MeController.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/* eslint-disable import/no-cycle */ | ||
import type { Response } from 'express'; | ||
import validator from 'validator'; | ||
import { LoggerProxy as Logger } from 'n8n-workflow'; | ||
import { randomBytes } from 'crypto'; | ||
import { Delete, Get, Patch, Post, RestController } from '../decorators'; | ||
import type { AuthenticatedRequest, MeRequest } from '../../requests'; | ||
import type { PublicUser } from '../Interfaces'; | ||
import { compareHash, hashPassword, sanitizeUser, validatePassword } from '../UserManagementHelper'; | ||
import { Db, InternalHooksManager, ResponseHelper } from '../..'; | ||
import { User } from '../../databases/entities/User'; | ||
import { validateEntity } from '../../GenericHelpers'; | ||
import { issueCookie } from '../auth/jwt'; | ||
|
||
@RestController('/me') | ||
export class MeController { | ||
/** | ||
* Return the logged-in user. | ||
*/ | ||
@Get() | ||
async getCurrentUser(req: AuthenticatedRequest): Promise<PublicUser> { | ||
return sanitizeUser(req.user); | ||
} | ||
|
||
/** | ||
* Update the logged-in user's settings, except password. | ||
*/ | ||
@Patch() | ||
async updateCurrentUser(req: MeRequest.Settings, res: Response): Promise<PublicUser> { | ||
const { email } = req.body; | ||
if (!email) { | ||
Logger.debug('Request to update user email failed because of missing email in payload', { | ||
userId: req.user.id, | ||
payload: req.body, | ||
}); | ||
throw new ResponseHelper.ResponseError('Email is mandatory', undefined, 400); | ||
} | ||
|
||
if (!validator.isEmail(email)) { | ||
Logger.debug('Request to update user email failed because of invalid email in payload', { | ||
userId: req.user.id, | ||
invalidEmail: email, | ||
}); | ||
throw new ResponseHelper.ResponseError('Invalid email address', undefined, 400); | ||
} | ||
|
||
const newUser = new User(); | ||
|
||
Object.assign(newUser, req.user, req.body); | ||
|
||
await validateEntity(newUser); | ||
|
||
const user = await Db.collections.User.save(newUser); | ||
|
||
Logger.info('User updated successfully', { userId: user.id }); | ||
|
||
await issueCookie(res, user); | ||
|
||
const updatedkeys = Object.keys(req.body); | ||
void InternalHooksManager.getInstance().onUserUpdate({ | ||
user_id: req.user.id, | ||
fields_changed: updatedkeys, | ||
}); | ||
|
||
return sanitizeUser(user); | ||
} | ||
|
||
/** | ||
* Update the logged-in user's password. | ||
*/ | ||
@Patch('/password') | ||
async updatePassword(req: MeRequest.Password, res: Response) { | ||
const { currentPassword, newPassword } = req.body; | ||
|
||
if (typeof currentPassword !== 'string' || typeof newPassword !== 'string') { | ||
throw new ResponseHelper.ResponseError('Invalid payload.', undefined, 400); | ||
} | ||
|
||
if (!req.user.password) { | ||
throw new ResponseHelper.ResponseError('Requesting user not set up.'); | ||
} | ||
|
||
const isCurrentPwCorrect = await compareHash(currentPassword, req.user.password); | ||
if (!isCurrentPwCorrect) { | ||
throw new ResponseHelper.ResponseError( | ||
'Provided current password is incorrect.', | ||
undefined, | ||
400, | ||
); | ||
} | ||
|
||
const validPassword = validatePassword(newPassword); | ||
|
||
req.user.password = await hashPassword(validPassword); | ||
|
||
const user = await Db.collections.User.save(req.user); | ||
Logger.info('Password updated successfully', { userId: user.id }); | ||
|
||
await issueCookie(res, user); | ||
|
||
void InternalHooksManager.getInstance().onUserUpdate({ | ||
user_id: req.user.id, | ||
fields_changed: ['password'], | ||
}); | ||
|
||
return { success: true }; | ||
} | ||
|
||
/** | ||
* Store the logged-in user's survey answers. | ||
*/ | ||
@Post('/survey') | ||
async storeSurveyAnswers(req: MeRequest.SurveyAnswers) { | ||
const { body: personalizationAnswers } = req; | ||
|
||
if (!personalizationAnswers) { | ||
Logger.debug('Request to store user personalization survey failed because of empty payload', { | ||
userId: req.user.id, | ||
}); | ||
throw new ResponseHelper.ResponseError( | ||
'Personalization answers are mandatory', | ||
undefined, | ||
400, | ||
); | ||
} | ||
|
||
await Db.collections.User.save({ | ||
id: req.user.id, | ||
personalizationAnswers, | ||
}); | ||
|
||
Logger.info('User survey updated successfully', { userId: req.user.id }); | ||
|
||
void InternalHooksManager.getInstance().onPersonalizationSurveySubmitted( | ||
req.user.id, | ||
personalizationAnswers, | ||
); | ||
|
||
return { success: true }; | ||
} | ||
|
||
/** | ||
* Creates an API Key | ||
*/ | ||
@Post('/api-key') | ||
async createAPIKey(req: AuthenticatedRequest) { | ||
const apiKey = `n8n_api_${randomBytes(40).toString('hex')}`; | ||
|
||
await Db.collections.User.update(req.user.id, { apiKey }); | ||
|
||
const telemetryData = { | ||
user_id: req.user.id, | ||
public_api: false, | ||
}; | ||
|
||
void InternalHooksManager.getInstance().onApiKeyCreated(telemetryData); | ||
|
||
return { apiKey }; | ||
} | ||
|
||
/** | ||
* Get an API Key | ||
*/ | ||
@Get('/api-key') | ||
async getAPIKey(req: AuthenticatedRequest) { | ||
return { apiKey: req.user.apiKey }; | ||
} | ||
|
||
/** | ||
* Deletes an API Key | ||
*/ | ||
@Delete('/api-key') | ||
async deleteAPIKey(req: AuthenticatedRequest) { | ||
await Db.collections.User.update(req.user.id, { apiKey: null }); | ||
|
||
const telemetryData = { | ||
user_id: req.user.id, | ||
public_api: false, | ||
}; | ||
|
||
void InternalHooksManager.getInstance().onApiKeyDeleted(telemetryData); | ||
|
||
return { success: true }; | ||
} | ||
} |
Oops, something went wrong.