/
Session.ts
156 lines (127 loc) · 5.09 KB
/
Session.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { nanoid } from 'https://deno.land/x/nanoid@v3.0.0/async.ts'
import MemoryStore from './stores/MemoryStore.ts'
import CookieStore from './stores/CookieStore.ts'
import type { Context } from '../deps.ts'
import type Store from './stores/Store.ts'
import type { CookiesGetOptions, CookiesSetDeleteOptions } from '../deps.ts'
interface SessionOptions {
expireAfterSeconds?: number | null
cookieGetOptions?: CookiesGetOptions
cookieSetOptions?: CookiesSetDeleteOptions
}
export interface SessionData {
_flash: Record<string, unknown>
_accessed: string | null
_expire: string | null
_delete: boolean
[key: string]: unknown
}
export default class Session {
sid: string
// user should interact with data using `get(), set(), flash(), has()`
private data: SessionData
private ctx: Context
// construct a Session with no data and id
// private: force user to create session in initMiddleware()
private constructor (sid : string, data : SessionData, ctx : Context) {
this.sid = sid
this.data = data
this.ctx = ctx
}
static initMiddleware(store: Store | CookieStore = new MemoryStore(), {
expireAfterSeconds = null,
cookieGetOptions = {},
cookieSetOptions = {}
}: SessionOptions = {}) {
return async (ctx : Context, next : () => Promise<unknown>) => {
// get sessionId from cookie
const sid = await ctx.cookies.get('session', cookieGetOptions)
let session: Session;
if (sid) {
// load session data from store
const sessionData = store instanceof CookieStore ? await store.getSessionByCtx(ctx) : await store.getSessionById(sid)
if (sessionData) {
// load success, check if it's valid (not expired)
if (this.sessionValid(sessionData)) {
session = new Session(sid, sessionData, ctx);
await session.reupSession(store, expireAfterSeconds);
} else {
// invalid session
store instanceof CookieStore ? store.deleteSession(ctx) : await store.deleteSession(sid)
session = await this.createSession(ctx, store, expireAfterSeconds)
}
} else {
session = await this.createSession(ctx, store, expireAfterSeconds)
}
} else {
session = await this.createSession(ctx, store, expireAfterSeconds)
}
// store session to ctx.state so user can interact (set, get) with it
ctx.state.session = session;
// update _access time
session.set('_accessed', new Date().toISOString())
await ctx.cookies.set('session', session.sid, cookieSetOptions)
await next()
// request done, push session data to store
await session.persistSessionData(store)
if (session.data._delete) {
store instanceof CookieStore ? store.deleteSession(ctx) : await store.deleteSession(session.sid)
}
}
}
// should only be called in `initMiddleware()` when validating session data
private static sessionValid(sessionData: SessionData) {
return sessionData._expire == null || Date.now() < new Date(sessionData._expire).getTime();
}
// should only be called in `initMiddleware()`
private async reupSession(store : Store | CookieStore, expiration : number | null | undefined) {
// expiration in seconds
this.data._expire = expiration ? new Date(Date.now() + expiration * 1000).toISOString() : null
await this.persistSessionData(store)
}
// should only be called in `initMiddleware()` when creating a new session
private static async createSession(ctx : Context, store : Store | CookieStore, expiration : number | null | undefined) : Promise<Session> {
const sessionData = {
'_flash': {},
'_accessed': new Date().toISOString(),
'_expire': expiration ? new Date(Date.now() + expiration * 1000).toISOString() : null,
'_delete': false
}
const newID = await nanoid(21)
store instanceof CookieStore ? await store.createSession(ctx, sessionData) : await store.createSession(newID, sessionData)
return new Session(newID, sessionData, ctx)
}
// set _delete to true, will be deleted in middleware
// should be called by user using `ctx.state.session.deleteSession()`
async deleteSession() : Promise<void> {
this.data._delete = true
}
// push current session data to Session.store
// ctx is needed for CookieStore
private persistSessionData(store : Store | CookieStore): Promise<void> | void {
return store instanceof CookieStore ? store.persistSessionData(this.ctx, this.data) : store.persistSessionData(this.sid, this.data)
}
// Methods exposed for users to manipulate session data
get(key : string) {
if (key in this.data) {
return this.data[key]
} else {
const value = this.data['_flash'][key]
delete this.data['_flash'][key]
return value
}
}
set(key : string, value : unknown) {
if(value === null || value === undefined) {
delete this.data[key]
} else {
this.data[key] = value
}
}
flash(key : string, value : unknown) {
this.data['_flash'][key] = value
}
has(key : string) {
return key in this.data || key in this.data['_flash'];
}
}