Migrate omsorgapp to browser SPA, add Docker/CI build setup
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 3s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Failing after 4s
Docker-Images bauen und veröffentlichen / build (VITE_OMSORG_CORE_URL=${{ vars.OMSORG_CORE_PUBLIC_URL }}, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 3s
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 3s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Failing after 4s
Docker-Images bauen und veröffentlichen / build (VITE_OMSORG_CORE_URL=${{ vars.OMSORG_CORE_PUBLIC_URL }}, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 3s
- omsorgapp: drop Electron, run as a plain Vite/React browser app; refresh token moves to an HttpOnly cookie (omsorgCore), CORS added for the new browser origin, document download/preview switched to Blob-based browser APIs. - Add Dockerfiles for omsorgCore, omsorgapp, and omsorgWeb, a docker-compose.yml wiring Postgres/MySQL/all three apps together, and a Gitea Actions workflow that builds and pushes images to the repo's container registry on push to main and on version tags. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e9e96a57dc
commit
598dfcd38a
@@ -0,0 +1,128 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { FormActions } from "../employees/EmployeeForm";
|
||||
import { useAuth } from "../../app/AuthContext";
|
||||
import ModalPortal from "../../components/ui/ModalPortal";
|
||||
|
||||
const PIN_VALIDITY_OPTIONS = [1, 3, 7, 14];
|
||||
const DEFAULT_PIN_VALIDITY_DAYS = 7;
|
||||
|
||||
function errorMessage(result) {
|
||||
if (result.status === 403) {
|
||||
return "Keine Berechtigung, Passwörter zurückzusetzen.";
|
||||
}
|
||||
if (result.status === 404) {
|
||||
return "Nutzer nicht gefunden.";
|
||||
}
|
||||
if (result.status === 400) {
|
||||
return typeof result.data === "string" ? result.data : "Eingaben prüfen.";
|
||||
}
|
||||
return "Passwort konnte nicht zurückgesetzt werden.";
|
||||
}
|
||||
|
||||
export default function ResetUserPasswordDialog({ user, onClose, onDone }) {
|
||||
const { passwordMinLength } = useAuth();
|
||||
const [mode, setMode] = useState("Invite");
|
||||
const [pinValidityDays, setPinValidityDays] = useState(DEFAULT_PIN_VALIDITY_DAYS);
|
||||
const [initialPassword, setInitialPassword] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (mode === "Direct" && initialPassword.trim().length < passwordMinLength) {
|
||||
setError(`Initiales Passwort muss mindestens ${passwordMinLength} Zeichen lang sein.`);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
|
||||
const result = await window.omsorg.users.resetPassword(user.id, {
|
||||
mode,
|
||||
initialPassword: mode === "Direct" ? initialPassword : null,
|
||||
pinValidityDays: mode === "Invite" ? pinValidityDays : null,
|
||||
});
|
||||
|
||||
setIsSaving(false);
|
||||
|
||||
if (!result.ok) {
|
||||
setError(errorMessage(result));
|
||||
return;
|
||||
}
|
||||
|
||||
onDone();
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalPortal>
|
||||
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="Passwort zurücksetzen">
|
||||
<div className="modal-panel">
|
||||
<h2>Passwort für {user.username} zurücksetzen</h2>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<fieldset className="form-field">
|
||||
<legend>Zugang</legend>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="reset-mode"
|
||||
value="Invite"
|
||||
checked={mode === "Invite"}
|
||||
onChange={() => setMode("Invite")}
|
||||
/>
|
||||
Einladung per Mail (Nutzer setzt eigenes Passwort)
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="reset-mode"
|
||||
value="Direct"
|
||||
checked={mode === "Direct"}
|
||||
onChange={() => setMode("Direct")}
|
||||
/>
|
||||
Passwort direkt vergeben (muss beim nächsten Login geändert werden)
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
{mode === "Invite" && (
|
||||
<label className="form-field">
|
||||
<span>PIN gültig für</span>
|
||||
<select
|
||||
value={pinValidityDays}
|
||||
onChange={(event) => setPinValidityDays(Number(event.target.value))}
|
||||
>
|
||||
{PIN_VALIDITY_OPTIONS.map((days) => (
|
||||
<option key={days} value={days}>
|
||||
{days} {days === 1 ? "Tag" : "Tage"}
|
||||
{days === DEFAULT_PIN_VALIDITY_DAYS ? " (Standard)" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{mode === "Direct" && (
|
||||
<label className="form-field">
|
||||
<span>Neues initiales Passwort</span>
|
||||
<input
|
||||
type="text"
|
||||
value={initialPassword}
|
||||
onChange={(event) => setInitialPassword(event.target.value)}
|
||||
placeholder={`Mindestens ${passwordMinLength} Zeichen`}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{error && <p className="login-error form-error">{error}</p>}
|
||||
|
||||
<FormActions onCancel={onClose} isSaving={isSaving} submitLabel="Zurücksetzen" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user