texlite 0.11.0 → 0.13.0
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/DESIGN.md +64 -12
- package/dist/client/assets/AdminUsers-CfKlKMUu.js +1 -0
- package/dist/client/assets/{CitationLibraryDialog-tVD3i2gg.js → CitationLibraryDialog-BpnX28Le.js} +1 -1
- package/dist/client/assets/Dashboard-nbu54S0M.js +2 -0
- package/dist/client/assets/GitDialog-CrLMkNSh.js +3 -0
- package/dist/client/assets/GitHubAccountDialog-Bt0c8yBQ.js +1 -0
- package/dist/client/assets/HistoryDialog-CNEHivv5.js +7 -0
- package/dist/client/assets/LatexEditor-Dq9cqkpP.js +138 -0
- package/dist/client/assets/{PdfPreview-CMkO72I5.js → PdfPreview-xiB5Piyv.js} +1 -1
- package/dist/client/assets/ProjectIconPickerDialog-BSdiBwtv.js +1 -0
- package/dist/client/assets/{ProjectNavigationDialogs-isGMXCfG.js → ProjectNavigationDialogs-C7cD7PvT.js} +1 -1
- package/dist/client/assets/ProjectSettings-CRHG-jAZ.js +1 -0
- package/dist/client/assets/ProjectWorkspace-BdmKRaK2.js +32 -0
- package/dist/client/assets/{SelectionHistoryDialog-ZvAhIJMy.js → SelectionHistoryDialog-JL_2qLGf.js} +1 -1
- package/dist/client/assets/SystemMetricsDialog-BIWJwsxD.js +1 -0
- package/dist/client/assets/{TagManagementDialog-qhZfHgTr.js → TagManagementDialog-CJXWtqXN.js} +1 -1
- package/dist/client/assets/{WordCountDialog-tFhMCGyn.js → WordCountDialog-sr21g-ex.js} +1 -1
- package/dist/client/assets/check-CzfkSYah.js +1 -0
- package/dist/client/assets/{chktex-tM1XXPk2.js → chktex-BEVGf49u.js} +1 -1
- package/dist/client/assets/{editor-B9HNwtsd.js → editor-taw1iptB.js} +1 -1
- package/dist/client/assets/external-link-DuxFggPh.js +1 -0
- package/dist/client/assets/github-D805ABUR.js +1 -0
- package/dist/client/assets/{hard-drive-CkIPwUSV.js → hard-drive-De8wdQpw.js} +1 -1
- package/dist/client/assets/index-BYAZKPNO.js +10 -0
- package/dist/client/assets/{index-DTYEGzB7.js → index-CXWzCgcU.js} +1 -1
- package/dist/client/assets/index-Cc9-JFj7.css +1 -0
- package/dist/client/assets/{minimize-2-CLuDohEf.js → minimize-2-DP0ZJHGp.js} +1 -1
- package/dist/client/assets/{minus-BpuvzV2m.js → minus-BDK-NFbs.js} +1 -1
- package/dist/client/assets/{plus-BFr75l91.js → plus-GB_3iSP8.js} +1 -1
- package/dist/client/assets/{refresh-cw-DyA4NKh7.js → refresh-cw-DwUVAz9L.js} +1 -1
- package/dist/client/assets/{search-Nu4OcY_n.js → search-BJ0HR5pC.js} +1 -1
- package/dist/client/assets/{sigma-B-wjepsY.js → sigma-DOfd27T4.js} +1 -1
- package/dist/client/assets/{spellCheck-C-bNJsIU.js → spellCheck-CRN7y5Qf.js} +1 -1
- package/dist/client/assets/{tags-BSA08jK3.js → tags-BdjO5Lmr.js} +1 -1
- package/dist/client/assets/{type-BCdIbDmF.js → type-qV4gbXDh.js} +1 -1
- package/dist/client/assets/{user-plus-gFwY-NA3.js → user-plus-CPne6OnV.js} +1 -1
- package/dist/client/assets/x-DPtqmVM9.js +1 -0
- package/dist/client/index.html +3 -3
- package/dist/server/db.js +30 -1
- package/dist/server/git.js +64 -29
- package/dist/server/harper.js +14 -0
- package/dist/server/history.js +14 -5
- package/dist/server/i18n.js +2 -2
- package/dist/server/routes/projectGit.js +10 -11
- package/dist/server/routes/projectHistory.js +32 -5
- package/dist/server/routes/projectMembers.js +0 -5
- package/dist/server/routes/projects.js +10 -0
- package/dist/server/routes/users.js +0 -2
- package/dist/shared/historyFiles.js +6 -0
- package/package.json +3 -1
- package/dist/client/assets/AdminUsers-BExOMZsW.js +0 -1
- package/dist/client/assets/Dashboard-DRt4rjKP.js +0 -2
- package/dist/client/assets/GitDialog-CjDCi1uh.js +0 -3
- package/dist/client/assets/HistoryDialog-Pa9mK3Ky.js +0 -7
- package/dist/client/assets/LatexEditor-8r486VeG.js +0 -138
- package/dist/client/assets/ProjectIconPickerDialog-B52xbSyr.js +0 -1
- package/dist/client/assets/ProjectSettings-Br_kJ113.js +0 -1
- package/dist/client/assets/ProjectWorkspace-CcbrdCy8.js +0 -32
- package/dist/client/assets/SystemMetricsDialog-D4ouU4HI.js +0 -1
- package/dist/client/assets/check-DKIW1zpR.js +0 -1
- package/dist/client/assets/index-90X2GUd_.css +0 -1
- package/dist/client/assets/index-CqBmQAzW.js +0 -10
- package/dist/client/assets/key-round-BEnQo7IM.js +0 -1
- package/dist/client/assets/x-Cspw_pa4.js +0 -1
package/dist/server/git.js
CHANGED
|
@@ -22,6 +22,7 @@ export class ProjectGitService {
|
|
|
22
22
|
await this.ensureGitAvailable();
|
|
23
23
|
assertNoSourceSymlinks(this.config, project.id);
|
|
24
24
|
const settings = this.settings(project.id);
|
|
25
|
+
const account = this.userSettings(project.owner_id);
|
|
25
26
|
const gitDirectory = path.join(sourceRoot(this.config, project.id), ".git");
|
|
26
27
|
let gitStat = null;
|
|
27
28
|
try {
|
|
@@ -37,8 +38,8 @@ export class ProjectGitService {
|
|
|
37
38
|
if (!initialized)
|
|
38
39
|
return {
|
|
39
40
|
initialized: false,
|
|
40
|
-
tokenConfigured: Boolean(
|
|
41
|
-
githubLogin:
|
|
41
|
+
tokenConfigured: Boolean(account?.token_ciphertext),
|
|
42
|
+
githubLogin: account?.github_login ?? null,
|
|
42
43
|
remoteUrl: settings?.remote_url ?? null,
|
|
43
44
|
repositoryName: settings?.repository_name ?? null,
|
|
44
45
|
repositoryHtmlUrl: settings?.repository_html_url ?? null,
|
|
@@ -56,11 +57,17 @@ export class ProjectGitService {
|
|
|
56
57
|
const branch = branchResult.code === 0 ? branchResult.stdout.trim() : null;
|
|
57
58
|
const remoteResult = await this.git(root, ["remote", "get-url", "origin"], [2]);
|
|
58
59
|
const latestResult = await this.git(root, ["log", "-1", "--format=%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s"], [128]);
|
|
59
|
-
const
|
|
60
|
+
const upstreamResult = branch
|
|
61
|
+
? await this.git(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], [128])
|
|
62
|
+
: null;
|
|
63
|
+
// A newly initialized backup repository has no upstream until its first
|
|
64
|
+
// push. In that case, every local commit is pending for the remote.
|
|
65
|
+
const aheadRevision = upstreamResult?.code === 0 ? "@{upstream}..HEAD" : "HEAD";
|
|
66
|
+
const aheadResult = branch ? await this.git(root, ["rev-list", "--count", aheadRevision], [128]) : null;
|
|
60
67
|
return {
|
|
61
68
|
initialized: true,
|
|
62
|
-
tokenConfigured: Boolean(
|
|
63
|
-
githubLogin:
|
|
69
|
+
tokenConfigured: Boolean(account?.token_ciphertext),
|
|
70
|
+
githubLogin: account?.github_login ?? null,
|
|
64
71
|
remoteUrl: settings?.remote_url ?? (remoteResult.code === 0 ? remoteResult.stdout.trim() : null),
|
|
65
72
|
repositoryName: settings?.repository_name ?? null,
|
|
66
73
|
repositoryHtmlUrl: settings?.repository_html_url ?? null,
|
|
@@ -69,41 +76,43 @@ export class ProjectGitService {
|
|
|
69
76
|
dirty: Boolean(porcelain),
|
|
70
77
|
restorable: porcelain.split("\n").some((line) => Boolean(line) && !line.startsWith("??")),
|
|
71
78
|
changedFiles: porcelain ? porcelain.split("\n").length : 0,
|
|
72
|
-
ahead: aheadResult
|
|
79
|
+
ahead: aheadResult?.code === 0 ? Number.parseInt(aheadResult.stdout.trim(), 10) || 0 : 0,
|
|
73
80
|
latestCommit: latestResult.code === 0 ? parseCommit(latestResult.stdout.trim()) : null
|
|
74
81
|
};
|
|
75
82
|
}
|
|
76
|
-
|
|
83
|
+
githubAccountStatus(userId) {
|
|
84
|
+
const settings = this.userSettings(userId);
|
|
85
|
+
return { tokenConfigured: Boolean(settings?.token_ciphertext), githubLogin: settings?.github_login ?? null };
|
|
86
|
+
}
|
|
87
|
+
async configureUserToken(userId, tokenInput) {
|
|
77
88
|
const token = tokenInput.trim();
|
|
78
89
|
if (token.length < 20 || token.length > 500)
|
|
79
90
|
throw httpError(400, "GIT_TOKEN_INVALID");
|
|
80
|
-
await this.ensureGitAvailable();
|
|
81
91
|
const account = await this.githubRequest(token, "/user", { method: "GET" });
|
|
82
92
|
if (typeof account.login !== "string" || !account.login)
|
|
83
93
|
throw httpError(502, "GIT_ACCOUNT_INVALID");
|
|
84
|
-
await this.ensureRepository(project);
|
|
85
94
|
const timestamp = new Date().toISOString();
|
|
86
|
-
this.db.prepare(`INSERT INTO
|
|
87
|
-
(
|
|
88
|
-
VALUES (?, ?, ?,
|
|
89
|
-
ON CONFLICT(
|
|
95
|
+
this.db.prepare(`INSERT INTO user_github_settings
|
|
96
|
+
(user_id, token_ciphertext, github_login, created_at, updated_at)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?)
|
|
98
|
+
ON CONFLICT(user_id) DO UPDATE SET token_ciphertext = excluded.token_ciphertext,
|
|
90
99
|
github_login = excluded.github_login, updated_at = excluded.updated_at`)
|
|
91
|
-
.run(
|
|
92
|
-
return this.
|
|
100
|
+
.run(userId, encryptToken(this.config, token), account.login, timestamp, timestamp);
|
|
101
|
+
return this.githubAccountStatus(userId);
|
|
93
102
|
}
|
|
94
|
-
|
|
95
|
-
this.db.prepare("
|
|
96
|
-
|
|
97
|
-
return this.status(project);
|
|
103
|
+
removeUserToken(userId) {
|
|
104
|
+
this.db.prepare("DELETE FROM user_github_settings WHERE user_id = ?").run(userId);
|
|
105
|
+
return this.githubAccountStatus(userId);
|
|
98
106
|
}
|
|
99
107
|
async createGitHubRepository(project, name, isPrivate) {
|
|
100
108
|
if (!/^[A-Za-z0-9._-]{1,100}$/.test(name))
|
|
101
109
|
throw httpError(400, "GIT_REPOSITORY_NAME_INVALID");
|
|
102
110
|
await this.ensureGitAvailable();
|
|
103
|
-
const
|
|
104
|
-
|
|
111
|
+
const account = this.requireUserTokenSettings(project.owner_id);
|
|
112
|
+
const settings = this.settings(project.id);
|
|
113
|
+
if (settings?.remote_url)
|
|
105
114
|
throw httpError(409, "GIT_REMOTE_ALREADY_CONFIGURED");
|
|
106
|
-
const token = decryptToken(this.config,
|
|
115
|
+
const token = decryptToken(this.config, account.token_ciphertext);
|
|
107
116
|
const repository = await this.githubRequest(token, "/user/repos", {
|
|
108
117
|
method: "POST",
|
|
109
118
|
body: JSON.stringify({ name, private: isPrivate, auto_init: false, description: `Backup of ${project.name}` })
|
|
@@ -112,6 +121,7 @@ export class ProjectGitService {
|
|
|
112
121
|
throw httpError(502, "GIT_REPOSITORY_RESPONSE_INVALID");
|
|
113
122
|
}
|
|
114
123
|
await this.ensureRepository(project);
|
|
124
|
+
this.ensureProjectSettings(project.id);
|
|
115
125
|
const root = sourceRoot(this.config, project.id);
|
|
116
126
|
const currentRemote = await this.git(root, ["remote", "get-url", "origin"], [2]);
|
|
117
127
|
if (currentRemote.code === 0)
|
|
@@ -149,8 +159,8 @@ export class ProjectGitService {
|
|
|
149
159
|
return parseCommit(result.stdout.trim());
|
|
150
160
|
}
|
|
151
161
|
async push(project) {
|
|
152
|
-
const
|
|
153
|
-
if (!settings.remote_url)
|
|
162
|
+
const account = this.requireUserTokenSettings(project.owner_id);
|
|
163
|
+
if (!this.settings(project.id)?.remote_url)
|
|
154
164
|
throw httpError(409, "GIT_REMOTE_NOT_CONFIGURED");
|
|
155
165
|
const root = this.requireRepository(project.id);
|
|
156
166
|
assertNoSourceSymlinks(this.config, project.id);
|
|
@@ -163,7 +173,7 @@ export class ProjectGitService {
|
|
|
163
173
|
const branch = branchResult.stdout.trim();
|
|
164
174
|
if (!/^[A-Za-z0-9._/-]+$/.test(branch))
|
|
165
175
|
throw httpError(400, "GIT_BRANCH_INVALID");
|
|
166
|
-
const token = decryptToken(this.config,
|
|
176
|
+
const token = decryptToken(this.config, account.token_ciphertext);
|
|
167
177
|
await this.git(root, ["push", "--set-upstream", "origin", `HEAD:refs/heads/${branch}`], [], this.authEnvironment(token));
|
|
168
178
|
return this.status(project);
|
|
169
179
|
}
|
|
@@ -269,12 +279,20 @@ export class ProjectGitService {
|
|
|
269
279
|
settings(projectId) {
|
|
270
280
|
return this.db.prepare("SELECT * FROM project_git_settings WHERE project_id = ?").get(projectId);
|
|
271
281
|
}
|
|
272
|
-
|
|
273
|
-
|
|
282
|
+
userSettings(userId) {
|
|
283
|
+
return this.db.prepare("SELECT * FROM user_github_settings WHERE user_id = ?").get(userId);
|
|
284
|
+
}
|
|
285
|
+
requireUserTokenSettings(userId) {
|
|
286
|
+
const settings = this.userSettings(userId);
|
|
274
287
|
if (!settings?.token_ciphertext)
|
|
275
288
|
throw httpError(409, "GIT_TOKEN_NOT_CONFIGURED");
|
|
276
289
|
return settings;
|
|
277
290
|
}
|
|
291
|
+
ensureProjectSettings(projectId) {
|
|
292
|
+
const timestamp = new Date().toISOString();
|
|
293
|
+
this.db.prepare(`INSERT INTO project_git_settings (project_id, default_branch, created_at, updated_at)
|
|
294
|
+
VALUES (?, 'main', ?, ?) ON CONFLICT(project_id) DO NOTHING`).run(projectId, timestamp, timestamp);
|
|
295
|
+
}
|
|
278
296
|
async verifyCommit(root, revision) {
|
|
279
297
|
const result = await this.git(root, ["rev-parse", "--verify", `${revision}^{commit}`], [128]);
|
|
280
298
|
if (result.code !== 0)
|
|
@@ -303,7 +321,10 @@ export class ProjectGitService {
|
|
|
303
321
|
}
|
|
304
322
|
});
|
|
305
323
|
if (!response.ok) {
|
|
306
|
-
|
|
324
|
+
const authError = classifyGitHubAuthFailure(response.status);
|
|
325
|
+
if (authError)
|
|
326
|
+
throw httpError(400, authError);
|
|
327
|
+
throw httpError(502, "GIT_OPERATION_FAILED");
|
|
307
328
|
}
|
|
308
329
|
return await response.json();
|
|
309
330
|
}
|
|
@@ -376,13 +397,27 @@ export class ProjectGitService {
|
|
|
376
397
|
if (size > MAX_COMMAND_OUTPUT)
|
|
377
398
|
return reject(httpError(413, "GIT_OUTPUT_TOO_LARGE"));
|
|
378
399
|
if (result.code !== 0 && !allowedCodes.includes(result.code)) {
|
|
379
|
-
|
|
400
|
+
const pushAuthError = args[0] === "push" ? classifyGitPushFailure(result.stderr) : null;
|
|
401
|
+
return reject(httpError(400, pushAuthError ?? "GIT_COMMAND_FAILED"));
|
|
380
402
|
}
|
|
381
403
|
resolve(result);
|
|
382
404
|
});
|
|
383
405
|
});
|
|
384
406
|
}
|
|
385
407
|
}
|
|
408
|
+
function classifyGitHubAuthFailure(status) {
|
|
409
|
+
// GitHub's 401/403 responses do not reliably distinguish invalid or expired
|
|
410
|
+
// credentials from insufficient repository access (including SSO/policy
|
|
411
|
+
// restrictions), so give users one actionable message for these cases.
|
|
412
|
+
return status === 401 || status === 403 ? "GIT_TOKEN_REJECTED" : null;
|
|
413
|
+
}
|
|
414
|
+
export function classifyGitPushFailure(stderr) {
|
|
415
|
+
if (/saml|single[- ]sign[- ]on|\bsso\b/i.test(stderr) ||
|
|
416
|
+
/invalid username or token|bad credentials|authentication failed|could not read username/i.test(stderr) ||
|
|
417
|
+
/write access.{0,80}(?:not granted|denied)|permission to .{1,200} denied|repository not found|returned error:\s*403/i.test(stderr))
|
|
418
|
+
return "GIT_TOKEN_REJECTED";
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
386
421
|
function parseCommit(line) {
|
|
387
422
|
const [sha = "", shortSha = "", authorName = "", authorEmail = "", authoredAt = "", ...message] = line.split("\x1f");
|
|
388
423
|
return { sha, shortSha, authorName, authorEmail, authoredAt, message: message.join("\x1f") };
|
package/dist/server/harper.js
CHANGED
|
@@ -115,6 +115,20 @@ export class HarperService {
|
|
|
115
115
|
async preload() {
|
|
116
116
|
await this.ensureAvailable();
|
|
117
117
|
}
|
|
118
|
+
/** Availability probe only; does not run a writing check or join its queue. */
|
|
119
|
+
async isAvailable() {
|
|
120
|
+
if (this.disposed)
|
|
121
|
+
return false;
|
|
122
|
+
try {
|
|
123
|
+
await this.ensureAvailable();
|
|
124
|
+
return !this.disposed;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof HarperUnavailableError)
|
|
128
|
+
return false;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
118
132
|
/**
|
|
119
133
|
* Schedule one host-side check. A lane represents one browser page's
|
|
120
134
|
* current file, so an edit can replace only its own stale waiting request
|
package/dist/server/history.js
CHANGED
|
@@ -4,6 +4,8 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import { assertNoSourceSymlinks, listProjectFiles, outputRoot, projectRoot, resolveSourcePath, safeRelativePath, sourceRoot } from "./files.js";
|
|
5
5
|
import { httpError } from "./http.js";
|
|
6
6
|
import { MAX_TEXT_PREVIEW_BYTES } from "./limits.js";
|
|
7
|
+
import { isUtf8 } from "node:buffer";
|
|
8
|
+
import { isHistoryTextFile } from "../shared/historyFiles.js";
|
|
7
9
|
const AUTOSAVE_COALESCE_MS = 2 * 60_000;
|
|
8
10
|
// Logical stored payload; SQLite pages, indexes and WAL are shared database overhead.
|
|
9
11
|
function historyMetadataBytes(row) {
|
|
@@ -39,7 +41,9 @@ export class ProjectHistoryService {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
const changedPaths = previousManifest ? changedManifestPaths(previousManifest, manifest) : Object.keys(manifest.files).sort();
|
|
42
|
-
|
|
44
|
+
// Settings alone do not create a file-history entry. Keep the baseline
|
|
45
|
+
// current so the next real file change captures the effective settings.
|
|
46
|
+
if (changedPaths.length === 0) {
|
|
43
47
|
this.saveBaseline(projectId, manifest);
|
|
44
48
|
return null;
|
|
45
49
|
}
|
|
@@ -72,7 +76,8 @@ export class ProjectHistoryService {
|
|
|
72
76
|
list(projectId, limit = 100) {
|
|
73
77
|
const rows = this.db.prepare(`SELECT history.*, user.username AS author_username, user.display_name AS author_name
|
|
74
78
|
FROM project_history_versions history LEFT JOIN users user ON user.id = history.author_id
|
|
75
|
-
WHERE history.project_id = ?
|
|
79
|
+
WHERE history.project_id = ? AND json_array_length(history.changed_paths_json) > 0
|
|
80
|
+
ORDER BY history.created_at DESC, history.rowid DESC LIMIT ?`)
|
|
76
81
|
.all(projectId, Math.min(200, Math.max(1, limit)));
|
|
77
82
|
return rows.map((row) => versionJson(row));
|
|
78
83
|
}
|
|
@@ -97,7 +102,7 @@ export class ProjectHistoryService {
|
|
|
97
102
|
const rows = this.db.prepare(`SELECT history.*, history.rowid AS history_rowid,
|
|
98
103
|
user.username AS author_username, user.display_name AS author_name
|
|
99
104
|
FROM project_history_versions history LEFT JOIN users user ON user.id = history.author_id
|
|
100
|
-
WHERE history.project_id = ? ${cursorFilter}
|
|
105
|
+
WHERE history.project_id = ? AND json_array_length(history.changed_paths_json) > 0 ${cursorFilter}
|
|
101
106
|
ORDER BY history.created_at DESC, history.rowid DESC LIMIT ?`)
|
|
102
107
|
.all(...values);
|
|
103
108
|
const pageRows = rows.slice(0, limitValue);
|
|
@@ -231,7 +236,8 @@ export class ProjectHistoryService {
|
|
|
231
236
|
previousVersion(projectId, versionId) {
|
|
232
237
|
const row = this.db.prepare(`SELECT older.id FROM project_history_versions older
|
|
233
238
|
JOIN project_history_versions selected ON selected.id = ? AND selected.project_id = older.project_id
|
|
234
|
-
WHERE older.project_id = ? AND (older.
|
|
239
|
+
WHERE older.project_id = ? AND json_array_length(older.changed_paths_json) > 0
|
|
240
|
+
AND (older.created_at < selected.created_at
|
|
235
241
|
OR (older.created_at = selected.created_at AND older.rowid < selected.rowid))
|
|
236
242
|
ORDER BY older.created_at DESC, older.rowid DESC LIMIT 1`).get(versionId, projectId);
|
|
237
243
|
return row ? this.version(row.id, projectId) : null;
|
|
@@ -243,6 +249,8 @@ export class ProjectHistoryService {
|
|
|
243
249
|
}
|
|
244
250
|
readTextFile(projectId, versionId, filePathInput) {
|
|
245
251
|
const filePath = safeRelativePath(filePathInput);
|
|
252
|
+
if (!isHistoryTextFile(filePath))
|
|
253
|
+
return null;
|
|
246
254
|
const entry = this.manifest(projectId, versionId)?.files[filePath];
|
|
247
255
|
if (!entry || entry.size > MAX_TEXT_PREVIEW_BYTES)
|
|
248
256
|
return null;
|
|
@@ -364,7 +372,8 @@ export class ProjectHistoryService {
|
|
|
364
372
|
*/
|
|
365
373
|
readStoredObject(projectId, digest, filePath) {
|
|
366
374
|
try {
|
|
367
|
-
|
|
375
|
+
const bytes = fs.readFileSync(this.objectPath(projectId, digest));
|
|
376
|
+
return !bytes.includes(0) && isUtf8(bytes) ? bytes.toString("utf8") : null;
|
|
368
377
|
}
|
|
369
378
|
catch (error) {
|
|
370
379
|
this.rethrowHistoryObjectError(error, filePath);
|
package/dist/server/i18n.js
CHANGED
|
@@ -46,7 +46,7 @@ const enMessages = {
|
|
|
46
46
|
GIT_REVISION_INVALID: "Choose a Git revision to check out.",
|
|
47
47
|
GIT_TOKEN_INVALID: "Enter a GitHub token.",
|
|
48
48
|
GIT_TOKEN_NOT_CONFIGURED: "Configure a GitHub token first.",
|
|
49
|
-
GIT_TOKEN_REJECTED: "GitHub rejected this token.
|
|
49
|
+
GIT_TOKEN_REJECTED: "GitHub rejected this token. It may be invalid, expired, or lack the required permissions.",
|
|
50
50
|
GIT_UNAVAILABLE: "Git is not available on this server.",
|
|
51
51
|
HARPER_UNAVAILABLE: "Harper spelling and grammar checking is temporarily unavailable.",
|
|
52
52
|
HISTORY_FILE_NOT_FOUND: "The historical file does not exist.",
|
|
@@ -183,7 +183,7 @@ const zhMessages = {
|
|
|
183
183
|
GIT_REVISION_INVALID: "请选择要 checkout 的 Git 版本。",
|
|
184
184
|
GIT_TOKEN_INVALID: "请输入 GitHub token。",
|
|
185
185
|
GIT_TOKEN_NOT_CONFIGURED: "请先配置 GitHub token。",
|
|
186
|
-
GIT_TOKEN_REJECTED: "GitHub
|
|
186
|
+
GIT_TOKEN_REJECTED: "GitHub 拒绝了此 token:它可能无效、已过期或权限不足。",
|
|
187
187
|
GIT_UNAVAILABLE: "服务器未安装 Git。",
|
|
188
188
|
HARPER_UNAVAILABLE: "Harper 拼写与语法检查暂不可用。",
|
|
189
189
|
HISTORY_FILE_NOT_FOUND: "历史文件不存在。",
|
|
@@ -23,27 +23,26 @@ export function registerProjectGitRoutes(app, context) {
|
|
|
23
23
|
return { status: await projectGit.status(requireGitOwner(id, user)) };
|
|
24
24
|
}, { preflight: () => { requireGitOwner(id, user); } });
|
|
25
25
|
});
|
|
26
|
-
app.
|
|
26
|
+
app.get("/api/account/github", async (request, reply) => {
|
|
27
|
+
const user = requireUser(request, reply, db);
|
|
28
|
+
if (!user)
|
|
29
|
+
return;
|
|
30
|
+
return { status: projectGit.githubAccountStatus(user.id) };
|
|
31
|
+
});
|
|
32
|
+
app.put("/api/account/github", async (request, reply) => {
|
|
27
33
|
const user = requireUser(request, reply, db);
|
|
28
34
|
if (!user)
|
|
29
35
|
return;
|
|
30
|
-
const { id } = request.params;
|
|
31
36
|
const body = request.body;
|
|
32
37
|
if (typeof body.token !== "string")
|
|
33
38
|
return apiError(reply, 400, "GIT_TOKEN_INVALID");
|
|
34
|
-
|
|
35
|
-
return await projectMutations.runSerialized(id, async () => {
|
|
36
|
-
return { status: await projectGit.configureToken(requireGitOwner(id, user), token) };
|
|
37
|
-
}, { preflight: () => { requireGitOwner(id, user); } });
|
|
39
|
+
return { status: await projectGit.configureUserToken(user.id, body.token) };
|
|
38
40
|
});
|
|
39
|
-
app.delete("/api/
|
|
41
|
+
app.delete("/api/account/github", async (request, reply) => {
|
|
40
42
|
const user = requireUser(request, reply, db);
|
|
41
43
|
if (!user)
|
|
42
44
|
return;
|
|
43
|
-
|
|
44
|
-
return await projectMutations.runSerialized(id, async () => {
|
|
45
|
-
return { status: await projectGit.removeToken(requireGitOwner(id, user)) };
|
|
46
|
-
}, { preflight: () => { requireGitOwner(id, user); } });
|
|
45
|
+
return { status: projectGit.removeUserToken(user.id) };
|
|
47
46
|
});
|
|
48
47
|
app.post("/api/projects/:id/git/repository", async (request, reply) => {
|
|
49
48
|
const user = requireUser(request, reply, db);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { isUtf8 } from "node:buffer";
|
|
2
3
|
import { requireUser } from "../auth.js";
|
|
3
4
|
import { isCollaborativeTextFile, maxCollaborativeFileBytes } from "../collaboration.js";
|
|
4
5
|
import { hashText } from "../editHistory.js";
|
|
@@ -6,6 +7,7 @@ import { HISTORY_RETENTION_DELAY_MS } from "../historyRetention.js";
|
|
|
6
7
|
import { resolveSourcePath, safeRelativePath } from "../files.js";
|
|
7
8
|
import { apiError, httpError } from "../http.js";
|
|
8
9
|
import { MAX_TEXT_PREVIEW_BYTES } from "../limits.js";
|
|
10
|
+
import { isHistoryTextFile } from "../../shared/historyFiles.js";
|
|
9
11
|
import { accessibleProject, canEdit } from "../projects.js";
|
|
10
12
|
import { commentsSummaryForProject, projectJson, projectTextSnapshot, reanchorProjectSnapshot, requireEditableProject, requireProjectOwnerPermission, tagsForProject, text, touchProject } from "./projectShared.js";
|
|
11
13
|
/** Register retained project version listing, inspection, restoration, and cleanup routes. */
|
|
@@ -112,7 +114,10 @@ export function registerProjectHistoryRoutes(app, context) {
|
|
|
112
114
|
return {
|
|
113
115
|
version,
|
|
114
116
|
settings: manifest.settings,
|
|
115
|
-
files:
|
|
117
|
+
files: [
|
|
118
|
+
...Object.entries(manifest.files).map(([filePath, file]) => ({ path: filePath, size: file.size })),
|
|
119
|
+
...version.changedPaths.filter((filePath) => !manifest.files[filePath]).map((filePath) => ({ path: filePath, size: 0, deleted: true }))
|
|
120
|
+
].sort((left, right) => left.path.localeCompare(right.path))
|
|
116
121
|
};
|
|
117
122
|
});
|
|
118
123
|
app.get("/api/projects/:id/history/:versionId/file", async (request, reply) => {
|
|
@@ -125,19 +130,41 @@ export function registerProjectHistoryRoutes(app, context) {
|
|
|
125
130
|
const query = request.query;
|
|
126
131
|
const filePath = safeRelativePath(query.path ?? "");
|
|
127
132
|
return await projectMutations.runConsistentRead(id, () => {
|
|
128
|
-
const
|
|
133
|
+
const manifest = history.manifest(id, versionId);
|
|
134
|
+
const version = history.version(versionId, id);
|
|
135
|
+
if (!manifest || !version)
|
|
136
|
+
return apiError(reply, 404, "HISTORY_VERSION_NOT_FOUND");
|
|
137
|
+
if (!manifest.files[filePath] && !version.changedPaths.includes(filePath))
|
|
138
|
+
return apiError(reply, 404, "HISTORY_FILE_NOT_FOUND");
|
|
139
|
+
// Never decode attachment bytes as UTF-8, including direct API calls.
|
|
140
|
+
if (!isHistoryTextFile(filePath))
|
|
141
|
+
return apiError(reply, 415, "HISTORY_FILE_PREVIEW_UNSUPPORTED", { path: filePath });
|
|
142
|
+
const historical = manifest.files[filePath] ? history.readTextFile(id, versionId, filePath) : "";
|
|
129
143
|
if (historical === null)
|
|
130
144
|
return apiError(reply, 415, "HISTORY_FILE_PREVIEW_UNSUPPORTED", { path: filePath });
|
|
131
145
|
let comparison = "";
|
|
132
146
|
const previousVersion = query.against === "__previous__" ? history.previousVersion(id, versionId) : null;
|
|
133
147
|
const against = query.against === "__previous__" ? previousVersion?.id ?? "__none__" : query.against;
|
|
134
148
|
if (query.against) {
|
|
135
|
-
|
|
149
|
+
const comparisonManifest = history.manifest(id, against);
|
|
150
|
+
if (against !== "__none__" && !comparisonManifest)
|
|
151
|
+
return apiError(reply, 404, "HISTORY_VERSION_NOT_FOUND");
|
|
152
|
+
if (comparisonManifest?.files[filePath]) {
|
|
153
|
+
const text = history.readTextFile(id, against, filePath);
|
|
154
|
+
if (text === null)
|
|
155
|
+
return apiError(reply, 415, "HISTORY_FILE_PREVIEW_UNSUPPORTED", { path: filePath });
|
|
156
|
+
comparison = text;
|
|
157
|
+
}
|
|
136
158
|
}
|
|
137
159
|
else {
|
|
138
160
|
const current = resolveSourcePath(config, id, filePath);
|
|
139
|
-
if (fs.existsSync(current) && fs.statSync(current).isFile()
|
|
140
|
-
|
|
161
|
+
if (fs.existsSync(current) && fs.statSync(current).isFile()) {
|
|
162
|
+
if (fs.statSync(current).size > MAX_TEXT_PREVIEW_BYTES)
|
|
163
|
+
return apiError(reply, 415, "HISTORY_FILE_PREVIEW_UNSUPPORTED", { path: filePath });
|
|
164
|
+
const bytes = fs.readFileSync(current);
|
|
165
|
+
if (bytes.includes(0) || !isUtf8(bytes))
|
|
166
|
+
return apiError(reply, 415, "HISTORY_FILE_PREVIEW_UNSUPPORTED", { path: filePath });
|
|
167
|
+
comparison = bytes.toString("utf8");
|
|
141
168
|
}
|
|
142
169
|
}
|
|
143
170
|
return { path: filePath, historical, comparison, against: against ?? "current", previousVersion };
|
|
@@ -53,11 +53,6 @@ export function registerProjectMemberRoutes(app, context) {
|
|
|
53
53
|
.run(id, user.id, changedAt);
|
|
54
54
|
db.prepare("UPDATE projects SET owner_id = ?, last_modified_by = ?, updated_at = ? WHERE id = ?")
|
|
55
55
|
.run(currentTarget.id, user.id, changedAt, id);
|
|
56
|
-
// A project token belongs to the account that supplied it. Preserve the
|
|
57
|
-
// local repository/remote metadata, but require the new owner to add
|
|
58
|
-
// their own credential before the next GitHub operation.
|
|
59
|
-
db.prepare("UPDATE project_git_settings SET token_ciphertext = NULL, github_login = NULL, updated_at = ? WHERE project_id = ?")
|
|
60
|
-
.run(changedAt, id);
|
|
61
56
|
db.exec("COMMIT");
|
|
62
57
|
}
|
|
63
58
|
catch (error) {
|
|
@@ -358,6 +358,16 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
358
358
|
WHERE project_id = ? ORDER BY word COLLATE NOCASE`).all(id);
|
|
359
359
|
return { words: rows.map((row) => row.word) };
|
|
360
360
|
});
|
|
361
|
+
app.get("/api/projects/:id/spellcheck/status", async (request, reply) => {
|
|
362
|
+
const user = requireUser(request, reply, db);
|
|
363
|
+
if (!user)
|
|
364
|
+
return;
|
|
365
|
+
const { id } = request.params;
|
|
366
|
+
if (!accessibleProject(db, id, user))
|
|
367
|
+
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
368
|
+
reply.header("Cache-Control", "no-store");
|
|
369
|
+
return { available: await harper.isAvailable() };
|
|
370
|
+
});
|
|
361
371
|
app.post("/api/projects/:id/spellcheck", async (request, reply) => {
|
|
362
372
|
const user = requireUser(request, reply, db);
|
|
363
373
|
if (!user)
|
|
@@ -121,8 +121,6 @@ export function registerUserManagementRoutes(app, context) {
|
|
|
121
121
|
db.prepare("DELETE FROM projects WHERE owner_id = ?").run(id);
|
|
122
122
|
}
|
|
123
123
|
else {
|
|
124
|
-
db.prepare("UPDATE project_git_settings SET token_ciphertext = NULL, github_login = NULL, updated_at = ? WHERE project_id IN (SELECT id FROM projects WHERE owner_id = ?)")
|
|
125
|
-
.run(now(), id);
|
|
126
124
|
db.prepare("DELETE FROM project_members WHERE user_id = ? AND project_id IN (SELECT id FROM projects WHERE owner_id = ?)")
|
|
127
125
|
.run(admin.id, id);
|
|
128
126
|
db.prepare("UPDATE projects SET owner_id = ?, last_modified_by = ?, updated_at = ? WHERE owner_id = ?")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** File types for which a source-level snapshot diff is meaningful. */
|
|
2
|
+
export function isHistoryTextFile(filePath) {
|
|
3
|
+
const name = filePath.split("/").at(-1) ?? "";
|
|
4
|
+
return /\.(?:tex|bib|bst|sty|cls|txt|md|rst|dat|csv|tsv|json|ya?ml|toml|xml|html?|css|[cm]?js|tsx?|py|sh|r|log|bbl|aux|idx|ind|gls)$/i.test(name)
|
|
5
|
+
|| /^(?:\.?latexmkrc|readme|licen[cs]e|makefile)$/i.test(name);
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "texlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "A lightweight local-first collaborative LaTeX workspace",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
"@lezer/highlight": "^1.2.3",
|
|
81
81
|
"@lezer/lr": "^1.4.10",
|
|
82
82
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
83
|
+
"@radix-ui/react-dropdown-menu": "^2.1.25",
|
|
84
|
+
"@radix-ui/react-popover": "^1.2.0",
|
|
83
85
|
"@replit/codemirror-vim": "^6.4.0",
|
|
84
86
|
"better-sqlite3": "^13.0.2",
|
|
85
87
|
"bibtex-tidy": "1.14.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as Y,r as n,j as e}from"./react-BVUjbTTn.js";import{c as m,M as D,d as _,C as ee,g as u,h}from"./index-CqBmQAzW.js";import{U as se,T as R,X as O}from"./x-Cspw_pa4.js";import{U as $}from"./user-plus-gFwY-NA3.js";import{S as ae}from"./Dashboard-DRt4rjKP.js";import{K as B}from"./key-round-BEnQo7IM.js";import"./hard-drive-CkIPwUSV.js";import"./sigma-B-wjepsY.js";import"./tags-BSA08jK3.js";const re=m("Dices",[["rect",{width:"12",height:"12",x:"2",y:"10",rx:"2",ry:"2",key:"6agr2n"}],["path",{d:"m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6",key:"1o487t"}],["path",{d:"M6 18h.01",key:"uhywen"}],["path",{d:"M10 14h.01",key:"ssrbsk"}],["path",{d:"M15 6h.01",key:"cblpky"}],["path",{d:"M18 9h.01",key:"2061c0"}]]);const ne=m("FolderCheck",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}],["path",{d:"m9 13 2 2 4-4",key:"6343dt"}]]);const te=m("FolderX",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}],["path",{d:"m9.5 10.5 5 5",key:"ra9qjz"}],["path",{d:"m14.5 10.5-5 5",key:"l2rkpq"}]]);const ie=m("ShieldOff",[["path",{d:"m2 2 20 20",key:"1ooewy"}],["path",{d:"M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71",key:"1jlk70"}],["path",{d:"M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264",key:"18rp1v"}]]);const oe=m("UserCheck",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["polyline",{points:"16 11 18 13 22 9",key:"1pwet4"}]]);const le=m("UserX",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["line",{x1:"17",x2:"22",y1:"8",y2:"13",key:"3nzzx3"}],["line",{x1:"22",x2:"17",y1:"8",y2:"13",key:"1swrse"}]]);function de(p=10){const t="ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%",a=crypto.getRandomValues(new Uint8Array(p));return Array.from(a,S=>t[S%t.length]).join("")}function be({currentUser:p,minPasswordLength:t}){const{t:a}=Y(),[S,J]=n.useState([]),[E,d]=n.useState(""),[V,C]=n.useState(!1),[r,o]=n.useState({username:"",displayName:"",password:"",role:"user",canCreateProjects:!1}),[k,g]=n.useState(null),[j,P]=n.useState(""),[x,w]=n.useState(null),[z,N]=n.useState(!1),[i,F]=n.useState(!1),[U,y]=n.useState(""),[l,f]=n.useState(null),[A,H]=n.useState(!1),[X,c]=n.useState(""),b=()=>u("/api/admin/users").then(({users:s})=>J(s)).catch(s=>d(h(s)));n.useEffect(()=>{b()},[]);const q=async()=>{if(r.password.length<t)return d(a("auth.passwordMinimum",{count:t}));try{await u("/api/admin/users",{method:"POST",body:JSON.stringify({username:r.username,displayName:r.displayName||r.username,password:r.password,role:r.role,canCreateProjects:r.canCreateProjects})}),C(!1),o({username:"",displayName:"",password:"",role:"user",canCreateProjects:!1}),await b()}catch(s){d(h(s))}},M=async(s,v)=>{try{await u(`/api/admin/users/${s.id}`,{method:"PATCH",body:JSON.stringify(v)}),await b()}catch(T){d(h(T))}},K=async s=>{if(!s.disabled){f({target:s,setting:"disable"}),c("");return}await M(s,{disabled:!1})},Z=async s=>{if(s.role==="admin"){f({target:s,setting:"demote"}),c("");return}await M(s,{role:"admin"})},G=async s=>{if(s.canCreateProjects){f({target:s,setting:"denyProjectCreation"}),c("");return}await M(s,{canCreateProjects:!0})},I=async()=>{if(!l)return;const{target:s,setting:v}=l,T=v==="disable"?{disabled:!0}:v==="demote"?{role:"user"}:{canCreateProjects:!1};H(!0),c("");try{await u(`/api/admin/users/${s.id}`,{method:"PATCH",body:JSON.stringify(T)}),await b(),f(null)}catch(W){c(h(W))}finally{H(!1)}},L=async()=>{if(!(!k||!j)){if(j.length<t)return d(a("auth.passwordMinimum",{count:t}));try{await u(`/api/admin/users/${k.id}`,{method:"PATCH",body:JSON.stringify({password:j})}),g(null),P("")}catch(s){d(h(s))}}},Q=async()=>{if(!(!x||i)){F(!0),y("");try{await u(`/api/admin/users/${x.id}`,{method:"DELETE",body:JSON.stringify({deleteProjects:z})}),w(null),N(!1),await b()}catch(s){y(h(s))}finally{F(!1)}}};return e.jsxs("main",{className:"dashboard",children:[e.jsxs("div",{className:"section-title",children:[e.jsxs("div",{children:[e.jsxs("h1",{children:[e.jsx(se,{"aria-hidden":!0,size:25}),a("users.manage")]}),e.jsx("p",{className:"muted",children:a("users.onlyAdmin")})]}),e.jsxs("button",{className:"primary icon-button",onClick:()=>C(!0),children:[e.jsx($,{"aria-hidden":!0,size:15}),a("users.add")]})]}),E&&e.jsx("p",{className:"error",children:E}),e.jsx("div",{className:"table-card",children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:a("common.user")}),e.jsx("th",{children:a("users.role")}),e.jsx("th",{children:a("users.createProjects")}),e.jsx("th",{children:a("users.ownedProjects")}),e.jsx("th",{children:a("users.status")}),e.jsx("th",{children:a("users.actions")})]})}),e.jsx("tbody",{children:S.map(s=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("strong",{children:s.displayName}),e.jsxs("small",{children:["@",s.username]})]}),e.jsx("td",{children:s.role==="admin"?a("common.admin"):a("common.user")}),e.jsx("td",{children:s.canCreateProjects?a("users.allow"):a("users.deny")}),e.jsx("td",{children:s.ownedProjects}),e.jsx("td",{children:s.disabled?a("common.disabled"):a("common.normal")}),e.jsxs("td",{children:[e.jsxs("button",{className:"icon-button",disabled:s.id===p.id,onClick:()=>K(s),children:[s.disabled?e.jsx(oe,{"aria-hidden":!0,size:13}):e.jsx(le,{"aria-hidden":!0,size:13}),s.disabled?a("users.enable"):a("users.disable")]}),e.jsxs("button",{className:"icon-button",disabled:s.id===p.id,onClick:()=>Z(s),children:[s.role==="admin"?e.jsx(ie,{"aria-hidden":!0,size:13}):e.jsx(ae,{"aria-hidden":!0,size:13}),s.role==="admin"?a("users.demote"):a("users.promote")]}),e.jsxs("button",{className:"icon-button",disabled:s.role==="admin",onClick:()=>G(s),children:[s.canCreateProjects?e.jsx(te,{"aria-hidden":!0,size:13}):e.jsx(ne,{"aria-hidden":!0,size:13}),s.canCreateProjects?a("users.denyCreate"):a("users.allowCreate")]}),e.jsxs("button",{className:"icon-button",onClick:()=>{g(s),P("")},children:[e.jsx(B,{"aria-hidden":!0,size:13}),a("users.resetPassword")]}),e.jsxs("button",{className:"icon-button danger-text",disabled:s.id===p.id,onClick:()=>{w(s),N(!1),y("")},children:[e.jsx(R,{"aria-hidden":!0,size:13}),a("common.delete")]})]})]},s.id))})]})}),e.jsx(D,{open:V,title:a("users.add"),description:a("users.addDescription"),onOpenChange:C,footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",onClick:()=>C(!1),children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"primary icon-button",disabled:r.password.length<t,onClick:()=>{q()},children:[e.jsx($,{"aria-hidden":!0,size:14}),a("users.createUser")]})]}),children:e.jsxs("div",{className:"form-stack",children:[e.jsxs("label",{className:"form-field",children:[a("auth.username"),e.jsx("input",{value:r.username,onChange:s=>o({...r,username:s.target.value})})]}),e.jsxs("label",{className:"form-field",children:[a("users.displayName"),e.jsx("input",{value:r.displayName,onChange:s=>o({...r,displayName:s.target.value})})]}),e.jsxs("label",{className:"form-field",children:[a("users.initialPassword"),e.jsxs("span",{className:"password-generator",children:[e.jsx("input",{minLength:t,autoComplete:"new-password",value:r.password,onChange:s=>o({...r,password:s.target.value})}),e.jsxs("button",{type:"button",title:a("users.generatePassword"),onClick:()=>o({...r,password:de()}),children:[e.jsx(re,{size:15}),a("users.randomPassword")]})]}),e.jsx("small",{className:"field-hint",children:a("auth.passwordMinimum",{count:t})})]}),e.jsxs("label",{className:"form-field",children:[a("users.role"),e.jsxs("select",{value:r.role,onChange:s=>o({...r,role:s.target.value}),children:[e.jsx("option",{value:"user",children:a("common.user")}),e.jsx("option",{value:"admin",children:a("common.admin")})]})]}),e.jsxs("label",{className:"checkbox-field",children:[e.jsx("input",{type:"checkbox",checked:r.canCreateProjects||r.role==="admin",disabled:r.role==="admin",onChange:s=>o({...r,canCreateProjects:s.target.checked})})," ",a("users.allowCreate")]})]})}),e.jsx(D,{open:!!k,title:a("users.resetPassword"),description:a("users.resetDescription",{username:k?.username??""}),onOpenChange:s=>{s||g(null)},footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",onClick:()=>g(null),children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"primary icon-button",disabled:j.length<t,onClick:()=>{L()},children:[e.jsx(B,{"aria-hidden":!0,size:14}),a("users.reset")]})]}),children:e.jsxs("label",{className:"form-field",children:[a("auth.newPassword"),e.jsx("input",{autoFocus:!0,type:"password",minLength:t,autoComplete:"new-password",value:j,onChange:s=>P(s.target.value)}),e.jsx("small",{className:"field-hint",children:a("auth.passwordMinimum",{count:t})})]})}),e.jsx(D,{open:!!x,title:a("users.deleteTitle"),description:a("users.deleteDescription",{username:x?.username??""}),onOpenChange:s=>{!s&&!i&&(w(null),y(""))},footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",disabled:i,onClick:()=>{w(null),y("")},children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"danger icon-button",disabled:i,"aria-busy":i,onClick:()=>{Q()},children:[i?e.jsx(_,{className:"spin","aria-hidden":!0,size:14}):e.jsx(R,{"aria-hidden":!0,size:14}),a(i?"common.loading":"users.deleteTitle")]})]}),children:e.jsxs(e.Fragment,{children:[U&&e.jsx("p",{className:"error dialog-error",children:U}),e.jsxs("fieldset",{className:"choice-group",disabled:i,children:[e.jsx("legend",{children:a("users.ownedChoice",{count:x?.ownedProjects??0})}),e.jsxs("label",{children:[e.jsx("input",{type:"radio",checked:!z,onChange:()=>N(!1)})," ",a("users.transferProjects")]}),e.jsxs("label",{children:[e.jsx("input",{type:"radio",checked:z,onChange:()=>N(!0)})," ",a("users.deleteProjects")]})]})]})}),e.jsx(ee,{open:!!l,title:a(`userConfirmations.${l?.setting??"disable"}Title`),description:a(`userConfirmations.${l?.setting??"disable"}Description`,{username:l?.target.username??""}),confirmLabel:a(`userConfirmations.${l?.setting??"disable"}Confirm`),danger:!0,busy:A,error:X,onCancel:()=>{A||(f(null),c(""))},onConfirm:()=>{I()}})]})}export{be as AdminUsers};
|