@weibaohui/dsh-git-server 0.1.0 → 0.1.1

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.
@@ -1,377 +0,0 @@
1
- // /api/web/* JSON endpoints consumed by the React SPA (sign-in/up, MFA,
2
- // activate, reset password, repo header/watch/star/commit).
3
- import * as db from './db/db.js';
4
- import { conf } from './conf.js';
5
- import { completeSignIn } from './context.js';
6
- import { getCommit } from './gitx/git.js';
7
- function errBody(error, fields) {
8
- const out = {};
9
- if (error)
10
- out.error = error;
11
- if (fields)
12
- out.fields = fields;
13
- return out;
14
- }
15
- function isAlphaDashDot(s) {
16
- return !/[^\d\w\-_.]/.test(s);
17
- }
18
- export async function handleWebAPI(c, subPath) {
19
- // subPath begins after /api/web
20
- const method = c.Method();
21
- if (subPath === '/user/info' && method === 'GET') {
22
- if (!c.User) {
23
- c.Status(204);
24
- c.res.end();
25
- c.rendered = true;
26
- return true;
27
- }
28
- c.JSONSuccess({
29
- username: c.User.name,
30
- avatarURL: c.User.AvatarURL(),
31
- isAdmin: c.User.is_admin === 1,
32
- canCreateOrganization: c.User.CanCreateOrganization(),
33
- });
34
- return true;
35
- }
36
- if (subPath === '/user/sign-up') {
37
- if (method === 'GET') {
38
- c.JSONSuccess({
39
- registrationDisabled: conf.disableRegistration,
40
- captchaEnabled: conf.enableRegistrationCaptcha,
41
- });
42
- return true;
43
- }
44
- if (method === 'POST') {
45
- const req = await c.form();
46
- const userName = String(req.userName ?? '');
47
- const email = String(req.email ?? '');
48
- const password = String(req.password ?? '');
49
- if (conf.disableRegistration) {
50
- c.JSON(403, errBody(c.Tr('auth.disable_register_prompt')));
51
- return true;
52
- }
53
- if (conf.enableRegistrationCaptcha) {
54
- const { validateCaptcha } = await import('./toolx.js');
55
- const captchaID = c.GetCookie('gogs_captcha');
56
- if (!validateCaptcha(captchaID, String(req.captcha ?? ''))) {
57
- const msg = c.Tr('form.captcha_incorrect');
58
- c.JSON(401, errBody(undefined, { captcha: msg }));
59
- return true;
60
- }
61
- }
62
- if (!userName || !isAlphaDashDot(userName) || userName.length > 35) {
63
- c.JSON(400, errBody(undefined, { userName: c.Tr('form.username') + c.Tr('form.alpha_dash_dot_error') }));
64
- return true;
65
- }
66
- if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || email.length > 254) {
67
- c.JSON(400, errBody(undefined, { email: c.Tr('form.email') + c.Tr('form.email_error') }));
68
- return true;
69
- }
70
- if (!password || password.length > 255) {
71
- c.JSON(400, errBody(undefined, { password: c.Tr('form.password') + c.Tr('form.require_error') }));
72
- return true;
73
- }
74
- let user;
75
- try {
76
- user = db.createUser(userName, email, { password, activated: !conf.requireEmailConfirmation });
77
- }
78
- catch (e) {
79
- if (e instanceof db.AlreadyExistError) {
80
- if (String(e.message).includes('email')) {
81
- c.JSON(422, errBody(undefined, { email: c.Tr('form.email_been_used') }));
82
- }
83
- else {
84
- c.JSON(422, errBody(undefined, { userName: c.Tr('form.username_been_taken') }));
85
- }
86
- return true;
87
- }
88
- if (e instanceof db.NameNotAllowedError) {
89
- c.JSON(400, errBody(undefined, { userName: c.Tr('user.form.name_not_allowed', userName) }));
90
- return true;
91
- }
92
- throw e;
93
- }
94
- // first user becomes admin & activated
95
- const count = db.db().prepare('SELECT COUNT(*) AS c FROM user WHERE type = 0').get().c;
96
- if (count === 1) {
97
- db.updateUserColumns(user.id, { is_active: 1, is_admin: 1 });
98
- }
99
- c.JSONSuccess({});
100
- return true;
101
- }
102
- }
103
- if (subPath === '/user/sign-in') {
104
- if (method === 'GET') {
105
- // local auth only in this port; no external login sources yet
106
- c.JSONSuccess({ loginSources: [] });
107
- return true;
108
- }
109
- if (method === 'POST') {
110
- const req = await c.form();
111
- const username = String(req.username ?? '');
112
- const password = String(req.password ?? '');
113
- const user = db.getUserByUsername(username) ?? db.getUserByEmail(username);
114
- const { verifyPassword } = await import('./authx/password.js');
115
- // 兜底管理员:永远允许本地登录(插件自有紧急账号)
116
- const bootstrapName = (process.env.DSH_BOOTSTRAP_ADMIN || '').split(':')[0] || 'root';
117
- if (user && user.type === 0 && user.name === bootstrapName && verifyPassword(password, user.salt, user.passwd)) {
118
- completeSignIn(c, user);
119
- c.JSONSuccess({});
120
- return true;
121
- }
122
- // 账户单一来源:UM service 启用时登录只走 service;本地密码仅当
123
- // service 不可用时兜底(防影子账号残留的旧同步密码绕过)
124
- const um = await import('./authx/um.js');
125
- if (um.umServiceEnabled()) {
126
- const check = await um.umCheckLogin(username, password);
127
- if (check.ok) {
128
- const shadow = um.ensureShadowUser(check.user);
129
- if (shadow && shadow.type === 0) {
130
- completeSignIn(c, shadow);
131
- c.JSONSuccess({});
132
- return true;
133
- }
134
- }
135
- if (!check.unavailable) {
136
- c.JSON(401, errBody(c.Tr('form.username_password_incorrect'), { username: null, password: null }));
137
- return true;
138
- }
139
- }
140
- const authed = !!(user && user.type === 0 && verifyPassword(password, user.salt, user.passwd));
141
- if (!authed) {
142
- c.JSON(401, errBody(c.Tr('form.username_password_incorrect'), { username: null, password: null }));
143
- return true;
144
- }
145
- completeSignIn(c, user);
146
- c.JSONSuccess({});
147
- return true;
148
- }
149
- }
150
- if (subPath === '/user/sign-out' && method === 'POST') {
151
- c.session.Clear();
152
- c.session.Release();
153
- c.SetCookie(conf.cookieUserName, '', 0);
154
- c.NoContent();
155
- return true;
156
- }
157
- if (subPath === '/user/reset-password') {
158
- if (method === 'GET') {
159
- const code = c.Query('code');
160
- let valid = false;
161
- if (code) {
162
- const { verifyUserFromCode } = await import('./toolx.js');
163
- const parsed = verifyUserFromCode(code, (u) => db.getUserByUsername(u));
164
- valid = !!parsed?.valid;
165
- }
166
- c.JSONSuccess({ emailEnabled: conf.emailEnabled, valid });
167
- return true;
168
- }
169
- if (method === 'POST') {
170
- if (!conf.emailEnabled) {
171
- c.JSON(403, errBody(c.Tr('auth.disable_register_mail')));
172
- return true;
173
- }
174
- const req = await c.form();
175
- const email = String(req.email ?? '').toLowerCase().trim();
176
- const user = db.getUserByEmail(email);
177
- if (!user) {
178
- c.JSONSuccess({ hours: Math.floor(conf.activateCodeLives / 60) });
179
- return true;
180
- }
181
- if (user.type !== 0) {
182
- const msg = c.Tr('auth.non_local_account');
183
- c.JSON(403, errBody(undefined, { email: msg }));
184
- return true;
185
- }
186
- try {
187
- const { createActivateCode } = await import('./toolx.js');
188
- const { sendResetPasswordMail } = await import('./mailer.js');
189
- const code = createActivateCode(user, conf.resetPwdCodeLives) + Buffer.from(user.name).toString('hex');
190
- await sendResetPasswordMail(user, code);
191
- c.JSONSuccess({ hours: Math.floor(conf.resetPwdCodeLives / 60) });
192
- }
193
- catch (e) {
194
- console.error('[mailer] reset mail:', e?.message ?? e);
195
- c.JSONSuccess({ hours: Math.floor(conf.resetPwdCodeLives / 60) });
196
- }
197
- return true;
198
- }
199
- }
200
- if (subPath === '/user/reset-password/complete' && method === 'POST') {
201
- const req = await c.form();
202
- const code = String(req.code ?? '');
203
- const { verifyUserFromCode } = await import('./toolx.js');
204
- const parsed = verifyUserFromCode(code, (u) => db.getUserByUsername(u));
205
- if (!parsed || !parsed.valid) {
206
- c.JSON(400, errBody(c.Tr('auth.invalid_code')));
207
- return true;
208
- }
209
- const password = String(req.password ?? '');
210
- if (password.length < 6) {
211
- const msg = c.Tr('auth.password_too_short');
212
- c.JSON(400, errBody(undefined, { password: msg }));
213
- return true;
214
- }
215
- const { encodePassword, randomSalt } = await import('./authx/password.js');
216
- const salt = randomSalt();
217
- db.updateUserColumns(parsed.user.id, { passwd: encodePassword(password, salt), salt });
218
- c.Status(204);
219
- c.res.end();
220
- c.rendered = true;
221
- return true;
222
- }
223
- if (subPath === '/user/activate') {
224
- if (!c.User) {
225
- c.Status(401);
226
- c.res.end();
227
- c.rendered = true;
228
- return true;
229
- }
230
- if (method === 'POST') {
231
- if (!conf.requireEmailConfirmation) {
232
- c.JSON(403, errBody(c.Tr('auth.disable_register_mail')));
233
- return true;
234
- }
235
- if (!conf.emailEnabled) {
236
- c.JSON(403, errBody(c.Tr('auth.disable_register_mail')));
237
- return true;
238
- }
239
- try {
240
- const { createActivateCode } = await import('./toolx.js');
241
- const { sendActivateMail } = await import('./mailer.js');
242
- const code = createActivateCode(c.User, conf.activateCodeLives) + Buffer.from(c.User.name).toString('hex');
243
- await sendActivateMail(c.User, code);
244
- c.JSONSuccess({ codeLifetimeHours: Math.floor(conf.activateCodeLives / 60) });
245
- }
246
- catch (e) {
247
- console.error('[mailer] activate mail:', e?.message ?? e);
248
- c.JSON(500, errBody(String(e?.message ?? e)));
249
- }
250
- return true;
251
- }
252
- c.JSONSuccess({ email: c.User.email, codeLifetimeHours: Math.floor(conf.activateCodeLives / 60) });
253
- return true;
254
- }
255
- if (subPath === '/user/activate/complete' && method === 'POST') {
256
- const req = await c.form();
257
- const code = String(req.code ?? '');
258
- const { verifyUserFromCode } = await import('./toolx.js');
259
- const parsed = verifyUserFromCode(code, (u) => db.getUserByUsername(u));
260
- if (!parsed || !parsed.valid) {
261
- c.JSON(400, errBody(c.Tr('auth.invalid_code')));
262
- return true;
263
- }
264
- const { randomSalt } = await import('./authx/password.js');
265
- const salt = randomSalt();
266
- db.updateUserColumns(parsed.user.id, { is_active: 1, rands: salt });
267
- completeSignIn(c, parsed.user);
268
- c.Status(204);
269
- c.res.end();
270
- c.rendered = true;
271
- return true;
272
- }
273
- // repo endpoints
274
- const repoMatch = /^\/([^/]+)\/([^/]+)(\/.*)?$/.exec(subPath);
275
- if (repoMatch) {
276
- const [, ownerName, repoName, rest] = repoMatch;
277
- const owner = db.getUserByUsername(ownerName);
278
- const repo = owner ? db.getRepoByOwnerAndName(owner, repoName) : null;
279
- if (owner && repo) {
280
- const viewerID = c.UserID();
281
- const mode = db.accessMode(viewerID, repo);
282
- const canRead = mode >= db.AccessMode.READ || (!repo.is_private && !conf.requireSigninView);
283
- if (rest === '/header' && method === 'GET') {
284
- if (!canRead) {
285
- c.Status(404);
286
- c.res.end();
287
- c.rendered = true;
288
- return true;
289
- }
290
- c.JSONSuccess({
291
- id: repo.id,
292
- owner: owner.name,
293
- name: repo.name,
294
- avatarURL: owner.AvatarURL(),
295
- visibility: repo.is_private ? 'private' : 'public',
296
- watchCount: repo.num_watches,
297
- starCount: repo.num_stars,
298
- forkCount: repo.num_forks,
299
- issuesEnabled: repo.enable_issues === 1,
300
- openIssueCount: repo.num_issues - repo.num_closed_issues,
301
- pullRequestsEnabled: repo.enable_pulls === 1,
302
- openPullRequestCount: repo.num_pulls - repo.num_closed_pulls,
303
- wikiEnabled: repo.enable_wiki === 1,
304
- viewerCanAdminister: mode >= db.AccessMode.ADMIN,
305
- viewerIsWatching: !!db.isWatching(viewerID, repo.id),
306
- viewerIsStarring: !!db.isStaring(viewerID, repo.id),
307
- });
308
- return true;
309
- }
310
- if (rest === '/watch') {
311
- if (method === 'POST') {
312
- db.watchRepo(viewerID, repo.id, true);
313
- c.NoContent();
314
- return true;
315
- }
316
- if (method === 'DELETE') {
317
- db.watchRepo(viewerID, repo.id, false);
318
- c.NoContent();
319
- return true;
320
- }
321
- }
322
- if (rest === '/star') {
323
- if (method === 'POST') {
324
- db.starRepo(viewerID, repo.id, true);
325
- c.NoContent();
326
- return true;
327
- }
328
- if (method === 'DELETE') {
329
- db.starRepo(viewerID, repo.id, false);
330
- c.NoContent();
331
- return true;
332
- }
333
- }
334
- const commitMatch = /^\/commit\/([0-9a-f]{7,40})$/.exec(rest ?? '');
335
- if (commitMatch && method === 'GET') {
336
- if (!canRead) {
337
- c.Status(404);
338
- c.res.end();
339
- c.rendered = true;
340
- return true;
341
- }
342
- const commit = await getCommit(repo.RepoPath(), commitMatch[1]);
343
- if (!commit) {
344
- c.Status(404);
345
- c.res.end();
346
- c.rendered = true;
347
- return true;
348
- }
349
- const authorUser = db.getUserByEmail(commit.author.email);
350
- c.JSONSuccess({
351
- sha: commit.id,
352
- subject: commit.Summary(),
353
- body: commit.message.slice(commit.Summary().length).replace(/^\n/, ''),
354
- author: {
355
- name: commit.author.name,
356
- email: commit.author.email,
357
- when: commit.author.when.toISOString(),
358
- avatarURL: avatarURLForEmail(commit.author.email),
359
- ...(authorUser ? { profileURL: authorUser.HomeURLPath() } : {}),
360
- },
361
- parents: commit.parents,
362
- });
363
- return true;
364
- }
365
- }
366
- }
367
- return false;
368
- }
369
- function avatarURLForEmail(email) {
370
- return conf.subpath + '/user/avatar/' + require_md5(email);
371
- }
372
- // small helper to avoid import cycle overhead
373
- import { md5 } from './authx/password.js';
374
- function require_md5(s) {
375
- return md5(s.trim().toLowerCase());
376
- }
377
- //# sourceMappingURL=webapi.js.map