750d74a2af
- 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.
111 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|