-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
Copy pathsessions.ts
271 lines (239 loc) · 7.29 KB
/
sessions.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
import type { CookieParseOptions, CookieSerializeOptions } from "cookie";
import type { Cookie, CookieOptions, CreateCookieFunction } from "./cookies";
import { isCookie } from "./cookies";
import { warnOnce } from "./warnings";
/**
* An object of name/value pairs to be used in the session.
*/
export interface SessionData {
[name: string]: any;
}
/**
* Session persists data across HTTP requests.
*
* @see https://remix.run/utils/sessions#session-api
*/
export interface Session {
/**
* A unique identifier for this session.
*
* Note: This will be the empty string for newly created sessions and
* sessions that are not backed by a database (i.e. cookie-based sessions).
*/
readonly id: string;
/**
* The raw data contained in this session.
*
* This is useful mostly for SessionStorage internally to access the raw
* session data to persist.
*/
readonly data: SessionData;
/**
* Returns `true` if the session has a value for the given `name`, `false`
* otherwise.
*/
has(name: string): boolean;
/**
* Returns the value for the given `name` in this session.
*/
get(name: string): any;
/**
* Sets a value in the session for the given `name`.
*/
set(name: string, value: any): void;
/**
* Sets a value in the session that is only valid until the next `get()`.
* This can be useful for temporary values, like error messages.
*/
flash(name: string, value: any): void;
/**
* Removes a value from the session.
*/
unset(name: string): void;
}
function flash(name: string): string {
return `__flash_${name}__`;
}
export type CreateSessionFunction = (
initialData?: SessionData,
id?: string
) => Session;
/**
* Creates a new Session object.
*
* Note: This function is typically not invoked directly by application code.
* Instead, use a `SessionStorage` object's `getSession` method.
*
* @see https://remix.run/utils/sessions#createsession
*/
export const createSession: CreateSessionFunction = (
initialData = {},
id = ""
) => {
let map = new Map<string, any>(Object.entries(initialData));
return {
get id() {
return id;
},
get data() {
return Object.fromEntries(map);
},
has(name) {
return map.has(name) || map.has(flash(name));
},
get(name) {
if (map.has(name)) return map.get(name);
let flashName = flash(name);
if (map.has(flashName)) {
let value = map.get(flashName);
map.delete(flashName);
return value;
}
return undefined;
},
set(name, value) {
map.set(name, value);
},
flash(name, value) {
map.set(flash(name), value);
},
unset(name) {
map.delete(name);
},
};
};
export type IsSessionFunction = (object: any) => object is Session;
/**
* Returns true if an object is a Remix session.
*
* @see https://remix.run/utils/sessions#issession
*/
export const isSession: IsSessionFunction = (object): object is Session => {
return (
object != null &&
typeof object.id === "string" &&
typeof object.data !== "undefined" &&
typeof object.has === "function" &&
typeof object.get === "function" &&
typeof object.set === "function" &&
typeof object.flash === "function" &&
typeof object.unset === "function"
);
};
/**
* SessionStorage stores session data between HTTP requests and knows how to
* parse and create cookies.
*
* A SessionStorage creates Session objects using a `Cookie` header as input.
* Then, later it generates the `Set-Cookie` header to be used in the response.
*/
export interface SessionStorage {
/**
* Parses a Cookie header from a HTTP request and returns the associated
* Session. If there is no session associated with the cookie, this will
* return a new Session with no data.
*/
getSession(
cookieHeader?: string | null,
options?: CookieParseOptions
): Promise<Session>;
/**
* Stores all data in the Session and returns the Set-Cookie header to be
* used in the HTTP response.
*/
commitSession(
session: Session,
options?: CookieSerializeOptions
): Promise<string>;
/**
* Deletes all data associated with the Session and returns the Set-Cookie
* header to be used in the HTTP response.
*/
destroySession(
session: Session,
options?: CookieSerializeOptions
): Promise<string>;
}
/**
* SessionIdStorageStrategy is designed to allow anyone to easily build their
* own SessionStorage using `createSessionStorage(strategy)`.
*
* This strategy describes a common scenario where the session id is stored in
* a cookie but the actual session data is stored elsewhere, usually in a
* database or on disk. A set of create, read, update, and delete operations
* are provided for managing the session data.
*/
export interface SessionIdStorageStrategy {
/**
* The Cookie used to store the session id, or options used to automatically
* create one.
*/
cookie?: Cookie | (CookieOptions & { name?: string });
/**
* Creates a new record with the given data and returns the session id.
*/
createData: (data: SessionData, expires?: Date) => Promise<string>;
/**
* Returns data for a given session id, or `null` if there isn't any.
*/
readData: (id: string) => Promise<SessionData | null>;
/**
* Updates data for the given session id.
*/
updateData: (id: string, data: SessionData, expires?: Date) => Promise<void>;
/**
* Deletes data for a given session id from the data store.
*/
deleteData: (id: string) => Promise<void>;
}
export type CreateSessionStorageFunction = (
strategy: SessionIdStorageStrategy
) => SessionStorage;
/**
* Creates a SessionStorage object using a SessionIdStorageStrategy.
*
* Note: This is a low-level API that should only be used if none of the
* existing session storage options meet your requirements.
*
* @see https://remix.run/utils/sessions#createsessionstorage
*/
export const createSessionStorageFactory =
(createCookie: CreateCookieFunction): CreateSessionStorageFunction =>
({ cookie: cookieArg, createData, readData, updateData, deleteData }) => {
let cookie = isCookie(cookieArg)
? cookieArg
: createCookie(cookieArg?.name || "__session", cookieArg);
warnOnceAboutSigningSessionCookie(cookie);
return {
async getSession(cookieHeader, options) {
let id = cookieHeader && (await cookie.parse(cookieHeader, options));
let data = id && (await readData(id));
return createSession(data || {}, id || "");
},
async commitSession(session, options) {
let { id, data } = session;
if (id) {
await updateData(id, data, cookie.expires);
} else {
id = await createData(data, cookie.expires);
}
return cookie.serialize(id, options);
},
async destroySession(session, options) {
await deleteData(session.id);
return cookie.serialize("", {
...options,
expires: new Date(0),
});
},
};
};
export function warnOnceAboutSigningSessionCookie(cookie: Cookie) {
warnOnce(
cookie.isSigned,
`The "${cookie.name}" cookie is not signed, but session cookies should be ` +
`signed to prevent tampering on the client before they are sent back to the ` +
`server. See https://remix.run/utils/cookies#signing-cookies ` +
`for more information.`
);
}