mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
fix: active sessions are now destroyed if auth/reset and auth/validate endpoints are used (#806)
This commit is contained in:
parent
0de4c98a58
commit
578078e03f
@ -2,29 +2,29 @@
|
|||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { Knex } from 'knex';
|
|
||||||
import { AccessStore } from './access-store';
|
|
||||||
import { ResetTokenStore } from './reset-token-store';
|
|
||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
import { IUnleashStores } from '../types/stores';
|
import { IUnleashStores } from '../types/stores';
|
||||||
|
|
||||||
const { createDb } = require('./db-pool');
|
import { createDb } from './db-pool';
|
||||||
const EventStore = require('./event-store');
|
import EventStore from './event-store';
|
||||||
const FeatureToggleStore = require('./feature-toggle-store');
|
import FeatureToggleStore from './feature-toggle-store';
|
||||||
const FeatureTypeStore = require('./feature-type-store');
|
import FeatureTypeStore from './feature-type-store';
|
||||||
const StrategyStore = require('./strategy-store');
|
import StrategyStore from './strategy-store';
|
||||||
const ClientInstanceStore = require('./client-instance-store');
|
import ClientInstanceStore from './client-instance-store';
|
||||||
const ClientMetricsDb = require('./client-metrics-db');
|
import ClientMetricsDb from './client-metrics-db';
|
||||||
const ClientMetricsStore = require('./client-metrics-store');
|
import ClientMetricsStore from './client-metrics-store';
|
||||||
const ClientApplicationsStore = require('./client-applications-store');
|
import ClientApplicationsStore from './client-applications-store';
|
||||||
const ContextFieldStore = require('./context-field-store');
|
import ContextFieldStore from './context-field-store';
|
||||||
const SettingStore = require('./setting-store');
|
import SettingStore from './setting-store';
|
||||||
const UserStore = require('./user-store');
|
import UserStore from './user-store';
|
||||||
const ProjectStore = require('./project-store');
|
import ProjectStore from './project-store';
|
||||||
const TagStore = require('./tag-store');
|
import TagStore from './tag-store';
|
||||||
const TagTypeStore = require('./tag-type-store');
|
import TagTypeStore from './tag-type-store';
|
||||||
const AddonStore = require('./addon-store');
|
import AddonStore from './addon-store';
|
||||||
const { ApiTokenStore } = require('./api-token-store');
|
import { ApiTokenStore } from './api-token-store';
|
||||||
|
import SessionStore from './session-store';
|
||||||
|
import { AccessStore } from './access-store';
|
||||||
|
import { ResetTokenStore } from './reset-token-store';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -41,11 +41,7 @@ export const createStores = (
|
|||||||
featureToggleStore: new FeatureToggleStore(db, eventBus, getLogger),
|
featureToggleStore: new FeatureToggleStore(db, eventBus, getLogger),
|
||||||
featureTypeStore: new FeatureTypeStore(db, getLogger),
|
featureTypeStore: new FeatureTypeStore(db, getLogger),
|
||||||
strategyStore: new StrategyStore(db, getLogger),
|
strategyStore: new StrategyStore(db, getLogger),
|
||||||
clientApplicationsStore: new ClientApplicationsStore(
|
clientApplicationsStore: new ClientApplicationsStore(db, eventBus),
|
||||||
db,
|
|
||||||
eventBus,
|
|
||||||
getLogger,
|
|
||||||
),
|
|
||||||
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
|
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
|
||||||
clientMetricsStore: new ClientMetricsStore(
|
clientMetricsStore: new ClientMetricsStore(
|
||||||
clientMetricsDb,
|
clientMetricsDb,
|
||||||
@ -62,6 +58,7 @@ export const createStores = (
|
|||||||
accessStore: new AccessStore(db, eventBus, getLogger),
|
accessStore: new AccessStore(db, eventBus, getLogger),
|
||||||
apiTokenStore: new ApiTokenStore(db, eventBus, getLogger),
|
apiTokenStore: new ApiTokenStore(db, eventBus, getLogger),
|
||||||
resetTokenStore: new ResetTokenStore(db, eventBus, getLogger),
|
resetTokenStore: new ResetTokenStore(db, eventBus, getLogger),
|
||||||
|
sessionStore: new SessionStore(db, eventBus, getLogger),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
105
src/lib/db/session-store.ts
Normal file
105
src/lib/db/session-store.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import EventEmitter from 'events';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Logger, LogProvider } from '../logger';
|
||||||
|
import NotFoundError from '../error/notfound-error';
|
||||||
|
|
||||||
|
const TABLE = 'unleash_session';
|
||||||
|
|
||||||
|
interface ISessionRow {
|
||||||
|
sid: string;
|
||||||
|
sess: string;
|
||||||
|
created_at: Date;
|
||||||
|
expired?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISession {
|
||||||
|
sid: string;
|
||||||
|
sess: any;
|
||||||
|
createdAt: Date;
|
||||||
|
expired?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SessionStore {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private eventBus: EventEmitter;
|
||||||
|
|
||||||
|
private db: Knex;
|
||||||
|
|
||||||
|
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
|
||||||
|
this.db = db;
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.logger = getLogger('lib/db/session-store.ts');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveSessions(): Promise<ISession[]> {
|
||||||
|
const rows = await this.db<ISessionRow>(TABLE)
|
||||||
|
.whereNull('expired')
|
||||||
|
.orWhere('expired', '>', new Date())
|
||||||
|
.orderBy('created_at', 'desc');
|
||||||
|
return rows.map(this.rowToSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSessionsForUser(userId: number): Promise<ISession[]> {
|
||||||
|
const rows = await this.db<ISessionRow>(
|
||||||
|
TABLE,
|
||||||
|
).whereRaw(`(sess -> 'user' ->> 'id')::int = ?`, [userId]);
|
||||||
|
if (rows && rows.length > 0) {
|
||||||
|
return rows.map(this.rowToSession);
|
||||||
|
}
|
||||||
|
throw new NotFoundError(
|
||||||
|
`Could not find sessions for user with id ${userId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSession(sid: string): Promise<ISession> {
|
||||||
|
const row = await this.db<ISessionRow>(TABLE)
|
||||||
|
.where('sid', '=', sid)
|
||||||
|
.first();
|
||||||
|
if (row) {
|
||||||
|
return this.rowToSession(row);
|
||||||
|
}
|
||||||
|
throw new NotFoundError(`Could not find session with sid ${sid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSessionsForUser(userId: number): Promise<void> {
|
||||||
|
await this.db<ISessionRow>(TABLE)
|
||||||
|
.whereRaw(`(sess -> 'user' ->> 'id')::int = ?`, [userId])
|
||||||
|
.del();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSession(sid: string): Promise<void> {
|
||||||
|
await this.db<ISessionRow>(TABLE)
|
||||||
|
.where('sid', '=', sid)
|
||||||
|
.del();
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertSession(data: Omit<ISession, 'createdAt'>): Promise<ISession> {
|
||||||
|
const row = await this.db<ISessionRow>(TABLE)
|
||||||
|
.insert({
|
||||||
|
sid: data.sid,
|
||||||
|
sess: JSON.stringify(data.sess),
|
||||||
|
expired: data.expired || new Date(Date.now() + 86400000),
|
||||||
|
})
|
||||||
|
.returning<ISessionRow>(['sid', 'sess', 'created_at', 'expired']);
|
||||||
|
if (row) {
|
||||||
|
return this.rowToSession(row);
|
||||||
|
}
|
||||||
|
throw new Error('Could not insert session');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<void> {
|
||||||
|
await this.db(TABLE).del();
|
||||||
|
}
|
||||||
|
|
||||||
|
private rowToSession(row: ISessionRow): ISession {
|
||||||
|
return {
|
||||||
|
sid: row.sid,
|
||||||
|
sess: row.sess,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
expired: row.expired,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SessionStore;
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { ADMIN } from '../../permissions';
|
import { ADMIN } from '../../permissions';
|
||||||
import UserService from '../../services/user-service';
|
import UserService from '../../services/user-service';
|
||||||
@ -8,6 +9,7 @@ import { IUnleashConfig } from '../../types/option';
|
|||||||
import { EmailService, MAIL_ACCEPTED } from '../../services/email-service';
|
import { EmailService, MAIL_ACCEPTED } from '../../services/email-service';
|
||||||
import ResetTokenService from '../../services/reset-token-service';
|
import ResetTokenService from '../../services/reset-token-service';
|
||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices } from '../../types/services';
|
||||||
|
import SessionService from '../../services/session-service';
|
||||||
|
|
||||||
const getCreatorUsernameOrPassword = req => req.user.username || req.user.email;
|
const getCreatorUsernameOrPassword = req => req.user.username || req.user.email;
|
||||||
|
|
||||||
@ -22,6 +24,8 @@ export default class UserAdminController extends Controller {
|
|||||||
|
|
||||||
private resetTokenService: ResetTokenService;
|
private resetTokenService: ResetTokenService;
|
||||||
|
|
||||||
|
private sessionService: SessionService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
{
|
{
|
||||||
@ -29,12 +33,14 @@ export default class UserAdminController extends Controller {
|
|||||||
accessService,
|
accessService,
|
||||||
emailService,
|
emailService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
|
sessionService,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
| 'userService'
|
| 'userService'
|
||||||
| 'accessService'
|
| 'accessService'
|
||||||
| 'emailService'
|
| 'emailService'
|
||||||
| 'resetTokenService'
|
| 'resetTokenService'
|
||||||
|
| 'sessionService'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
@ -43,6 +49,7 @@ export default class UserAdminController extends Controller {
|
|||||||
this.logger = config.getLogger('routes/user-controller.ts');
|
this.logger = config.getLogger('routes/user-controller.ts');
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
this.resetTokenService = resetTokenService;
|
this.resetTokenService = resetTokenService;
|
||||||
|
this.sessionService = sessionService;
|
||||||
|
|
||||||
this.get('/', this.getUsers, ADMIN);
|
this.get('/', this.getUsers, ADMIN);
|
||||||
this.get('/search', this.search);
|
this.get('/search', this.search);
|
||||||
@ -52,6 +59,7 @@ export default class UserAdminController extends Controller {
|
|||||||
this.post('/:id/change-password', this.changePassword, ADMIN);
|
this.post('/:id/change-password', this.changePassword, ADMIN);
|
||||||
this.delete('/:id', this.deleteUser, ADMIN);
|
this.delete('/:id', this.deleteUser, ADMIN);
|
||||||
this.post('/reset-password', this.resetPassword);
|
this.post('/reset-password', this.resetPassword);
|
||||||
|
this.get('/active-sessions', this.getActiveSessions, ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
@ -88,6 +96,15 @@ export default class UserAdminController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getActiveSessions(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const sessions = await this.sessionService.getActiveSessions();
|
||||||
|
res.json(sessions);
|
||||||
|
} catch (error) {
|
||||||
|
handleErrors(res, this.logger, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async search(req, res): Promise<void> {
|
async search(req, res): Promise<void> {
|
||||||
const { q } = req.query;
|
const { q } = req.query;
|
||||||
|
@ -10,6 +10,7 @@ import UserService from '../../services/user-service';
|
|||||||
import User from '../../types/user';
|
import User from '../../types/user';
|
||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { handleErrors } from './util';
|
import { handleErrors } from './util';
|
||||||
|
import SessionService from '../../services/session-service';
|
||||||
|
|
||||||
interface IChangeUserRequest {
|
interface IChangeUserRequest {
|
||||||
password: string;
|
password: string;
|
||||||
@ -26,6 +27,8 @@ class UserController extends Controller {
|
|||||||
|
|
||||||
private userService: UserService;
|
private userService: UserService;
|
||||||
|
|
||||||
|
private sessionService: SessionService;
|
||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -33,15 +36,21 @@ class UserController extends Controller {
|
|||||||
{
|
{
|
||||||
accessService,
|
accessService,
|
||||||
userService,
|
userService,
|
||||||
}: Pick<IUnleashServices, 'accessService' | 'userService'>,
|
sessionService,
|
||||||
|
}: Pick<
|
||||||
|
IUnleashServices,
|
||||||
|
'accessService' | 'userService' | 'sessionService'
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
this.accessService = accessService;
|
this.accessService = accessService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.sessionService = sessionService;
|
||||||
this.logger = config.getLogger('lib/routes/admin-api/user.ts');
|
this.logger = config.getLogger('lib/routes/admin-api/user.ts');
|
||||||
|
|
||||||
this.get('/', this.getUser);
|
this.get('/', this.getUser);
|
||||||
this.post('/change-password', this.updateUserPass);
|
this.post('/change-password', this.updateUserPass);
|
||||||
|
this.get('/my-sessions', this.mySessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(req: IAuthRequest, res: Response): Promise<void> {
|
async getUser(req: IAuthRequest, res: Response): Promise<void> {
|
||||||
@ -81,6 +90,25 @@ class UserController extends Controller {
|
|||||||
res.status(401).end();
|
res.status(401).end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async mySessions(
|
||||||
|
req: UserRequest<any, any, any, any>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const { user } = req;
|
||||||
|
if (user) {
|
||||||
|
try {
|
||||||
|
const sessions = await this.sessionService.getSessionsForUser(
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
res.json(sessions);
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(401).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = UserController;
|
module.exports = UserController;
|
||||||
|
@ -4,10 +4,7 @@ import UserService from '../../services/user-service';
|
|||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { handleErrors } from '../admin-api/util';
|
import { handleErrors } from '../admin-api/util';
|
||||||
import { IUnleashConfig } from '../../types/option';
|
import { IUnleashConfig } from '../../types/option';
|
||||||
|
import { IUnleashServices } from '../../types/services';
|
||||||
interface IServices {
|
|
||||||
userService: UserService;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IValidateQuery {
|
interface IValidateQuery {
|
||||||
token: string;
|
token: string;
|
||||||
@ -18,13 +15,19 @@ interface IChangePasswordBody {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionRequest<PARAMS, QUERY, BODY, K>
|
||||||
|
extends Request<PARAMS, QUERY, BODY, K> {
|
||||||
|
session?;
|
||||||
|
user?;
|
||||||
|
}
|
||||||
|
|
||||||
const UNLEASH = 'Unleash';
|
const UNLEASH = 'Unleash';
|
||||||
class ResetPasswordController extends Controller {
|
class ResetPasswordController extends Controller {
|
||||||
userService: UserService;
|
private userService: UserService;
|
||||||
|
|
||||||
logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(config: IUnleashConfig, { userService }: IServices) {
|
constructor(config: IUnleashConfig, { userService }: IUnleashServices) {
|
||||||
super(config);
|
super(config);
|
||||||
this.logger = config.getLogger(
|
this.logger = config.getLogger(
|
||||||
'lib/routes/auth/reset-password-controller.ts',
|
'lib/routes/auth/reset-password-controller.ts',
|
||||||
@ -65,6 +68,7 @@ class ResetPasswordController extends Controller {
|
|||||||
const { token } = req.query;
|
const { token } = req.query;
|
||||||
try {
|
try {
|
||||||
const user = await this.userService.getUserForToken(token);
|
const user = await this.userService.getUserForToken(token);
|
||||||
|
await this.logout(req);
|
||||||
res.status(200).json(user);
|
res.status(200).json(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleErrors(res, this.logger, e);
|
handleErrors(res, this.logger, e);
|
||||||
@ -75,6 +79,7 @@ class ResetPasswordController extends Controller {
|
|||||||
req: Request<unknown, unknown, IChangePasswordBody, unknown>,
|
req: Request<unknown, unknown, IChangePasswordBody, unknown>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await this.logout(req);
|
||||||
const { token, password } = req.body;
|
const { token, password } = req.body;
|
||||||
try {
|
try {
|
||||||
await this.userService.resetPassword(token, password);
|
await this.userService.resetPassword(token, password);
|
||||||
@ -83,6 +88,12 @@ class ResetPasswordController extends Controller {
|
|||||||
handleErrors(res, this.logger, e);
|
handleErrors(res, this.logger, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async logout(req: SessionRequest<any, any, any, any>) {
|
||||||
|
if (req.session) {
|
||||||
|
req.session.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResetPasswordController;
|
export default ResetPasswordController;
|
||||||
|
@ -16,19 +16,19 @@ const MASKED_VALUE = '*****';
|
|||||||
class AddonService {
|
class AddonService {
|
||||||
constructor(
|
constructor(
|
||||||
{ addonStore, eventStore, featureToggleStore },
|
{ addonStore, eventStore, featureToggleStore },
|
||||||
{ getLogger, unleashUrl },
|
config,
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
) {
|
) {
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
this.addonStore = addonStore;
|
this.addonStore = addonStore;
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.getLogger = getLogger;
|
this.getLogger = config.getLogger;
|
||||||
this.logger = getLogger('services/addon-service.js');
|
this.logger = config.getLogger('services/addon-service.js');
|
||||||
this.tagTypeService = tagTypeService;
|
this.tagTypeService = tagTypeService;
|
||||||
|
|
||||||
this.addonProviders = this.loadProviders({
|
this.addonProviders = this.loadProviders({
|
||||||
getLogger,
|
getLogger: config.getLogger,
|
||||||
unleashUrl,
|
unleashUrl: config.server.unleashUrl,
|
||||||
});
|
});
|
||||||
this.sensitiveParams = this.loadSensitiveParams(this.addonProviders);
|
this.sensitiveParams = this.loadSensitiveParams(this.addonProviders);
|
||||||
if (addonStore) {
|
if (addonStore) {
|
||||||
|
@ -88,7 +88,11 @@ function getSetup() {
|
|||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
const tagTypeService = new TagTypeService(stores, { getLogger });
|
const tagTypeService = new TagTypeService(stores, { getLogger });
|
||||||
return {
|
return {
|
||||||
addonService: new AddonService(stores, { getLogger }, tagTypeService),
|
addonService: new AddonService(
|
||||||
|
stores,
|
||||||
|
{ getLogger, server: { unleashUrl: 'http://test' } },
|
||||||
|
tagTypeService,
|
||||||
|
),
|
||||||
stores,
|
stores,
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
};
|
};
|
||||||
|
@ -5,22 +5,23 @@ import FeatureTypeService from './feature-type-service';
|
|||||||
import EventService from './event-service';
|
import EventService from './event-service';
|
||||||
import HealthService from './health-service';
|
import HealthService from './health-service';
|
||||||
|
|
||||||
const FeatureToggleService = require('./feature-toggle-service');
|
import FeatureToggleService from './feature-toggle-service';
|
||||||
const ProjectService = require('./project-service');
|
import ProjectService from './project-service';
|
||||||
const StateService = require('./state-service');
|
import StateService from './state-service';
|
||||||
const ClientMetricsService = require('./client-metrics');
|
import ClientMetricsService from './client-metrics';
|
||||||
const TagTypeService = require('./tag-type-service');
|
import TagTypeService from './tag-type-service';
|
||||||
const TagService = require('./tag-service');
|
import TagService from './tag-service';
|
||||||
const StrategyService = require('./strategy-service');
|
import StrategyService from './strategy-service';
|
||||||
const AddonService = require('./addon-service');
|
import AddonService from './addon-service';
|
||||||
const ContextService = require('./context-service');
|
import ContextService from './context-service';
|
||||||
const VersionService = require('./version-service');
|
import VersionService from './version-service';
|
||||||
const { EmailService } = require('./email-service');
|
import { EmailService } from './email-service';
|
||||||
const { AccessService } = require('./access-service');
|
import { AccessService } from './access-service';
|
||||||
const { ApiTokenService } = require('./api-token-service');
|
import { ApiTokenService } from './api-token-service';
|
||||||
const UserService = require('./user-service');
|
import UserService from './user-service';
|
||||||
const ResetTokenService = require('./reset-token-service');
|
import ResetTokenService from './reset-token-service';
|
||||||
const SettingService = require('./setting-service');
|
import SettingService from './setting-service';
|
||||||
|
import SessionService from './session-service';
|
||||||
|
|
||||||
export const createServices = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -32,11 +33,7 @@ export const createServices = (
|
|||||||
const contextService = new ContextService(stores, config);
|
const contextService = new ContextService(stores, config);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
const eventService = new EventService(stores, config);
|
const eventService = new EventService(stores, config);
|
||||||
const featureToggleService = new FeatureToggleService(
|
const featureToggleService = new FeatureToggleService(stores, config);
|
||||||
stores,
|
|
||||||
config,
|
|
||||||
accessService,
|
|
||||||
);
|
|
||||||
const featureTypeService = new FeatureTypeService(stores, config);
|
const featureTypeService = new FeatureTypeService(stores, config);
|
||||||
const projectService = new ProjectService(stores, config, accessService);
|
const projectService = new ProjectService(stores, config, accessService);
|
||||||
const resetTokenService = new ResetTokenService(stores, config);
|
const resetTokenService = new ResetTokenService(stores, config);
|
||||||
@ -45,10 +42,12 @@ export const createServices = (
|
|||||||
const tagService = new TagService(stores, config);
|
const tagService = new TagService(stores, config);
|
||||||
const tagTypeService = new TagTypeService(stores, config);
|
const tagTypeService = new TagTypeService(stores, config);
|
||||||
const addonService = new AddonService(stores, config, tagTypeService);
|
const addonService = new AddonService(stores, config, tagTypeService);
|
||||||
|
const sessionService = new SessionService(stores, config);
|
||||||
const userService = new UserService(stores, config, {
|
const userService = new UserService(stores, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
const versionService = new VersionService(stores, config);
|
const versionService = new VersionService(stores, config);
|
||||||
const healthService = new HealthService(stores, config);
|
const healthService = new HealthService(stores, config);
|
||||||
@ -74,6 +73,7 @@ export const createServices = (
|
|||||||
resetTokenService,
|
resetTokenService,
|
||||||
eventService,
|
eventService,
|
||||||
settingService,
|
settingService,
|
||||||
|
sessionService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
47
src/lib/services/session-service.ts
Normal file
47
src/lib/services/session-service.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { IUnleashStores } from '../types/stores';
|
||||||
|
import { IUnleashConfig } from '../types/option';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
import SessionStore, { ISession } from '../db/session-store';
|
||||||
|
|
||||||
|
export default class SessionService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private sessionStore: SessionStore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{ sessionStore }: Pick<IUnleashStores, 'sessionStore'>,
|
||||||
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||||
|
) {
|
||||||
|
this.logger = getLogger('lib/services/session-service.ts');
|
||||||
|
this.sessionStore = sessionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveSessions(): Promise<ISession[]> {
|
||||||
|
return this.sessionStore.getActiveSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSessionsForUser(userId: number): Promise<ISession[]> {
|
||||||
|
return this.sessionStore.getSessionsForUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSession(sid: string): Promise<ISession> {
|
||||||
|
return this.sessionStore.getSession(sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSessionsForUser(userId: number): Promise<void> {
|
||||||
|
return this.sessionStore.deleteSessionsForUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSession(sid: string): Promise<void> {
|
||||||
|
return this.sessionStore.deleteSession(sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertSession({
|
||||||
|
sid,
|
||||||
|
sess,
|
||||||
|
}: Pick<ISession, 'sid' | 'sess'>): Promise<ISession> {
|
||||||
|
return this.sessionStore.insertSession({ sid, sess });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SessionService;
|
@ -8,6 +8,8 @@ import { EmailService } from './email-service';
|
|||||||
import OwaspValidationError from '../error/owasp-validation-error';
|
import OwaspValidationError from '../error/owasp-validation-error';
|
||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
import { createTestConfig } from '../../test/config/test-config';
|
import { createTestConfig } from '../../test/config/test-config';
|
||||||
|
import SessionService from './session-service';
|
||||||
|
import FakeSessionStore from '../../test/fixtures/fake-session-store';
|
||||||
|
|
||||||
const config: IUnleashConfig = createTestConfig();
|
const config: IUnleashConfig = createTestConfig();
|
||||||
|
|
||||||
@ -19,12 +21,15 @@ test('Should create new user', async t => {
|
|||||||
{ userStore, resetTokenStore },
|
{ userStore, resetTokenStore },
|
||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
|
||||||
const service = new UserService({ userStore }, config, {
|
const service = new UserService({ userStore }, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
const user = await service.createUser({
|
const user = await service.createUser({
|
||||||
username: 'test',
|
username: 'test',
|
||||||
@ -48,11 +53,14 @@ test('Should create default user', async t => {
|
|||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
|
|
||||||
const service = new UserService({ userStore }, config, {
|
const service = new UserService({ userStore }, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.initAdminUser();
|
await service.initAdminUser();
|
||||||
@ -71,11 +79,14 @@ test('Should be a valid password', async t => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
|
|
||||||
const service = new UserService({ userStore }, config, {
|
const service = new UserService({ userStore }, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
const valid = service.validatePassword('this is a strong password!');
|
const valid = service.validatePassword('this is a strong password!');
|
||||||
@ -92,11 +103,14 @@ test('Password must be at least 10 chars', async t => {
|
|||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
|
|
||||||
const service = new UserService({ userStore }, config, {
|
const service = new UserService({ userStore }, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
t.throws(() => service.validatePassword('admin'), {
|
t.throws(() => service.validatePassword('admin'), {
|
||||||
@ -114,11 +128,14 @@ test('The password must contain at least one uppercase letter.', async t => {
|
|||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
|
|
||||||
const service = new UserService({ userStore }, config, {
|
const service = new UserService({ userStore }, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
t.throws(() => service.validatePassword('qwertyabcde'), {
|
t.throws(() => service.validatePassword('qwertyabcde'), {
|
||||||
@ -137,10 +154,14 @@ test('The password must contain at least one number', async t => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
|
|
||||||
const service = new UserService({ userStore }, config, {
|
const service = new UserService({ userStore }, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
t.throws(() => service.validatePassword('qwertyabcdE'), {
|
t.throws(() => service.validatePassword('qwertyabcdE'), {
|
||||||
@ -158,11 +179,14 @@ test('The password must contain at least one special character', async t => {
|
|||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
|
|
||||||
const service = new UserService({ userStore }, config, {
|
const service = new UserService({ userStore }, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
t.throws(() => service.validatePassword('qwertyabcdE2'), {
|
t.throws(() => service.validatePassword('qwertyabcdE2'), {
|
||||||
@ -180,11 +204,14 @@ test('Should be a valid password with special chars', async t => {
|
|||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
|
|
||||||
const service = new UserService({ userStore }, config, {
|
const service = new UserService({ userStore }, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
const valid = service.validatePassword('this is a strong password!');
|
const valid = service.validatePassword('this is a strong password!');
|
||||||
|
@ -15,6 +15,9 @@ import NotFoundError from '../error/notfound-error';
|
|||||||
import OwaspValidationError from '../error/owasp-validation-error';
|
import OwaspValidationError from '../error/owasp-validation-error';
|
||||||
import { EmailService } from './email-service';
|
import { EmailService } from './email-service';
|
||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
|
import SessionService from './session-service';
|
||||||
|
import { IUnleashServices } from '../types/services';
|
||||||
|
import { IUnleashStores } from '../types/stores';
|
||||||
|
|
||||||
export interface ICreateUser {
|
export interface ICreateUser {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -45,16 +48,6 @@ interface ITokenUser extends IUpdateUser {
|
|||||||
role: IRoleDescription;
|
role: IRoleDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IStores {
|
|
||||||
userStore: UserStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IServices {
|
|
||||||
accessService: AccessService;
|
|
||||||
resetTokenService: ResetTokenService;
|
|
||||||
emailService: EmailService;
|
|
||||||
}
|
|
||||||
|
|
||||||
const saltRounds = 10;
|
const saltRounds = 10;
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
@ -66,21 +59,35 @@ class UserService {
|
|||||||
|
|
||||||
private resetTokenService: ResetTokenService;
|
private resetTokenService: ResetTokenService;
|
||||||
|
|
||||||
|
private sessionService: SessionService;
|
||||||
|
|
||||||
private emailService: EmailService;
|
private emailService: EmailService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stores: IStores,
|
stores: Pick<IUnleashStores, 'userStore'>,
|
||||||
{
|
{
|
||||||
getLogger,
|
getLogger,
|
||||||
authentication,
|
authentication,
|
||||||
}: Pick<IUnleashConfig, 'getLogger' | 'authentication'>,
|
}: Pick<IUnleashConfig, 'getLogger' | 'authentication'>,
|
||||||
{ accessService, resetTokenService, emailService }: IServices,
|
{
|
||||||
|
accessService,
|
||||||
|
resetTokenService,
|
||||||
|
emailService,
|
||||||
|
sessionService,
|
||||||
|
}: Pick<
|
||||||
|
IUnleashServices,
|
||||||
|
| 'accessService'
|
||||||
|
| 'resetTokenService'
|
||||||
|
| 'emailService'
|
||||||
|
| 'sessionService'
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger('service/user-service.js');
|
this.logger = getLogger('service/user-service.js');
|
||||||
this.store = stores.userStore;
|
this.store = stores.userStore;
|
||||||
this.accessService = accessService;
|
this.accessService = accessService;
|
||||||
this.resetTokenService = resetTokenService;
|
this.resetTokenService = resetTokenService;
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
|
this.sessionService = sessionService;
|
||||||
if (authentication && authentication.createAdminUser) {
|
if (authentication && authentication.createAdminUser) {
|
||||||
process.nextTick(() => this.initAdminUser());
|
process.nextTick(() => this.initAdminUser());
|
||||||
}
|
}
|
||||||
@ -288,6 +295,11 @@ class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the password is a strong password will update password and delete all sessions for the user we're changing the password for
|
||||||
|
* @param token - the token authenticating this request
|
||||||
|
* @param password - new password
|
||||||
|
*/
|
||||||
async resetPassword(token: string, password: string): Promise<void> {
|
async resetPassword(token: string, password: string): Promise<void> {
|
||||||
this.validatePassword(password);
|
this.validatePassword(password);
|
||||||
const user = await this.getUserForToken(token);
|
const user = await this.getUserForToken(token);
|
||||||
@ -297,6 +309,7 @@ class UserService {
|
|||||||
});
|
});
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
await this.changePassword(user.id, password);
|
await this.changePassword(user.id, password);
|
||||||
|
await this.sessionService.deleteSessionsForUser(user.id);
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidTokenError();
|
throw new InvalidTokenError();
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import FeatureTypeService from '../services/feature-type-service';
|
|||||||
import EventService from '../services/event-service';
|
import EventService from '../services/event-service';
|
||||||
import HealthService from '../services/health-service';
|
import HealthService from '../services/health-service';
|
||||||
import SettingService from '../services/setting-service';
|
import SettingService from '../services/setting-service';
|
||||||
|
import SessionService from '../services/session-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@ -31,6 +32,7 @@ export interface IUnleashServices {
|
|||||||
healthService: HealthService;
|
healthService: HealthService;
|
||||||
projectService: ProjectService;
|
projectService: ProjectService;
|
||||||
resetTokenService: ResetTokenService;
|
resetTokenService: ResetTokenService;
|
||||||
|
sessionService: SessionService;
|
||||||
settingService: SettingService;
|
settingService: SettingService;
|
||||||
stateService: StateService;
|
stateService: StateService;
|
||||||
strategyService: StrategyService;
|
strategyService: StrategyService;
|
||||||
|
@ -16,6 +16,7 @@ import AddonStore from '../db/addon-store';
|
|||||||
import { AccessStore } from '../db/access-store';
|
import { AccessStore } from '../db/access-store';
|
||||||
import { ApiTokenStore } from '../db/api-token-store';
|
import { ApiTokenStore } from '../db/api-token-store';
|
||||||
import { ResetTokenStore } from '../db/reset-token-store';
|
import { ResetTokenStore } from '../db/reset-token-store';
|
||||||
|
import SessionStore from '../db/session-store';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
projectStore: ProjectStore;
|
projectStore: ProjectStore;
|
||||||
@ -28,6 +29,7 @@ export interface IUnleashStores {
|
|||||||
featureToggleStore: FeatureToggleStore;
|
featureToggleStore: FeatureToggleStore;
|
||||||
contextFieldStore: ContextFieldStore;
|
contextFieldStore: ContextFieldStore;
|
||||||
settingStore: SettingStore;
|
settingStore: SettingStore;
|
||||||
|
sessionStore: SessionStore;
|
||||||
userStore: UserStore;
|
userStore: UserStore;
|
||||||
tagStore: TagStore;
|
tagStore: TagStore;
|
||||||
tagTypeStore: TagTypeStore;
|
tagTypeStore: TagTypeStore;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
|
import sinon from 'sinon';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
import EventEmitter from 'events';
|
||||||
import dbInit from '../../helpers/database-init';
|
import dbInit from '../../helpers/database-init';
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
|
||||||
@ -9,11 +11,13 @@ import {
|
|||||||
} from '../../../../lib/services/access-service';
|
} from '../../../../lib/services/access-service';
|
||||||
import ResetTokenService from '../../../../lib/services/reset-token-service';
|
import ResetTokenService from '../../../../lib/services/reset-token-service';
|
||||||
import UserService from '../../../../lib/services/user-service';
|
import UserService from '../../../../lib/services/user-service';
|
||||||
import { setupApp } from '../../helpers/test-helper';
|
import { setupApp, setupAppWithAuth } from '../../helpers/test-helper';
|
||||||
import { EmailService } from '../../../../lib/services/email-service';
|
import { EmailService } from '../../../../lib/services/email-service';
|
||||||
import User from '../../../../lib/types/user';
|
import User from '../../../../lib/types/user';
|
||||||
import { IUnleashConfig } from '../../../../lib/types/option';
|
import { IUnleashConfig } from '../../../../lib/types/option';
|
||||||
import { createTestConfig } from '../../../config/test-config';
|
import { createTestConfig } from '../../../config/test-config';
|
||||||
|
import SessionStore from '../../../../lib/db/session-store';
|
||||||
|
import SessionService from '../../../../lib/services/session-service';
|
||||||
|
|
||||||
let stores;
|
let stores;
|
||||||
let db;
|
let db;
|
||||||
@ -46,11 +50,17 @@ test.before(async () => {
|
|||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
accessService = new AccessService(stores, config);
|
accessService = new AccessService(stores, config);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const sessionStore = new SessionStore(
|
||||||
|
db,
|
||||||
|
new EventEmitter(),
|
||||||
|
config.getLogger,
|
||||||
|
);
|
||||||
|
const sessionService = new SessionService({ sessionStore }, config);
|
||||||
userService = new UserService(stores, config, {
|
userService = new UserService(stores, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
resetTokenService = new ResetTokenService(stores, config);
|
resetTokenService = new ResetTokenService(stores, config);
|
||||||
const adminRole = await accessService.getRootRole(RoleName.ADMIN);
|
const adminRole = await accessService.getRootRole(RoleName.ADMIN);
|
||||||
@ -99,7 +109,7 @@ test.serial('Can use token to reset password', async t => {
|
|||||||
);
|
);
|
||||||
const relative = getBackendResetUrl(url);
|
const relative = getBackendResetUrl(url);
|
||||||
// Can't login before reset
|
// Can't login before reset
|
||||||
t.throwsAsync<Error>(
|
await t.throwsAsync<Error>(
|
||||||
async () => userService.loginUser(user.email, password),
|
async () => userService.loginUser(user.email, password),
|
||||||
{
|
{
|
||||||
instanceOf: Error,
|
instanceOf: Error,
|
||||||
@ -171,6 +181,69 @@ test.serial('Invalid token should yield 401', async t => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
'Calling validate endpoint with already existing session should destroy session',
|
||||||
|
async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const request = await setupAppWithAuth(stores);
|
||||||
|
await request
|
||||||
|
.post('/api/admin/login')
|
||||||
|
.send({
|
||||||
|
email: 'user@mail.com',
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
await request.get('/api/admin/features').expect(200);
|
||||||
|
const url = await resetTokenService.createResetPasswordUrl(
|
||||||
|
user.id,
|
||||||
|
adminUser.username,
|
||||||
|
);
|
||||||
|
const relative = getBackendResetUrl(url);
|
||||||
|
|
||||||
|
await request
|
||||||
|
.get(relative)
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/);
|
||||||
|
await request.get('/api/admin/features').expect(401); // we no longer should have a valid session
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
'Calling reset endpoint with already existing session should logout/destroy existing session',
|
||||||
|
async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const request = await setupAppWithAuth(stores);
|
||||||
|
const url = await resetTokenService.createResetPasswordUrl(
|
||||||
|
user.id,
|
||||||
|
adminUser.username,
|
||||||
|
);
|
||||||
|
const relative = getBackendResetUrl(url);
|
||||||
|
let token;
|
||||||
|
await request
|
||||||
|
.get(relative)
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(res => {
|
||||||
|
token = res.body.token;
|
||||||
|
});
|
||||||
|
await request
|
||||||
|
.post('/api/admin/login')
|
||||||
|
.send({
|
||||||
|
email: 'user@mail.com',
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
await request.get('/api/admin/features').expect(200); // If we login we can access features endpoint
|
||||||
|
await request
|
||||||
|
.post('/auth/reset/password')
|
||||||
|
.send({
|
||||||
|
email: user.email,
|
||||||
|
token,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
await request.get('/api/admin/features').expect(401); // we no longer have a valid session after using the reset password endpoint
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test.serial(
|
test.serial(
|
||||||
'Trying to change password with an invalid token should yield 401',
|
'Trying to change password with an invalid token should yield 401',
|
||||||
async t => {
|
async t => {
|
||||||
|
@ -9,6 +9,7 @@ import { EmailService } from '../../../lib/services/email-service';
|
|||||||
import User from '../../../lib/types/user';
|
import User from '../../../lib/types/user';
|
||||||
import { IUnleashConfig } from '../../../lib/types/option';
|
import { IUnleashConfig } from '../../../lib/types/option';
|
||||||
import { createTestConfig } from '../../config/test-config';
|
import { createTestConfig } from '../../config/test-config';
|
||||||
|
import SessionService from '../../../lib/services/session-service';
|
||||||
|
|
||||||
const config: IUnleashConfig = createTestConfig();
|
const config: IUnleashConfig = createTestConfig();
|
||||||
|
|
||||||
@ -20,18 +21,20 @@ let userIdToCreateResetFor: number;
|
|||||||
let accessService: AccessService;
|
let accessService: AccessService;
|
||||||
let userService: UserService;
|
let userService: UserService;
|
||||||
let resetTokenService: ResetTokenService;
|
let resetTokenService: ResetTokenService;
|
||||||
|
let sessionService: SessionService;
|
||||||
test.before(async () => {
|
test.before(async () => {
|
||||||
db = await dbInit('reset_token_service_serial', getLogger);
|
db = await dbInit('reset_token_service_serial', getLogger);
|
||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
accessService = new AccessService(stores, config);
|
accessService = new AccessService(stores, config);
|
||||||
resetTokenService = new ResetTokenService(stores, config);
|
resetTokenService = new ResetTokenService(stores, config);
|
||||||
|
sessionService = new SessionService(stores, config);
|
||||||
const emailService = new EmailService(undefined, config.getLogger);
|
const emailService = new EmailService(undefined, config.getLogger);
|
||||||
|
|
||||||
userService = new UserService(stores, config, {
|
userService = new UserService(stores, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
adminUser = await userService.createUser({
|
adminUser = await userService.createUser({
|
||||||
|
113
src/test/e2e/services/session-service.e2e.test.ts
Normal file
113
src/test/e2e/services/session-service.e2e.test.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import test, { after, before, beforeEach } from 'ava';
|
||||||
|
import noLoggerProvider from '../../fixtures/no-logger';
|
||||||
|
import dbInit from '../helpers/database-init';
|
||||||
|
import SessionService from '../../../lib/services/session-service';
|
||||||
|
import NotFoundError from '../../../lib/error/notfound-error';
|
||||||
|
|
||||||
|
let stores;
|
||||||
|
let db;
|
||||||
|
let sessionService;
|
||||||
|
const newSession = {
|
||||||
|
sid: 'abc123',
|
||||||
|
sess: {
|
||||||
|
cookie: {
|
||||||
|
originalMaxAge: 2880000,
|
||||||
|
expires: new Date(Date.now() + 86_400_000).toDateString(),
|
||||||
|
secure: false,
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
imageUrl:
|
||||||
|
'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro',
|
||||||
|
seenAt: '2021-04-26T10:59:18.782Z',
|
||||||
|
loginAttempts: 0,
|
||||||
|
createdAt: '2021-04-22T05:12:54.368Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const otherSession = {
|
||||||
|
sid: 'xyz321',
|
||||||
|
sess: {
|
||||||
|
cookie: {
|
||||||
|
originalMaxAge: 2880000,
|
||||||
|
expires: new Date(Date.now() + 86400000).toDateString(),
|
||||||
|
secure: false,
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
id: 2,
|
||||||
|
username: 'editor',
|
||||||
|
imageUrl:
|
||||||
|
'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro',
|
||||||
|
seenAt: '2021-04-26T10:59:18.782Z',
|
||||||
|
loginAttempts: 0,
|
||||||
|
createdAt: '2021-04-22T05:12:54.368Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
before(async () => {
|
||||||
|
db = await dbInit('session_service_serial', noLoggerProvider);
|
||||||
|
stores = db.stores;
|
||||||
|
sessionService = new SessionService(stores, {
|
||||||
|
getLogger: noLoggerProvider,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.stores.sessionStore.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
after.always(async () => {
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should list active sessions', async t => {
|
||||||
|
await sessionService.insertSession(newSession);
|
||||||
|
await sessionService.insertSession(otherSession);
|
||||||
|
|
||||||
|
const sessions = await sessionService.getActiveSessions();
|
||||||
|
t.is(sessions.length, 2);
|
||||||
|
t.deepEqual(sessions[0].sess, otherSession.sess); // Ordered newest first
|
||||||
|
t.deepEqual(sessions[1].sess, newSession.sess);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('Should list active sessions for user', async t => {
|
||||||
|
await sessionService.insertSession(newSession);
|
||||||
|
await sessionService.insertSession(otherSession);
|
||||||
|
|
||||||
|
const sessions = await sessionService.getSessionsForUser(2); // editor session
|
||||||
|
t.is(sessions.length, 1);
|
||||||
|
t.deepEqual(sessions[0].sess, otherSession.sess);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('Can delete sessions by user', async t => {
|
||||||
|
await sessionService.insertSession(newSession);
|
||||||
|
await sessionService.insertSession(otherSession);
|
||||||
|
|
||||||
|
const sessions = await sessionService.getActiveSessions();
|
||||||
|
t.is(sessions.length, 2);
|
||||||
|
await sessionService.deleteSessionsForUser(2);
|
||||||
|
await t.throwsAsync(
|
||||||
|
async () => {
|
||||||
|
await sessionService.getSessionsForUser(2);
|
||||||
|
},
|
||||||
|
{ instanceOf: NotFoundError },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('Can delete session by sid', async t => {
|
||||||
|
await sessionService.insertSession(newSession);
|
||||||
|
await sessionService.insertSession(otherSession);
|
||||||
|
|
||||||
|
const sessions = await sessionService.getActiveSessions();
|
||||||
|
t.is(sessions.length, 2);
|
||||||
|
|
||||||
|
await sessionService.deleteSession('abc123');
|
||||||
|
await t.throwsAsync(async () => sessionService.getSession('abc123'), {
|
||||||
|
instanceOf: NotFoundError,
|
||||||
|
});
|
||||||
|
});
|
@ -9,6 +9,7 @@ import { IRole } from '../../../lib/db/access-store';
|
|||||||
import ResetTokenService from '../../../lib/services/reset-token-service';
|
import ResetTokenService from '../../../lib/services/reset-token-service';
|
||||||
import { EmailService } from '../../../lib/services/email-service';
|
import { EmailService } from '../../../lib/services/email-service';
|
||||||
import { createTestConfig } from '../../config/test-config';
|
import { createTestConfig } from '../../config/test-config';
|
||||||
|
import SessionService from '../../../lib/services/session-service';
|
||||||
|
|
||||||
let db;
|
let db;
|
||||||
let stores;
|
let stores;
|
||||||
@ -23,11 +24,13 @@ test.before(async () => {
|
|||||||
const accessService = new AccessService(stores, config);
|
const accessService = new AccessService(stores, config);
|
||||||
const resetTokenService = new ResetTokenService(stores, config);
|
const resetTokenService = new ResetTokenService(stores, config);
|
||||||
const emailService = new EmailService(undefined, config.getLogger);
|
const emailService = new EmailService(undefined, config.getLogger);
|
||||||
|
const sessionService = new SessionService(stores, config);
|
||||||
|
|
||||||
userService = new UserService(stores, config, {
|
userService = new UserService(stores, config, {
|
||||||
accessService,
|
accessService,
|
||||||
resetTokenService,
|
resetTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
sessionService,
|
||||||
});
|
});
|
||||||
userStore = stores.userStore;
|
userStore = stores.userStore;
|
||||||
const rootRoles = await accessService.getRootRoles();
|
const rootRoles = await accessService.getRootRoles();
|
||||||
|
24
src/test/fixtures/fake-session-store.ts
vendored
Normal file
24
src/test/fixtures/fake-session-store.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import SessionStore, { ISession } from '../../lib/db/session-store';
|
||||||
|
import noLoggerProvider from './no-logger';
|
||||||
|
|
||||||
|
export default class FakeSessionStore extends SessionStore {
|
||||||
|
private sessions: ISession[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(undefined, undefined, noLoggerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveSessions(): Promise<ISession[]> {
|
||||||
|
return this.sessions.filter(session => session.expired != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSessionsForUser(userId: number): Promise<ISession[]> {
|
||||||
|
return this.sessions.filter(session => session.sess.user.id === userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSessionsForUser(userId: number): Promise<void> {
|
||||||
|
this.sessions = this.sessions.filter(
|
||||||
|
session => session.sess.user.id !== userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user