chore: v2.8.15 — Oh My Pi login sharing, omp --allow-home, Markdown Preview default, first-login password

- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as
  the host on loopback; a member's omp pane reaches it as a scoped client keyed
  by its pane token (clients file = live omp panes only), runs on
  --model agenthub/<model>, never holds a credential.
- omp catalog: --allow-home, so a workspace at the account's home keeps its
  sessions there (they were recorded under a temp dir and dropped).
- Settings → Appearance → Open Markdown as: Preview (default) or Raw, for
  Files/Changes, the config viewer and a chat's file panel.
- First-login password change and password rules, avatar URLs, Artifacts
  "Mine" includes installed shared skills/plugins, a member's own hook pages.
This commit is contained in:
2026-10-22 10:04:30 +07:00
parent 8a6dc6a29e
commit 750d74a2af
66 changed files with 3021 additions and 696 deletions
+27 -8
View File
@@ -2,7 +2,9 @@
* @file UserEditDialog.tsx
* @description The Users page's ONE account editor (admin only): profile
* (display name, email), access (role, active/disabled — neither for your own
* account), a new password (typed or generated, copyable), the workspaces the
* account), a new password (typed or generated, copyable — someone else's
* may be anything and makes them choose their own at next sign-in; your own
* must pass every rule of `lib/passwordRules.ts`), the workspaces the
* account owns (remove, add from every workspace), and "sign out everywhere".
* Every change is staged and sent as ONE `PATCH /api/users/:id` carrying only
* what changed, confirmed with the admin's OWN password (`currentPassword`) —
@@ -20,6 +22,8 @@ import type { Lane } from "../../lib/types";
import { fmtCost, formatDateTime } from "../../lib/format";
import { copyText } from "../../lib/clipboard";
import { isSubmitEnter } from "../../lib/keys";
import { passwordProblems } from "../../lib/passwordRules";
import { PasswordRules } from "./PasswordRules";
import { useDialog } from "../../hooks/useDialog";
import { Switch } from "../Switch";
import { Checkbox } from "../Checkbox";
@@ -34,11 +38,15 @@ export const ROLE_BADGE: Record<Role, string> = {
};
/** A random password of `n` characters, no look-alikes (0/O, 1/l/I). */
export function generatePassword(n = 16): string {
export function generatePassword(n = 16, username?: string): string {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789-_!@#%";
const bytes = new Uint32Array(n);
crypto.getRandomValues(bytes);
return Array.from(bytes, (b) => chars[b % chars.length]).join("");
// Drawn again until it passes every rule, so it also fits an own password.
for (;;) {
crypto.getRandomValues(bytes);
const out = Array.from(bytes, (b) => chars[b % chars.length]).join("");
if (passwordProblems(out, username).length === 0) return out;
}
}
type OwnedWorkspace = { id: number; title: string; cwd: string };
@@ -110,8 +118,11 @@ export function UserEditDialog({
if (removed.size) patch.removeWorkspaces = [...removed];
if (signOut) patch.signOut = true;
const changed = Object.keys(patch).length > 0;
// No minimum length (server/lib/auth.js) — a blank field keeps the password.
const ready = changed && current.length > 0 && !busy;
// A blank field keeps the password. Someone else's may be anything (they
// choose their own at next sign-in); your own must pass every rule.
const weakOwn =
self && password.length > 0 && passwordProblems(password, user.username).length > 0;
const ready = changed && current.length > 0 && !weakOwn && !busy;
const submit = async () => {
if (!ready) return;
@@ -309,7 +320,10 @@ export function UserEditDialog({
</Section>
<Section title={t("edit.password")}>
<Field label={t("newPassword")} hint={t("edit.passwordHint")}>
<Field
label={t("newPassword")}
hint={self ? t("edit.ownPasswordHint") : t("edit.passwordHint")}
>
<div className="flex gap-2">
<input
type={showPassword ? "text" : "password"}
@@ -343,7 +357,7 @@ export function UserEditDialog({
type="button"
className="btn-ghost whitespace-nowrap text-xs"
onClick={() => {
setPassword(generatePassword());
setPassword(generatePassword(16, user.username));
setShowPassword(true);
}}
>
@@ -351,6 +365,11 @@ export function UserEditDialog({
</button>
</div>
</Field>
{self && password.length > 0 && (
<div className="mt-2">
<PasswordRules password={password} username={user.username} />
</div>
)}
</Section>
<Section title={t("workspaces")}>