1 | import * as IORedis from 'ioredis';
|
2 | import * as shortid from 'shortid';
|
3 | import { merge } from 'lodash';
|
4 | import { Session, DatabaseInterfaceSessions, ConnectionInformations } from '@accounts/types';
|
5 | import { AccountsRedisOptions } from './types';
|
6 |
|
7 | const defaultOptions = {
|
8 | userCollectionName: 'users',
|
9 | sessionCollectionName: 'sessions',
|
10 | timestamps: {
|
11 | createdAt: 'createdAt',
|
12 | updatedAt: 'updatedAt',
|
13 | },
|
14 | idProvider: () => shortid.generate(),
|
15 | dateProvider: (date?: Date) => (date ? date.getTime() : Date.now()),
|
16 | };
|
17 |
|
18 | export class RedisSessions implements DatabaseInterfaceSessions {
|
19 | private options: AccountsRedisOptions & typeof defaultOptions;
|
20 | private db: IORedis.Redis;
|
21 |
|
22 | constructor(db: IORedis.Redis, options?: AccountsRedisOptions) {
|
23 | this.options = merge({ ...defaultOptions }, options);
|
24 | if (!db) {
|
25 | throw new Error('A database connection is required');
|
26 | }
|
27 | this.db = db;
|
28 | }
|
29 |
|
30 | public async createSession(
|
31 | userId: string,
|
32 | token: string,
|
33 | connection: ConnectionInformations = {},
|
34 | extraData?: object
|
35 | ): Promise<string> {
|
36 | const sessionId = this.options.idProvider();
|
37 | const pipeline = this.db.pipeline();
|
38 | pipeline.hmset(`${this.options.sessionCollectionName}:${sessionId}`, {
|
39 | userId,
|
40 | token,
|
41 | userAgent: connection.userAgent,
|
42 | ip: connection.ip,
|
43 | valid: true,
|
44 | [this.options.timestamps.createdAt]: this.options.dateProvider(),
|
45 | [this.options.timestamps.updatedAt]: this.options.dateProvider(),
|
46 | });
|
47 |
|
48 | pipeline.sadd(
|
49 | `${this.options.sessionCollectionName}:${this.options.userCollectionName}:${userId}`,
|
50 | sessionId
|
51 | );
|
52 |
|
53 | pipeline.set(`${this.options.sessionCollectionName}:token:${token}`, sessionId);
|
54 | await pipeline.exec();
|
55 | return sessionId;
|
56 | }
|
57 |
|
58 | public async updateSession(sessionId: string, connection: ConnectionInformations): Promise<void> {
|
59 | if (await this.db.exists(`${this.options.sessionCollectionName}:${sessionId}`)) {
|
60 | await this.db.hmset(`${this.options.sessionCollectionName}:${sessionId}`, {
|
61 | userAgent: connection.userAgent,
|
62 | ip: connection.ip,
|
63 | [this.options.timestamps.updatedAt]: this.options.dateProvider(),
|
64 | });
|
65 | }
|
66 | }
|
67 |
|
68 | public async invalidateSession(sessionId: string): Promise<void> {
|
69 | if (await this.db.exists(`${this.options.sessionCollectionName}:${sessionId}`)) {
|
70 | await this.db.hmset(`${this.options.sessionCollectionName}:${sessionId}`, {
|
71 | valid: false,
|
72 | [this.options.timestamps.updatedAt]: this.options.dateProvider(),
|
73 | });
|
74 | }
|
75 | }
|
76 |
|
77 | public async invalidateAllSessions(userId: string): Promise<void> {
|
78 | if (
|
79 | await this.db.exists(
|
80 | `${this.options.sessionCollectionName}:${this.options.userCollectionName}:${userId}`
|
81 | )
|
82 | ) {
|
83 | const sessionIds: string[] = await this.db.smembers(
|
84 | `${this.options.sessionCollectionName}:${this.options.userCollectionName}:${userId}`
|
85 | );
|
86 | await sessionIds.map(sessionId => this.invalidateSession(sessionId));
|
87 | }
|
88 | }
|
89 |
|
90 | public async findSessionByToken(token: string): Promise<Session | null> {
|
91 | if (await this.db.exists(`${this.options.sessionCollectionName}:token:${token}`)) {
|
92 | const sessionId = await this.db.get(`${this.options.sessionCollectionName}:token:${token}`);
|
93 | if (sessionId) {
|
94 | return this.findSessionById(sessionId);
|
95 | }
|
96 | }
|
97 | return null;
|
98 | }
|
99 |
|
100 | public async findSessionById(sessionId: string): Promise<Session | null> {
|
101 | if (await this.db.exists(`${this.options.sessionCollectionName}:${sessionId}`)) {
|
102 | const session = await this.db.hgetall(`${this.options.sessionCollectionName}:${sessionId}`);
|
103 | return this.formatSession(sessionId, session);
|
104 | }
|
105 | return null;
|
106 | }
|
107 |
|
108 | |
109 |
|
110 |
|
111 | private formatSession(sessionId: string, session: any): Session {
|
112 |
|
113 | session.valid = session.valid === 'true';
|
114 | return { id: sessionId, ...session };
|
115 | }
|
116 | }
|