web: moderate accounts, accept 12MB uploads, put SAVE under CREATE
- /admin User account rows gain BLOCK/UNBLOCK, REMOVE/RESTORE and DELETE. Blocked = cannot sign in (sessions swept), removed = hidden from the strip and cannot sign in, both reversible; DELETE drops the account with its photos and recipes and unlinks the files. An allowlisted account is never a target, so an admin cannot moderate or delete itself. - Photo uploads move from a 3MB API cap / 4m nginx cap to 12MB / 16m, and the browser shrinks an oversized still before sending it (2048px JPEG, avatars 512px) so the declared type still matches the sniffed bytes. - The studio SAVE leaves the top bar and sits under the CREATE RECIPES tab, labelled SAVE RECIPES.
This commit is contained in:
@@ -116,6 +116,22 @@ export function Admin() {
|
||||
setTab('pictures');
|
||||
};
|
||||
|
||||
// Block/unblock and remove/restore share one route; the list is reloaded
|
||||
// because either flag also changes what the built-in viewer would show.
|
||||
const setUserState = (id: number, patch: { blocked?: boolean; removed?: boolean }) =>
|
||||
run(async () => {
|
||||
await api.adminSetUser(id, patch);
|
||||
await refreshPhotos();
|
||||
});
|
||||
|
||||
const deleteUser = (u: AdminUser) => {
|
||||
if (!window.confirm(t('adm.userDeleteConfirm', { email: u.email }))) return;
|
||||
return run(async () => {
|
||||
await api.adminDeleteUser(u.id);
|
||||
await refreshPhotos();
|
||||
});
|
||||
};
|
||||
|
||||
const shown = owner ? photos.filter((p) => p.email === owner) : photos;
|
||||
|
||||
return (
|
||||
@@ -270,7 +286,11 @@ export function Admin() {
|
||||
<span className="adm-face adm-face-empty">{(u.email[0] ?? '?').toUpperCase()}</span>
|
||||
)}
|
||||
</td>
|
||||
<td>{u.email.split('@')[0]}</td>
|
||||
<td>
|
||||
{u.email.split('@')[0]}
|
||||
{u.blocked ? <span className="adm-badge adm-badge-warn">{t('adm.userBlocked')}</span> : null}
|
||||
{u.removed ? <span className="adm-badge adm-badge-warn">{t('adm.userRemoved')}</span> : null}
|
||||
</td>
|
||||
<td>{u.email}</td>
|
||||
<td>
|
||||
<span className={`adm-badge${u.admin ? ' on' : ''}`}>
|
||||
@@ -288,6 +308,40 @@ export function Admin() {
|
||||
>
|
||||
{t('adm.viewPhotos')}
|
||||
</button>
|
||||
{/* An admin account is the API's own privilege
|
||||
source, so it is never a moderation target and
|
||||
offers no buttons here. */}
|
||||
{u.admin ? null : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
data-key={`adm-user-block-${u.id}`}
|
||||
disabled={busy}
|
||||
onClick={() => void setUserState(u.id, { blocked: !u.blocked })}
|
||||
>
|
||||
{u.blocked ? t('adm.userUnblock') : t('adm.userBlock')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
data-key={`adm-user-remove-${u.id}`}
|
||||
disabled={busy}
|
||||
onClick={() => void setUserState(u.id, { removed: !u.removed })}
|
||||
>
|
||||
{u.removed ? t('adm.userRestore') : t('adm.userRemove')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn adm-danger"
|
||||
data-key={`adm-user-delete-${u.id}`}
|
||||
disabled={busy}
|
||||
onClick={() => void deleteUser(u)}
|
||||
>
|
||||
{t('adm.userDelete')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user