mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 18:38:11 +02:00
Added groups and restricted access.
This commit is contained in:
1 parent
8aa223bc7b
commit
9a5b9530a3
17 files changed
+1406
-66
No files matched your search
@@ -1,8 +1,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
ChangePassword,
|
||||
CreateGroupApi,
|
||||
CreateUserAccount,
|
||||
DeleteGroupApi,
|
||||
DeleteTheme,
|
||||
FetchGroups,
|
||||
FetchLocks,
|
||||
FetchProfileColourOptions,
|
||||
FetchSettings,
|
||||
FetchTheme,
|
||||
@@ -10,9 +15,12 @@ import {
|
||||
FetchUsers,
|
||||
GetAvatarUrl,
|
||||
Logout,
|
||||
RemoveLock,
|
||||
SaveSettings,
|
||||
SaveTheme,
|
||||
SetGroupMembersApi,
|
||||
UpdateProfileColour,
|
||||
UpdateUsername,
|
||||
UploadAvatar,
|
||||
} from "../Api.js";
|
||||
import Switch from "./Switch.jsx";
|
||||
@@ -46,12 +54,32 @@ export default function SettingsPanel({
|
||||
const [ProfileError, SetProfileError] = useState(null);
|
||||
const [AvatarNonce, SetAvatarNonce] = useState(0);
|
||||
|
||||
const [UsernameField, SetUsernameField] = useState("");
|
||||
const [UsernameError, SetUsernameError] = useState(null);
|
||||
const [CurrentPasswordField, SetCurrentPasswordField] = useState("");
|
||||
const [NewPasswordField, SetNewPasswordField] = useState("");
|
||||
const [PasswordError, SetPasswordError] = useState(null);
|
||||
const [PasswordChanged, SetPasswordChanged] = useState(false);
|
||||
|
||||
const [Locks, SetLocksList] = useState(null);
|
||||
const [LockError, SetLockError] = useState(null);
|
||||
|
||||
const [Groups, SetGroupsList] = useState(null);
|
||||
const [NewGroupName, SetNewGroupName] = useState("");
|
||||
const [GroupError, SetGroupError] = useState(null);
|
||||
const [ExpandedGroup, SetExpandedGroup] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
FetchSettings().then(SetSettings);
|
||||
FetchThemeNames().then(SetThemeNames);
|
||||
FetchProfileColourOptions().then(SetColourOptions);
|
||||
if (CurrentUser) {
|
||||
SetUsernameField(CurrentUser.Username);
|
||||
}
|
||||
if (CurrentUser && CurrentUser.IsAdmin) {
|
||||
FetchUsers().then(SetUsers);
|
||||
FetchLocks().then(SetLocksList);
|
||||
FetchGroups().then(SetGroupsList);
|
||||
}
|
||||
}, [CurrentUser]);
|
||||
|
||||
@@ -181,6 +209,86 @@ export default function SettingsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleChangeUsername() {
|
||||
SetUsernameError(null);
|
||||
|
||||
if (!UsernameField.trim() || UsernameField.trim() === CurrentUser.Username) return;
|
||||
|
||||
try {
|
||||
const Updated = await UpdateUsername(UsernameField.trim());
|
||||
OnProfileUpdated(Updated);
|
||||
} catch (Error) {
|
||||
SetUsernameError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleChangePassword() {
|
||||
SetPasswordError(null);
|
||||
SetPasswordChanged(false);
|
||||
|
||||
try {
|
||||
await ChangePassword(CurrentPasswordField, NewPasswordField);
|
||||
SetCurrentPasswordField("");
|
||||
SetNewPasswordField("");
|
||||
SetPasswordChanged(true);
|
||||
} catch (Error) {
|
||||
SetPasswordError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleUnlock(Lock) {
|
||||
SetLockError(null);
|
||||
try {
|
||||
await RemoveLock(Lock.Root, Lock.Path);
|
||||
const Updated = await FetchLocks();
|
||||
SetLocksList(Updated);
|
||||
} catch (Error) {
|
||||
SetLockError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleCreateGroup() {
|
||||
SetGroupError(null);
|
||||
|
||||
try {
|
||||
await CreateGroupApi(NewGroupName.trim());
|
||||
const Updated = await FetchGroups();
|
||||
SetGroupsList(Updated);
|
||||
SetNewGroupName("");
|
||||
} catch (Error) {
|
||||
SetGroupError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleDeleteGroup(Name) {
|
||||
if (!window.confirm(`Delete group "${Name}"? This can't be undone.`)) return;
|
||||
|
||||
SetGroupError(null);
|
||||
try {
|
||||
await DeleteGroupApi(Name);
|
||||
const Updated = await FetchGroups();
|
||||
SetGroupsList(Updated);
|
||||
} catch (Error) {
|
||||
SetGroupError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleToggleGroupMember(Group, Username) {
|
||||
const CurrentMembers = Group.Members || [];
|
||||
const NewMembers = CurrentMembers.includes(Username)
|
||||
? CurrentMembers.filter((Member) => Member !== Username)
|
||||
: [...CurrentMembers, Username];
|
||||
|
||||
SetGroupError(null);
|
||||
try {
|
||||
await SetGroupMembersApi(Group.Name, NewMembers);
|
||||
const Updated = await FetchGroups();
|
||||
SetGroupsList(Updated);
|
||||
} catch (Error) {
|
||||
SetGroupError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SettingsOverlay" onClick={OnClose}>
|
||||
<div className="SettingsModal" onClick={(Event) => Event.stopPropagation()}>
|
||||
@@ -245,6 +353,16 @@ export default function SettingsPanel({
|
||||
<Switch Checked={Settings.ShowGitBlame} OnChange={(Event) => UpdateField("ShowGitBlame", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show avatars instead of names in blame</label>
|
||||
<Switch Checked={Settings.ShowBlameAvatars} OnChange={(Event) => UpdateField("ShowBlameAvatars", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show my edits in others' blame view</label>
|
||||
<Switch Checked={Settings.VisibleInBlame} OnChange={(Event) => UpdateField("VisibleInBlame", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Sticky edit/view/save toolbar</label>
|
||||
<Switch Checked={Settings.StickyToolbar} OnChange={(Event) => UpdateField("StickyToolbar", Event.target.checked)} />
|
||||
@@ -352,6 +470,42 @@ export default function SettingsPanel({
|
||||
</span>
|
||||
<button onClick={HandleLogout}>Log out</button>
|
||||
</div>
|
||||
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Username</label>
|
||||
<div className="SettingsRowInline">
|
||||
<input
|
||||
type="text"
|
||||
value={UsernameField}
|
||||
onChange={(Event) => SetUsernameField(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleChangeUsername}>Save</button>
|
||||
</div>
|
||||
{UsernameError && <div className="SettingsSaveError">{UsernameError}</div>}
|
||||
<div className="SettingsHint">
|
||||
Renaming logs out any other devices/browsers you're signed in on — this one
|
||||
stays signed in under the new name.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Change password</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Current password"
|
||||
value={CurrentPasswordField}
|
||||
onChange={(Event) => SetCurrentPasswordField(Event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
value={NewPasswordField}
|
||||
onChange={(Event) => SetNewPasswordField(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleChangePassword}>Update password</button>
|
||||
{PasswordChanged && <div className="SettingsHint">Password updated.</div>}
|
||||
{PasswordError && <div className="SettingsSaveError">{PasswordError}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
@@ -409,6 +563,86 @@ export default function SettingsPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Locked Directories (admin)</div>
|
||||
{Locks && Locks.length === 0 && (
|
||||
<div className="SettingsHint">
|
||||
Nothing locked yet — use the 🔒 button next to any directory to lock it.
|
||||
</div>
|
||||
)}
|
||||
{Locks &&
|
||||
Locks.map((Lock, Index) => (
|
||||
<div className="SettingsRowInline" key={`${Lock.Root}:${Lock.Path}:${Index}`}>
|
||||
<span className="SettingsLabel">
|
||||
{Lock.Root}/{Lock.Path} —{" "}
|
||||
{[...(Lock.Users || []), ...(Lock.Groups || []).map((G) => `@${G}`)].join(
|
||||
", "
|
||||
)}
|
||||
</span>
|
||||
<button onClick={() => HandleUnlock(Lock)}>Unlock</button>
|
||||
</div>
|
||||
))}
|
||||
{LockError && <div className="SettingsSaveError">{LockError}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Groups (admin)</div>
|
||||
{Groups &&
|
||||
Groups.map((Group) => (
|
||||
<div key={Group.Name} className="GroupRow">
|
||||
<div className="SettingsRowInline">
|
||||
<span className="SettingsLabel">
|
||||
{Group.Name} ({(Group.Members || []).length})
|
||||
</span>
|
||||
<div className="SettingsRowInline">
|
||||
<button
|
||||
onClick={() =>
|
||||
SetExpandedGroup(ExpandedGroup === Group.Name ? null : Group.Name)
|
||||
}
|
||||
>
|
||||
{ExpandedGroup === Group.Name ? "Hide" : "Members"}
|
||||
</button>
|
||||
{Group.Name !== "Admin" && (
|
||||
<button onClick={() => HandleDeleteGroup(Group.Name)}>Delete</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{ExpandedGroup === Group.Name && (
|
||||
<div className="AccessPickerList GroupMemberList">
|
||||
{(Users || []).map((User) => (
|
||||
<label className="AccessPickerRow" key={User.Username}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(Group.Members || []).includes(User.Username)}
|
||||
onChange={() => HandleToggleGroupMember(Group, User.Username)}
|
||||
/>
|
||||
<span
|
||||
className="ColourSwatchSmall"
|
||||
style={{ background: `var(--${User.Colour || "Purple"})` }}
|
||||
/>
|
||||
<span>{User.Username}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="SettingsRowInline">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="New group name"
|
||||
value={NewGroupName}
|
||||
onChange={(Event) => SetNewGroupName(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleCreateGroup}>Create group</button>
|
||||
</div>
|
||||
{GroupError && <div className="SettingsSaveError">{GroupError}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{SaveError && <div className="SettingsSaveError">{SaveError}</div>}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user