Files
AgentHub/client/src/components/FirstPasswordPage.tsx
T
nntrivi2001 750d74a2af 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.
2026-10-22 10:04:30 +07:00

111 lines
3.7 KiB
TypeScript

/**
* @file FirstPasswordPage.tsx
* @description The screen an account signed in with a password an admin GAVE
* (created or reset on the Users page — `mustChangePassword`) sees instead of
* the app: the current password, a new one that must pass every rule (the
* `PasswordRules` checklist ticks them as they are typed) and the same again,
* sent to `POST /api/me/password`. On success the app opens; "Sign out"
* leaves. The server refuses every other request meanwhile (EPWCHANGE).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useState, type FormEvent } from "react";
import { useTranslation } from "react-i18next";
import { api, type AuthUser } from "../lib/api";
import { passwordProblems } from "../lib/passwordRules";
import { AuthCard } from "./AuthCard";
import { PasswordRules } from "./users/PasswordRules";
export function FirstPasswordPage({
user,
onDone,
onLogout,
}: {
user: AuthUser;
onDone: () => void;
onLogout: () => void;
}) {
const { t } = useTranslation();
const [current, setCurrent] = useState("");
const [next, setNext] = useState("");
const [again, setAgain] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const mismatch = again.length > 0 && again !== next;
const strong = passwordProblems(next, user.username).length === 0;
const ready = current.length > 0 && strong && again === next && !busy;
const submit = async (e: FormEvent) => {
e.preventDefault();
if (!ready) return;
setBusy(true);
setError(null);
try {
await api.me.changePassword(current, next);
onDone();
} catch (err) {
const code = (err as { code?: string }).code;
setError(
code && t(`users:errors.${code}`, { defaultValue: "" })
? t(`users:errors.${code}`)
: (err as Error).message
);
} finally {
setBusy(false);
}
};
const field = (
label: string,
value: string,
set: (v: string) => void,
autoComplete: string,
autoFocus = false
) => (
<label className="block space-y-1.5 text-sm font-medium text-fg-primary">
<span>{label}</span>
<input
type="password"
className="input h-11 w-full px-4"
autoComplete={autoComplete}
autoFocus={autoFocus}
required
value={value}
onChange={(e) => set(e.target.value)}
/>
</label>
);
return (
<AuthCard
title={t("auth.mustChangeTitle")}
subtitle={t("auth.mustChangeSubtitle")}
onSubmit={submit}
>
{field(t("auth.currentPassword"), current, setCurrent, "current-password", true)}
{field(t("auth.newPassword"), next, setNext, "new-password")}
<PasswordRules password={next} username={user.username} />
{field(t("auth.confirmPassword"), again, setAgain, "new-password")}
{(mismatch || error) && (
<p role="alert" className="text-sm text-status-danger">
{error ?? t("auth.mismatch")}
</p>
)}
<button
type="submit"
className="h-11 w-full rounded-xl bg-fg-primary text-sm font-medium text-surface-0 shadow-sm transition-opacity hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!ready}
>
{busy ? t("auth.saving") : t("auth.mustChangeSubmit")}
</button>
<button
type="button"
className="block w-full text-center text-sm text-fg-secondary hover:text-fg-primary"
onClick={onLogout}
>
{t("auth.logout")}
</button>
</AuthCard>
);
}