@tiddlywiki/mws 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env-cmdrc +5 -0
- package/README.md +3 -1
- package/dist/server.d.ts +923 -0
- package/dist/server.js +1763 -1415
- package/dist/server.js.map +1 -1
- package/editions/mws-docs/tiddlers/$__StoryList.tid +3 -3
- package/editions/mws-docs/tiddlers/MWS and SQLite.md +11 -0
- package/editions/mws-docs/tiddlers/Request Server.md +20 -0
- package/editions/mws-docs/tiddlers/Request Server.md.meta +5 -0
- package/package.json +17 -10
- package/prisma/schema.prisma +1 -0
- package/react-user-mgmt/package-lock.json +76 -321
- package/react-user-mgmt/package.json +2 -5
- package/react-user-mgmt/public/chunk-YALWN5OT.js +2 -0
- package/react-user-mgmt/public/chunk-YALWN5OT.js.map +7 -0
- package/react-user-mgmt/public/esm-QKDFEP3B.js +3 -0
- package/react-user-mgmt/public/esm-QKDFEP3B.js.map +7 -0
- package/react-user-mgmt/public/index.html +1 -26
- package/react-user-mgmt/public/main.js +68 -109
- package/react-user-mgmt/public/main.js.map +4 -4
- package/react-user-mgmt/public/stats.css +597 -0
- package/react-user-mgmt/public/stats.html +59 -0
- package/react-user-mgmt/public/stats.js +2013 -0
- package/react-user-mgmt/public/stats.json +1 -0
- package/react-user-mgmt/public/stats.svg +4 -0
- package/react-user-mgmt/tsconfig.json +3 -0
- package/dist/server.cjs +0 -16677
- package/dist/server.cjs.map +0 -1
- package/src/StateObject.ts +0 -316
- package/src/commander.ts +0 -417
- package/src/commands/divider.ts +0 -20
- package/src/commands/index.ts +0 -24
- package/src/commands/mws-init-store.ts +0 -77
- package/src/commands/mws-listen.ts +0 -136
- package/src/commands/mws-load-archive.ts +0 -281
- package/src/commands/mws-load-plugin-bags.ts +0 -151
- package/src/commands/mws-load-tiddlers.ts +0 -37
- package/src/commands/mws-load-wiki-folder.ts +0 -124
- package/src/commands/mws-render-tiddlywiki5.ts +0 -55
- package/src/commands/mws-save-archive.ts +0 -111
- package/src/commands/mws-save-tiddler-text.ts +0 -33
- package/src/commands/tests-complete.ts +0 -21
- package/src/global.ts +0 -247
- package/src/routes/TiddlerStore.ts +0 -722
- package/src/routes/bag-file-server.ts +0 -340
- package/src/routes/cache.ts +0 -25
- package/src/routes/index.ts +0 -138
- package/src/routes/managers/index.ts +0 -17
- package/src/routes/managers/manager-recipes.ts +0 -438
- package/src/routes/managers/manager-users.ts +0 -201
- package/src/routes/managers/router-tiddlers.ts +0 -313
- package/src/routes/router.ts +0 -429
- package/src/routes/services/PasswordService.ts +0 -178
- package/src/routes/services/attachments.ts +0 -259
- package/src/routes/services/sessions.ts +0 -166
- package/src/routes/tw-test.ts +0 -65
- package/src/server.ts +0 -151
- package/src/setupDevServer.ts +0 -71
- package/src/streamer.ts +0 -430
- package/src/tiddlywiki.ts +0 -94
- package/src/utils/data-checks.ts +0 -172
- package/src/utils/index.ts +0 -4
- package/src/utils/rootRoute.ts +0 -237
- package/src/utils/utils.ts +0 -403
- package/src/utils/zodAssert.ts +0 -269
- /package/prisma/{migrations/notes.md → notes.md} +0 -0
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { tryParseJSON } from "../../utils";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import * as fs from "fs";
|
|
4
|
-
import { SiteConfig } from "../router";
|
|
5
|
-
import sjcl from "sjcl";
|
|
6
|
-
export interface TiddlerFields extends Record<string, any> {
|
|
7
|
-
title: PrismaField<"Tiddlers", "title">;
|
|
8
|
-
}
|
|
9
|
-
const $tw: any = {};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/*
|
|
13
|
-
|
|
14
|
-
Class to handle the attachments in the filing system
|
|
15
|
-
|
|
16
|
-
The store folder looks like this:
|
|
17
|
-
|
|
18
|
-
store/
|
|
19
|
-
inbox/ - files that are in the process of being uploaded via a multipart form upload
|
|
20
|
-
202402282125432742/
|
|
21
|
-
0
|
|
22
|
-
1
|
|
23
|
-
...
|
|
24
|
-
...
|
|
25
|
-
files/ - files that are the text content of large tiddlers
|
|
26
|
-
b7def178-79c4-4d88-b7a4-39763014a58b/
|
|
27
|
-
data.jpg - the extension is provided for convenience when directly inspecting the file system
|
|
28
|
-
meta.json - contains:
|
|
29
|
-
{
|
|
30
|
-
"filename": "data.jpg",
|
|
31
|
-
"type": "video/mp4",
|
|
32
|
-
"uploaded": "2024021821224823"
|
|
33
|
-
}
|
|
34
|
-
database.sql - The database file (managed by sql-tiddler-database.js)
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
export class AttachmentService {
|
|
39
|
-
|
|
40
|
-
constructor(
|
|
41
|
-
protected config: SiteConfig,
|
|
42
|
-
protected prisma: PrismaTxnClient,
|
|
43
|
-
) {
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
makeCanonicalUri(bag_name: string, title: string) {
|
|
48
|
-
return `/bags/${encodeURIComponentExtended(bag_name)}/tiddlers/${encodeURIComponentExtended(title)}/blob`;
|
|
49
|
-
}
|
|
50
|
-
/*
|
|
51
|
-
Given tiddler fields, tiddler_id and a bag_name, return the tiddler fields after the following process:
|
|
52
|
-
- Apply the tiddler_id as the revision field
|
|
53
|
-
- Apply the bag_name as the bag field
|
|
54
|
-
*/
|
|
55
|
-
processOutgoingTiddler({ tiddler, tiddler_id, bag_name, attachment_hash }: {
|
|
56
|
-
tiddler: TiddlerFields;
|
|
57
|
-
tiddler_id: any;
|
|
58
|
-
bag_name: PrismaField<"Bags", "bag_name">;
|
|
59
|
-
attachment_hash: PrismaField<"Tiddlers", "attachment_hash">;
|
|
60
|
-
}) {
|
|
61
|
-
if (attachment_hash !== null) {
|
|
62
|
-
return Object.assign(
|
|
63
|
-
{},
|
|
64
|
-
tiddler,
|
|
65
|
-
{
|
|
66
|
-
text: undefined,
|
|
67
|
-
_canonical_uri: this.makeCanonicalUri(bag_name, tiddler.title),
|
|
68
|
-
revision: tiddler_id,
|
|
69
|
-
bag: bag_name
|
|
70
|
-
}
|
|
71
|
-
);
|
|
72
|
-
} else {
|
|
73
|
-
return tiddler;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/*
|
|
77
|
-
*/
|
|
78
|
-
async processIncomingTiddler({
|
|
79
|
-
tiddlerFields, existing_attachment_hash, existing_canonical_uri
|
|
80
|
-
}: {
|
|
81
|
-
tiddlerFields: TiddlerFields;
|
|
82
|
-
existing_attachment_hash: PrismaField<"Tiddlers", "attachment_hash">;
|
|
83
|
-
existing_canonical_uri: any;
|
|
84
|
-
}) {
|
|
85
|
-
const { attachmentSizeLimit, attachmentsEnabled, } = this.config;
|
|
86
|
-
const contentTypeInfo = this.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"];
|
|
87
|
-
const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64";
|
|
88
|
-
|
|
89
|
-
let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit;
|
|
90
|
-
|
|
91
|
-
if (existing_attachment_hash) {
|
|
92
|
-
const fileSize = await this.getAttachmentFileSize(existing_attachment_hash);
|
|
93
|
-
if (fileSize && fileSize <= attachmentSizeLimit) {
|
|
94
|
-
const existingAttachmentMeta = await this.getAttachmentMetadata(existing_attachment_hash);
|
|
95
|
-
const hasCanonicalField = !!tiddlerFields._canonical_uri;
|
|
96
|
-
const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri));
|
|
97
|
-
shouldProcessAttachment = !skipAttachment;
|
|
98
|
-
} else {
|
|
99
|
-
shouldProcessAttachment = false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (attachmentsEnabled && isBinary && shouldProcessAttachment) {
|
|
104
|
-
const attachment_hash = existing_attachment_hash || await this.saveAttachment({
|
|
105
|
-
text: tiddlerFields.text,
|
|
106
|
-
type: tiddlerFields.type,
|
|
107
|
-
reference: tiddlerFields.title,
|
|
108
|
-
_canonical_uri: tiddlerFields._canonical_uri
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (tiddlerFields && tiddlerFields._canonical_uri) {
|
|
112
|
-
delete tiddlerFields._canonical_uri;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }),
|
|
117
|
-
attachment_hash: attachment_hash
|
|
118
|
-
};
|
|
119
|
-
} else {
|
|
120
|
-
return {
|
|
121
|
-
tiddlerFields: tiddlerFields,
|
|
122
|
-
attachment_hash: existing_attachment_hash
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/*
|
|
128
|
-
Check if an attachment name is valid (is a 64 character hex string)
|
|
129
|
-
*/
|
|
130
|
-
isValidAttachmentName(content_hash: string) {
|
|
131
|
-
const re = new RegExp('^[a-f0-9]{64}$');
|
|
132
|
-
return re.test(content_hash);
|
|
133
|
-
}
|
|
134
|
-
/*
|
|
135
|
-
Saves an attachment to a file. Options include:
|
|
136
|
-
|
|
137
|
-
text: text content (may be binary)
|
|
138
|
-
type: MIME type of content
|
|
139
|
-
reference: reference to use for debugging
|
|
140
|
-
_canonical_uri: canonical uri of the content
|
|
141
|
-
*/
|
|
142
|
-
async saveAttachment(options: {
|
|
143
|
-
text: string;
|
|
144
|
-
type: string;
|
|
145
|
-
reference: string;
|
|
146
|
-
_canonical_uri: string;
|
|
147
|
-
}) {
|
|
148
|
-
|
|
149
|
-
// Compute the content hash for naming the attachment
|
|
150
|
-
const contentHash = sjcl.codec.hex.fromBits($tw.sjcl.hash.sha256.hash(options.text)).slice(0, 64).toString();
|
|
151
|
-
// Choose the best file extension for the attachment given its type
|
|
152
|
-
const contentTypeInfo = this.config.contentTypeInfo[options.type] || this.config.contentTypeInfo["application/octet-stream"];
|
|
153
|
-
// Creat the attachment directory
|
|
154
|
-
const attachmentPath = path.resolve(this.config.storePath, "files", contentHash);
|
|
155
|
-
createDirectory(attachmentPath);
|
|
156
|
-
// Save the data file
|
|
157
|
-
const dataFilename = "data" + contentTypeInfo.extension;
|
|
158
|
-
fs.writeFileSync(path.resolve(attachmentPath, dataFilename), options.text, contentTypeInfo.encoding);
|
|
159
|
-
// Save the meta.json file
|
|
160
|
-
this.writeMetaFile(fs, path, attachmentPath, options, contentHash, dataFilename);
|
|
161
|
-
return contentHash as PrismaField<"Tiddlers", "attachment_hash">;
|
|
162
|
-
}
|
|
163
|
-
private writeMetaFile(fs: any, path: any, attachmentPath: any, options: { text: string; type: string; reference: string; _canonical_uri: string; }, contentHash: any, dataFilename: string) {
|
|
164
|
-
fs.writeFileSync(path.resolve(attachmentPath, "meta.json"), JSON.stringify({
|
|
165
|
-
_canonical_uri: options._canonical_uri,
|
|
166
|
-
created: new Date().toISOString(),
|
|
167
|
-
modified: new Date().toISOString(),
|
|
168
|
-
contentHash: contentHash,
|
|
169
|
-
filename: dataFilename,
|
|
170
|
-
type: options.type
|
|
171
|
-
}, null, 4));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/*
|
|
175
|
-
Adopts an attachment file into the store (e.g. move from a temp upload directory)
|
|
176
|
-
*/
|
|
177
|
-
async adoptAttachment({ incomingFilepath, type, hash, _canonical_uri }: {
|
|
178
|
-
incomingFilepath: any;
|
|
179
|
-
type: string | number;
|
|
180
|
-
hash: any;
|
|
181
|
-
_canonical_uri: any;
|
|
182
|
-
}) {
|
|
183
|
-
// Choose the best file extension for the attachment given its type
|
|
184
|
-
const contentTypeInfo = this.config.contentTypeInfo[type] || this.config.contentTypeInfo["application/octet-stream"];
|
|
185
|
-
// Creat the attachment directory
|
|
186
|
-
const attachmentPath = path.resolve(this.config.storePath, "files", hash);
|
|
187
|
-
createDirectory(attachmentPath);
|
|
188
|
-
// Rename the data file
|
|
189
|
-
const dataFilename = "data" + contentTypeInfo.extension, dataFilepath = path.resolve(attachmentPath, dataFilename);
|
|
190
|
-
fs.renameSync(incomingFilepath, dataFilepath);
|
|
191
|
-
// Save the meta.json file
|
|
192
|
-
fs.writeFileSync(path.resolve(attachmentPath, "meta.json"), JSON.stringify({
|
|
193
|
-
_canonical_uri: _canonical_uri,
|
|
194
|
-
created: new Date().toISOString(),
|
|
195
|
-
modified: new Date().toISOString(),
|
|
196
|
-
contentHash: hash,
|
|
197
|
-
filename: dataFilename,
|
|
198
|
-
type: type
|
|
199
|
-
}, null, 4));
|
|
200
|
-
return hash;
|
|
201
|
-
}
|
|
202
|
-
/*
|
|
203
|
-
Get an attachment ready to stream. Returns null if there is an error or:
|
|
204
|
-
stream: filestream of file
|
|
205
|
-
type: type of file
|
|
206
|
-
*/
|
|
207
|
-
async getAttachmentStream(content_hash: string) {
|
|
208
|
-
// Check the attachment name
|
|
209
|
-
if (!this.isValidAttachmentName(content_hash)) return null;
|
|
210
|
-
|
|
211
|
-
// Construct the path to the attachment directory
|
|
212
|
-
const attachmentPath = path.resolve(this.config.storePath, "files", content_hash);
|
|
213
|
-
|
|
214
|
-
// Read the meta.json file
|
|
215
|
-
const metaJsonPath = path.resolve(attachmentPath, "meta.json");
|
|
216
|
-
if (!fs.existsSync(metaJsonPath) || !fs.statSync(metaJsonPath).isFile()) return null;
|
|
217
|
-
|
|
218
|
-
const meta = tryParseJSON<any>(fs.readFileSync(metaJsonPath, "utf8"));
|
|
219
|
-
if (!meta) return null;
|
|
220
|
-
|
|
221
|
-
const dataFilepath = path.resolve(attachmentPath, meta.filename);
|
|
222
|
-
// Check if the data file exists
|
|
223
|
-
if (!fs.existsSync(dataFilepath) || !fs.statSync(dataFilepath).isFile()) return null
|
|
224
|
-
|
|
225
|
-
// Stream the file
|
|
226
|
-
return {
|
|
227
|
-
stream: fs.createReadStream(dataFilepath),
|
|
228
|
-
type: meta.type
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
/*
|
|
234
|
-
Get the size of an attachment file given the contentHash.
|
|
235
|
-
Returns the size in bytes, or null if the file doesn't exist.
|
|
236
|
-
*/
|
|
237
|
-
async getAttachmentFileSize(content_hash: PrismaField<"Tiddlers", "attachment_hash"> & {}) {
|
|
238
|
-
// Construct the path to the attachment directory
|
|
239
|
-
const attachmentPath = path.resolve(this.config.storePath, "files", content_hash);
|
|
240
|
-
// Read the meta.json file
|
|
241
|
-
const metaJsonPath = path.resolve(attachmentPath, "meta.json");
|
|
242
|
-
if (!(fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile())) return null
|
|
243
|
-
const meta = tryParseJSON<any>(fs.readFileSync(metaJsonPath, "utf8"));
|
|
244
|
-
if (!meta) return null;
|
|
245
|
-
const dataFilepath = path.resolve(attachmentPath, meta.filename);
|
|
246
|
-
// Check if the data file exists and return its size
|
|
247
|
-
if (!(fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile())) return null;
|
|
248
|
-
return fs.statSync(dataFilepath).size;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async getAttachmentMetadata(content_hash: PrismaField<"Tiddlers", "attachment_hash"> & {}) {
|
|
252
|
-
const attachmentPath = path.resolve(this.config.storePath, "files", content_hash);
|
|
253
|
-
const metaJsonPath = path.resolve(attachmentPath, "meta.json");
|
|
254
|
-
if (!fs.existsSync(metaJsonPath)) return null;
|
|
255
|
-
return JSON.parse(fs.readFileSync(metaJsonPath, "utf8"));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Streamer } from "../../streamer";
|
|
3
|
-
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { registerZodRoutes, Router, RouterKeyMap, RouterRouteMap, zodRoute, ZodState } from "../router";
|
|
5
|
-
import { StateObject } from "../../StateObject";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import { JsonValue, Z2 } from "../../utils";
|
|
8
|
-
|
|
9
|
-
export interface AuthUser {
|
|
10
|
-
/** User ID. 0 if the user is not logged in. */
|
|
11
|
-
user_id: PrismaField<"Users", "user_id">;
|
|
12
|
-
/** User role_ids. This may have length even if the user isn't logged in, to allow ACL for anon. */
|
|
13
|
-
role_ids: PrismaField<"Roles", "role_id">[];
|
|
14
|
-
/** Username passed to the client */
|
|
15
|
-
username: PrismaField<"Users", "username">;
|
|
16
|
-
/** A session_id isn't guarenteed. There may be a session even if the user isn't logged in, and may not be even if they are, depending on the the situation. */
|
|
17
|
-
sessionId: PrismaField<"Sessions", "session_id"> | undefined;
|
|
18
|
-
/** Is this user considered a site-admin. This is determined by the auth service, not MWS. */
|
|
19
|
-
isAdmin: boolean;
|
|
20
|
-
/** Is the user logged in? This also means that user_id should be 0. role_ids may still be specified. */
|
|
21
|
-
isLoggedIn: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const SessionKeyMap: RouterKeyMap<SessionManager, true> = {
|
|
25
|
-
login1: true,
|
|
26
|
-
login2: true,
|
|
27
|
-
logout: true,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
* @param path path starting with a forward slash
|
|
34
|
-
* @param zodRequest the zod for state.data
|
|
35
|
-
* @param inner the handler to call
|
|
36
|
-
* @returns the ZodRoute
|
|
37
|
-
*/
|
|
38
|
-
export function zodSession<T extends z.ZodTypeAny, R extends JsonValue>(
|
|
39
|
-
path: string,
|
|
40
|
-
zodRequest: (z: Z2<"JSON">) => T,
|
|
41
|
-
inner: (state: ZodState<"POST", "json", Record<string, z.ZodTypeAny>, T>, prisma: PrismaTxnClient) => Promise<R>
|
|
42
|
-
) {
|
|
43
|
-
return zodRoute(["POST"], path, z => ({}), "json", zodRequest, async state => {
|
|
44
|
-
return state.$transaction(async (prisma) => await inner(state, prisma));
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
export type SessionManagerMap = RouterRouteMap<SessionManager>;
|
|
50
|
-
|
|
51
|
-
export class SessionManager {
|
|
52
|
-
|
|
53
|
-
static async defineRoutes(root: rootRoute) {
|
|
54
|
-
registerZodRoutes(root, new SessionManager(), Object.keys(SessionKeyMap))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
static async parseIncomingRequest(streamer: Streamer, router: Router): Promise<AuthUser> {
|
|
58
|
-
|
|
59
|
-
const sessionId = streamer.cookies.session as PrismaField<"Sessions", "session_id"> | undefined;
|
|
60
|
-
const session = sessionId && await router.engine.sessions.findUnique({
|
|
61
|
-
where: { session_id: sessionId },
|
|
62
|
-
select: { user: { select: { user_id: true, username: true, roles: { select: { role_id: true } } } } }
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (sessionId && session) return {
|
|
66
|
-
user_id: session.user.user_id,
|
|
67
|
-
username: session.user.username,
|
|
68
|
-
isAdmin: session.user.roles.some(e => e.role_id === 1),
|
|
69
|
-
role_ids: session.user.roles.map(e => e.role_id),
|
|
70
|
-
sessionId,
|
|
71
|
-
isLoggedIn: true,
|
|
72
|
-
};
|
|
73
|
-
else return {
|
|
74
|
-
user_id: 0 as PrismaField<"Users", "user_id">,
|
|
75
|
-
username: "(anon)" as PrismaField<"Users", "username">,
|
|
76
|
-
isAdmin: false,
|
|
77
|
-
role_ids: [] as PrismaField<"Roles", "role_id">[],
|
|
78
|
-
sessionId: undefined as PrismaField<"Sessions", "session_id"> | undefined,
|
|
79
|
-
isLoggedIn: false,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
login1 = zodSession("/login/1", z => z.object({
|
|
84
|
-
username: z.prismaField("Users", "username", "string"),
|
|
85
|
-
startLoginRequest: z.string(),
|
|
86
|
-
}), async (state, prisma) => {
|
|
87
|
-
const { username, startLoginRequest } = state.data;
|
|
88
|
-
|
|
89
|
-
const user = await prisma.users.findUnique({
|
|
90
|
-
where: { username },
|
|
91
|
-
select: { user_id: true, password: true, }
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (!user) throw "User not found.";
|
|
95
|
-
|
|
96
|
-
const { user_id, password } = user;
|
|
97
|
-
|
|
98
|
-
const stater = state.PasswordService.LoginGenerator({
|
|
99
|
-
user_id,
|
|
100
|
-
startLoginRequest,
|
|
101
|
-
registrationRecord: password,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const loginResponse = await stater.next(0);
|
|
105
|
-
|
|
106
|
-
if (loginResponse.done) throw "Login failed.";
|
|
107
|
-
|
|
108
|
-
const loginSession = await state.PasswordService.startLoginSession(stater);
|
|
109
|
-
|
|
110
|
-
state.setCookie("loginsession", loginSession, { httpOnly: true, path: "/", secure: state.isSecure, sameSite: "Strict" });
|
|
111
|
-
|
|
112
|
-
return { loginResponse: loginResponse.value };
|
|
113
|
-
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
login2 = zodSession("/login/2", z => z.object({
|
|
117
|
-
finishLoginRequest: z.string()
|
|
118
|
-
}), async (state, prisma) => {
|
|
119
|
-
const { finishLoginRequest } = state.data;
|
|
120
|
-
|
|
121
|
-
if (!state.cookies.loginsession) throw "Login session not found.";
|
|
122
|
-
|
|
123
|
-
const stater = state.PasswordService.serverState.get(state.cookies.loginsession);
|
|
124
|
-
|
|
125
|
-
if (!stater) throw "Login session not found.";
|
|
126
|
-
|
|
127
|
-
const { done, value } = await stater.next(1, finishLoginRequest);
|
|
128
|
-
|
|
129
|
-
if (!value?.session?.sessionKey) throw "Login failed.";
|
|
130
|
-
|
|
131
|
-
const session_id = await this.createSession(prisma, value.user_id, value.session.sessionKey);
|
|
132
|
-
|
|
133
|
-
state.setCookie("session", session_id, { httpOnly: true, path: "/", secure: state.isSecure, sameSite: "Strict" });
|
|
134
|
-
|
|
135
|
-
return null;
|
|
136
|
-
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
logout = zodSession("/logout", z => z.undefined(), async (state, prisma) => {
|
|
140
|
-
|
|
141
|
-
if (state.user.isLoggedIn) {
|
|
142
|
-
await prisma.sessions.delete({ where: { session_id: state.user.sessionId } });
|
|
143
|
-
}
|
|
144
|
-
var cookies = state.headers.cookie ? state.headers.cookie.split(";") : [];
|
|
145
|
-
for (var i = 0; i < cookies.length; i++) {
|
|
146
|
-
var cookie = cookies[i]?.trim().split("=")[0];
|
|
147
|
-
if (!cookie) continue;
|
|
148
|
-
// state.setHeader("Set-Cookie", cookie + "=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict");
|
|
149
|
-
state.setCookie(cookie, "", { httpOnly: true, path: "/", expires: new Date(0), secure: state.isSecure, sameSite: "Strict" });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return null;
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
async createSession(prisma: PrismaTxnClient, user_id: PrismaField<"Users", "user_id">, session_key: string) {
|
|
156
|
-
const session_id = randomBytes(16).toString("base64url");
|
|
157
|
-
return await prisma.sessions.create({
|
|
158
|
-
data: {
|
|
159
|
-
user_id,
|
|
160
|
-
session_key,
|
|
161
|
-
session_id,
|
|
162
|
-
last_accessed: new Date(),
|
|
163
|
-
}
|
|
164
|
-
}).then(({ session_id }) => session_id);
|
|
165
|
-
}
|
|
166
|
-
}
|
package/src/routes/tw-test.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { Server } from "http";
|
|
2
|
-
import { rootRoute } from "./router";
|
|
3
|
-
import { ZodAssert as zodAssert } from "../utils";
|
|
4
|
-
import { Tiddler, TiddlyWiki, TW } from "tiddlywiki";
|
|
5
|
-
|
|
6
|
-
export function TWRoutes(parent: rootRoute) {
|
|
7
|
-
const wikiRouter = new TWRouter();
|
|
8
|
-
parent.defineRoute({
|
|
9
|
-
method: ["OPTIONS", "GET", "HEAD", "POST", "PUT"],
|
|
10
|
-
bodyFormat: "stream",
|
|
11
|
-
path: /^\/mws(\/|$)/,
|
|
12
|
-
pathParams: ["final_slash"]
|
|
13
|
-
}, async (state) => {
|
|
14
|
-
zodAssert.pathParams(state, z => ({ final_slash: z.string().optional() }));
|
|
15
|
-
if (!state.pathParams.final_slash) throw state.redirect(state.urlInfo.pathname + "/");
|
|
16
|
-
// literally don't do this
|
|
17
|
-
const { req, res } = (state as any).streamer;
|
|
18
|
-
// options not required
|
|
19
|
-
wikiRouter.twserver.requestHandler(req, res, {} as any);
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
class TWRouter {
|
|
24
|
-
$tw: TW;
|
|
25
|
-
twserver!: any;
|
|
26
|
-
|
|
27
|
-
constructor() {
|
|
28
|
-
this.$tw = TiddlyWiki();
|
|
29
|
-
// tiddlywiki [+<pluginname> | ++<pluginpath>] [<wikipath>] ...[--command ...args]
|
|
30
|
-
this.$tw.boot.argv = [
|
|
31
|
-
// "++plugins/client",
|
|
32
|
-
// "++plugins/server",
|
|
33
|
-
// "+tiddlywiki/filesystem",
|
|
34
|
-
// "+tiddlywiki/tiddlyweb",
|
|
35
|
-
// "./editions/mws-server",
|
|
36
|
-
// "--mws-load-plugin-bags",
|
|
37
|
-
// "--build", "load-mws-demo-data",
|
|
38
|
-
// "--mws-listen", "port=5001", "host=::"
|
|
39
|
-
];
|
|
40
|
-
this.$tw.boot.boot(() => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.$tw.preloadTiddler({
|
|
44
|
-
text: "$protocol$//$host$" + "/mws/",
|
|
45
|
-
title: "$:/config/tiddlyweb/host",
|
|
46
|
-
});
|
|
47
|
-
// Boot the TW5 app
|
|
48
|
-
this.$tw.boot.boot(() => {
|
|
49
|
-
const Server = this.$tw.modules.execute("$:/core/modules/server/server.js", "tw-test.ts").Server;
|
|
50
|
-
this.twserver = new Server({
|
|
51
|
-
wiki: this.$tw.wiki,
|
|
52
|
-
variables: {
|
|
53
|
-
// do not use a trailing slash
|
|
54
|
-
"path-prefix": "/mws",
|
|
55
|
-
"root-tiddler": "$:/core/save/all",
|
|
56
|
-
},
|
|
57
|
-
routes: [],
|
|
58
|
-
});
|
|
59
|
-
this.$tw.hooks.invokeHook("th-server-command-post-start", this.twserver, null, "mws");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
package/src/server.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import "./routes/router";
|
|
2
|
-
import "./StateObject";
|
|
3
|
-
import "./streamer";
|
|
4
|
-
import "./global";
|
|
5
|
-
import * as opaque from "@serenity-kit/opaque";
|
|
6
|
-
import { existsSync, mkdirSync, PathOrFileDescriptor, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import * as sessions from "./routes/services/sessions";
|
|
8
|
-
import * as attacher from "./routes/services/attachments";
|
|
9
|
-
import { bootTiddlyWiki } from "./tiddlywiki";
|
|
10
|
-
import { Commander } from "./commander";
|
|
11
|
-
import { ListenerBase } from "./commands/mws-listen";
|
|
12
|
-
import { createPasswordService } from "./routes/services/PasswordService";
|
|
13
|
-
import { resolve } from "node:path";
|
|
14
|
-
|
|
15
|
-
export * from "./routes/services/sessions";
|
|
16
|
-
|
|
17
|
-
export interface MWSConfig {
|
|
18
|
-
/**
|
|
19
|
-
* Listener config for the mws-listen command.
|
|
20
|
-
* This is extracted from the config early and passed directly to mws-listen.
|
|
21
|
-
*/
|
|
22
|
-
listeners: {
|
|
23
|
-
/** The key file for the HTTPS server. If either key or cert are set, both are required, and HTTPS is enforced. */
|
|
24
|
-
key?: string
|
|
25
|
-
/** The cert file for the HTTPS server. If either key or cert are set, both are required, and HTTPS is enforced. */
|
|
26
|
-
cert?: string
|
|
27
|
-
/** The port to listen on. If not specified, listen will not be called. */
|
|
28
|
-
port?: number
|
|
29
|
-
/** The hostname to listen on. If this is specified, then port is required. */
|
|
30
|
-
host?: string
|
|
31
|
-
}[]
|
|
32
|
-
/**
|
|
33
|
-
* Called and awaited by the mws-listen command.
|
|
34
|
-
* This is extracted from the config early and passed directly to mws-listen.
|
|
35
|
-
*/
|
|
36
|
-
onListenersCreated?: (listeners: ListenerBase[]) => Promise<void>;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Enables the dev configuration of the server.
|
|
40
|
-
* The string is the absolute path to the dev repo.
|
|
41
|
-
*/
|
|
42
|
-
enableDevServer?: string
|
|
43
|
-
/**
|
|
44
|
-
* Path or file descriptor to the password master key.
|
|
45
|
-
* If this key changes, all passwords will be invalid and need to be changed.
|
|
46
|
-
*/
|
|
47
|
-
passwordMasterKeyFile: string | number
|
|
48
|
-
onPasswordKeyFileRead?: () => void;
|
|
49
|
-
/** Path to the datafolder. */
|
|
50
|
-
wikiPath: string
|
|
51
|
-
/** MWS site configuration */
|
|
52
|
-
config?: MWSConfigConfig
|
|
53
|
-
/**
|
|
54
|
-
* The session manager class registers the login handler routes and sets the auth user
|
|
55
|
-
*/
|
|
56
|
-
SessionManager?: typeof sessions.SessionManager;
|
|
57
|
-
/**
|
|
58
|
-
* The attachment service class is used by routes to handle attachments
|
|
59
|
-
*/
|
|
60
|
-
AttachmentService?: typeof attacher.AttachmentService;
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
export interface MWSConfigConfig {
|
|
66
|
-
/** If true, allow users that aren't logged in to read. */
|
|
67
|
-
readonly allowAnonReads?: boolean
|
|
68
|
-
/** If true, allow users that aren't logged in to write. */
|
|
69
|
-
readonly allowAnonWrites?: boolean
|
|
70
|
-
/** If true, recipes will allow access to a user who does not have read access to all its bags. */
|
|
71
|
-
readonly allowUnreadableBags?: boolean
|
|
72
|
-
/** If true, files larger than `this * 1024` will be saved to disk alongside the database instead of in the database. */
|
|
73
|
-
readonly saveLargeTextToFileSystem?: number;
|
|
74
|
-
readonly enableGzip?: boolean
|
|
75
|
-
readonly enableBrowserCache?: boolean
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
*
|
|
82
|
-
* The promise returned will resolve once the commander has completed.
|
|
83
|
-
* It will reject if the commander or anything else throws an error.
|
|
84
|
-
*
|
|
85
|
-
```
|
|
86
|
-
import startServer from "./src/server.ts";
|
|
87
|
-
|
|
88
|
-
startServer({
|
|
89
|
-
// enableDevServer: true,
|
|
90
|
-
passwordMasterKey: "./localpass.key",
|
|
91
|
-
listeners: [{
|
|
92
|
-
key: "./localhost.key",
|
|
93
|
-
cert: "./localhost.crt",
|
|
94
|
-
port: 5000,
|
|
95
|
-
}],
|
|
96
|
-
config: {
|
|
97
|
-
wikiPath: "./editions/mws",
|
|
98
|
-
allowAnonReads: false,
|
|
99
|
-
allowAnonWrites: false,
|
|
100
|
-
allowUnreadableBags: false,
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
*/
|
|
106
|
-
export default async function startServer(config: MWSConfig) {
|
|
107
|
-
|
|
108
|
-
await opaque.ready;
|
|
109
|
-
|
|
110
|
-
if (!["string", "number"].includes(typeof config.passwordMasterKeyFile)) {
|
|
111
|
-
throw new Error("passwordMasterKeyFile must be a string or number");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (typeof config.passwordMasterKeyFile === "string"
|
|
115
|
-
&& config.passwordMasterKeyFile
|
|
116
|
-
&& !existsSync(config.passwordMasterKeyFile)
|
|
117
|
-
) {
|
|
118
|
-
writeFileSync(config.passwordMasterKeyFile, opaque.server.createSetup());
|
|
119
|
-
console.log("Password master key created at", config.passwordMasterKeyFile);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const passwordMasterKey = readFileSync(config.passwordMasterKeyFile, "utf8").trim();
|
|
123
|
-
|
|
124
|
-
config.onPasswordKeyFileRead?.();
|
|
125
|
-
|
|
126
|
-
config.wikiPath = resolve(config.wikiPath);
|
|
127
|
-
|
|
128
|
-
const commander = new Commander(
|
|
129
|
-
config,
|
|
130
|
-
await bootTiddlyWiki(config.wikiPath),
|
|
131
|
-
await createPasswordService(passwordMasterKey)
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
await commander.init();
|
|
135
|
-
const cli = process.argv.slice(2);
|
|
136
|
-
// mws-command-separator prevents params after it from being applied to the command before it.
|
|
137
|
-
// it throws an error if it ends up with any params.
|
|
138
|
-
await commander.execute([
|
|
139
|
-
...cli.length ? cli : [
|
|
140
|
-
"--mws-init-store",
|
|
141
|
-
"--mws-listen"
|
|
142
|
-
],
|
|
143
|
-
]);
|
|
144
|
-
|
|
145
|
-
if (commander.setupRequired) {
|
|
146
|
-
console.log("MWS setup required. Please run either --mws-init-store or --mws-load-archive");
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|