mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-13 18:28:14 +02:00
Added guests.
This commit is contained in:
1 parent
6743dd7626
commit
9736a55e00
10 files changed
+156
-40
No files matched your search
+39
-8
@@ -6,7 +6,7 @@ from typing import Any
|
||||
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
|
||||
from Groups import AdminGroupName, GetGroupsForUser, IsUserInGroup
|
||||
from Groups import AdminGroupName, GetGroupsForUser, GuestGroupName, IsUserInGroup
|
||||
from Users import GetUser
|
||||
|
||||
|
||||
@@ -14,6 +14,14 @@ ActiveSessions: dict[str, str] = {}
|
||||
|
||||
SessionCookieName = "SaraViewSession"
|
||||
|
||||
GuestUser: dict[str, Any] = {
|
||||
"Username": None,
|
||||
"IsAdmin": False,
|
||||
"IsGuest": True,
|
||||
"Groups": [GuestGroupName],
|
||||
"Colour": "Purple",
|
||||
}
|
||||
|
||||
|
||||
def CreateSession(Username: str) -> str:
|
||||
Token = secrets.token_urlsafe(32)
|
||||
@@ -25,6 +33,21 @@ def DeleteSession(Token: str) -> None:
|
||||
ActiveSessions.pop(Token, None)
|
||||
|
||||
|
||||
def _BuildUserRecord(Username: str, User: dict[str, Any]) -> dict[str, Any]:
|
||||
UserGroups = GetGroupsForUser(Username)
|
||||
|
||||
if GuestGroupName not in UserGroups:
|
||||
UserGroups = [*UserGroups, GuestGroupName]
|
||||
|
||||
return {
|
||||
"Username": Username,
|
||||
"IsAdmin": AdminGroupName in UserGroups,
|
||||
"IsGuest": False,
|
||||
"Groups": UserGroups,
|
||||
"Colour": User.get("Colour", "Purple"),
|
||||
}
|
||||
|
||||
|
||||
def GetCurrentUser(request: Request) -> dict[str, Any]:
|
||||
Token = request.cookies.get(SessionCookieName)
|
||||
Username = ActiveSessions.get(Token) if Token else None
|
||||
@@ -37,14 +60,22 @@ def GetCurrentUser(request: Request) -> dict[str, Any]:
|
||||
if User is None:
|
||||
raise HTTPException(status_code=401, detail="Session user no longer exists.")
|
||||
|
||||
UserGroups = GetGroupsForUser(Username)
|
||||
return _BuildUserRecord(Username, User)
|
||||
|
||||
return {
|
||||
"Username": Username,
|
||||
"IsAdmin": AdminGroupName in UserGroups,
|
||||
"Groups": UserGroups,
|
||||
"Colour": User.get("Colour", "Purple"),
|
||||
}
|
||||
|
||||
def GetCurrentUserOrGuest(request: Request) -> dict[str, Any]:
|
||||
Token = request.cookies.get(SessionCookieName)
|
||||
Username = ActiveSessions.get(Token) if Token else None
|
||||
|
||||
if Username is None:
|
||||
return dict(GuestUser)
|
||||
|
||||
User = GetUser(Username)
|
||||
|
||||
if User is None:
|
||||
return dict(GuestUser)
|
||||
|
||||
return _BuildUserRecord(Username, User)
|
||||
|
||||
|
||||
def RequireAdmin(CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ from urllib.parse import quote
|
||||
|
||||
from Core import ResolvePath
|
||||
from Explorer import ClassifyFile, GetDirectoryNoteInfo
|
||||
from Groups import GuestGroupName
|
||||
from Locks import IsPathAccessible
|
||||
from Portals import ResolvePortals
|
||||
from Preview import GetMimeType, ReadTextFile, TextFileTypes
|
||||
@@ -33,7 +34,7 @@ def _StripMarkdownForExcerpt(Text: str) -> str:
|
||||
def _DescribeTextFile(RootName: str, RelativePath: str, FilePath: Path, Fallback: str) -> str:
|
||||
try:
|
||||
Content = ReadTextFile(FilePath)
|
||||
Resolved = ResolvePortals(RootName, RelativePath, Content, "", [])
|
||||
Resolved = ResolvePortals(RootName, RelativePath, Content, "", [GuestGroupName])
|
||||
Excerpt = _StripMarkdownForExcerpt(Resolved)
|
||||
return Excerpt or Fallback
|
||||
except (OSError, UnicodeDecodeError):
|
||||
@@ -45,7 +46,7 @@ def _MetaTag(Property: str, Content: str) -> str:
|
||||
|
||||
|
||||
def BuildOgTags(RootName: str, RelativePath: str, IsDirectoryUrl: bool, BaseUrl: str) -> str | None:
|
||||
if not IsPathAccessible(RootName, RelativePath, "", []):
|
||||
if not IsPathAccessible(RootName, RelativePath, "", [GuestGroupName]):
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
+12
-5
@@ -10,6 +10,8 @@ from Settings import GetDataDirectory
|
||||
|
||||
|
||||
AdminGroupName = "Admin"
|
||||
GuestGroupName = "Guest"
|
||||
BuiltinGroupNames = (AdminGroupName, GuestGroupName)
|
||||
|
||||
|
||||
def GetGroupsPath():
|
||||
@@ -25,7 +27,7 @@ def LoadGroups() -> dict[str, list[str]]:
|
||||
SeededAdmins = [
|
||||
Username for Username, Data in LoadUsers().items() if Data.get("IsAdmin", False)
|
||||
]
|
||||
Groups = {AdminGroupName: SeededAdmins}
|
||||
Groups = {AdminGroupName: SeededAdmins, GuestGroupName: []}
|
||||
SaveGroups(Groups)
|
||||
return Groups
|
||||
|
||||
@@ -33,9 +35,14 @@ def LoadGroups() -> dict[str, list[str]]:
|
||||
Loaded = tomllib.load(GroupsFile)
|
||||
|
||||
Groups = Loaded.get("Groups", {})
|
||||
Changed = False
|
||||
|
||||
if AdminGroupName not in Groups:
|
||||
Groups[AdminGroupName] = []
|
||||
for BuiltinName in BuiltinGroupNames:
|
||||
if BuiltinName not in Groups:
|
||||
Groups[BuiltinName] = []
|
||||
Changed = True
|
||||
|
||||
if Changed:
|
||||
SaveGroups(Groups)
|
||||
|
||||
return Groups
|
||||
@@ -69,8 +76,8 @@ def CreateGroup(Name: str) -> None:
|
||||
|
||||
|
||||
def DeleteGroup(Name: str) -> None:
|
||||
if Name == AdminGroupName:
|
||||
raise ValueError("The Admin group can't be deleted.")
|
||||
if Name in BuiltinGroupNames:
|
||||
raise ValueError(f"The {Name} group can't be deleted.")
|
||||
|
||||
Groups = LoadGroups()
|
||||
|
||||
|
||||
+27
-13
@@ -13,7 +13,14 @@ from fastapi import Depends, FastAPI, File, HTTPException, Request, Response, Up
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, StreamingResponse
|
||||
|
||||
from Auth import CreateSession, DeleteSession, GetCurrentUser, RequireAdmin, SessionCookieName
|
||||
from Auth import (
|
||||
CreateSession,
|
||||
DeleteSession,
|
||||
GetCurrentUser,
|
||||
GetCurrentUserOrGuest,
|
||||
RequireAdmin,
|
||||
SessionCookieName,
|
||||
)
|
||||
from BookReader import ReadBookForFile
|
||||
from Blame import LoadBlame, RenameUserInBlame, UpdateBlameOnSave
|
||||
from Core import ResolvePath
|
||||
@@ -31,6 +38,7 @@ from Explorer import (
|
||||
from Groups import (
|
||||
CreateGroup,
|
||||
DeleteGroup,
|
||||
GuestGroupName,
|
||||
ListGroups,
|
||||
RenameUserInGroups,
|
||||
SetGroupMembers,
|
||||
@@ -46,6 +54,7 @@ from Locks import (
|
||||
from Portals import ResolvePortals
|
||||
from Preview import GetMimeType, ReadTextFile, StreamBinaryFile, TextFileTypes
|
||||
from Settings import (
|
||||
DefaultSettings,
|
||||
DeleteTheme,
|
||||
ListThemeNames,
|
||||
LoadSettings,
|
||||
@@ -196,7 +205,7 @@ async def PostProfileAvatarEndpoint(
|
||||
|
||||
|
||||
@Application.get("/api/profile/avatar")
|
||||
def GetProfileAvatarEndpoint(Username: str, CurrentUser: dict = Depends(GetCurrentUser)):
|
||||
def GetProfileAvatarEndpoint(Username: str, CurrentUser: dict = Depends(GetCurrentUserOrGuest)):
|
||||
AvatarPath = GetAvatarPath(Username)
|
||||
|
||||
if AvatarPath is None:
|
||||
@@ -347,18 +356,20 @@ def GetLocksEndpoint(CurrentUser: dict = Depends(RequireAdmin)) -> list[dict]:
|
||||
|
||||
|
||||
@Application.get("/api/roots")
|
||||
def GetRootsEndpoint(CurrentUser: dict = Depends(GetCurrentUser)) -> list[dict]:
|
||||
def GetRootsEndpoint(CurrentUser: dict = Depends(GetCurrentUserOrGuest)) -> list[dict]:
|
||||
return ListRoots()
|
||||
|
||||
|
||||
@Application.get("/api/tree")
|
||||
def GetTreeEndpoint(
|
||||
Root: str, Path: str = "", CurrentUser: dict = Depends(GetCurrentUser)
|
||||
Root: str, Path: str = "", CurrentUser: dict = Depends(GetCurrentUserOrGuest)
|
||||
) -> list[dict]:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
try:
|
||||
UserSettings = LoadSettings(CurrentUser["Username"])
|
||||
UserSettings = (
|
||||
DefaultSettings if CurrentUser["Username"] is None else LoadSettings(CurrentUser["Username"])
|
||||
)
|
||||
ShowHidden = UserSettings.get("ShowHiddenFolders", False)
|
||||
ShowSvFiles = UserSettings.get("ShowSvFiles", False)
|
||||
Entries = ListDirectory(Root, Path, ShowHidden, ShowSvFiles)
|
||||
@@ -471,7 +482,7 @@ def DeleteTreeEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurr
|
||||
|
||||
@Application.get("/api/file")
|
||||
def GetFileEndpoint(
|
||||
Root: str, Path: str, request: Request, CurrentUser: dict = Depends(GetCurrentUser)
|
||||
Root: str, Path: str, request: Request, CurrentUser: dict = Depends(GetCurrentUserOrGuest)
|
||||
):
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
@@ -493,7 +504,7 @@ def GetFileEndpoint(
|
||||
|
||||
@Application.get("/api/file/resolved")
|
||||
def GetResolvedFileEndpoint(
|
||||
Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUser)
|
||||
Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUserOrGuest)
|
||||
) -> dict:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
@@ -548,7 +559,7 @@ async def PutFileEndpoint(
|
||||
|
||||
|
||||
@Application.get("/api/blame")
|
||||
def GetBlameEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
|
||||
def GetBlameEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUserOrGuest)) -> dict:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
try:
|
||||
@@ -588,7 +599,7 @@ def GetBlameEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurren
|
||||
|
||||
@Application.get("/api/raw/{RootName}/{FilePath:path}")
|
||||
def GetRawFileEndpoint(
|
||||
RootName: str, FilePath: str, CurrentUser: dict = Depends(GetCurrentUser)
|
||||
RootName: str, FilePath: str, CurrentUser: dict = Depends(GetCurrentUserOrGuest)
|
||||
):
|
||||
RequirePathAccessible(RootName, FilePath, CurrentUser)
|
||||
|
||||
@@ -611,7 +622,7 @@ def GetRawFileEndpoint(
|
||||
|
||||
|
||||
@Application.get("/api/book")
|
||||
def GetBookEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
|
||||
def GetBookEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUserOrGuest)) -> dict:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
try:
|
||||
@@ -624,7 +635,10 @@ def GetBookEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrent
|
||||
|
||||
|
||||
@Application.get("/api/settings")
|
||||
def GetSettingsEndpoint(CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
|
||||
def GetSettingsEndpoint(CurrentUser: dict = Depends(GetCurrentUserOrGuest)) -> dict:
|
||||
if CurrentUser["Username"] is None:
|
||||
return dict(DefaultSettings)
|
||||
|
||||
return LoadSettings(CurrentUser["Username"])
|
||||
|
||||
|
||||
@@ -639,7 +653,7 @@ def GetThemesEndpoint(CurrentUser: dict = Depends(GetCurrentUser)) -> list[str]:
|
||||
|
||||
|
||||
@Application.get("/api/theme")
|
||||
def GetThemeEndpoint(Name: str, CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
|
||||
def GetThemeEndpoint(Name: str, CurrentUser: dict = Depends(GetCurrentUserOrGuest)) -> dict:
|
||||
try:
|
||||
return LoadTheme(Name)
|
||||
except FileNotFoundError as Error:
|
||||
@@ -678,7 +692,7 @@ def DeleteThemeEndpoint(Name: str, CurrentUser: dict = Depends(GetCurrentUser))
|
||||
|
||||
@Application.get("/public/media/{RootName}/{FilePath:path}")
|
||||
def GetPublicMediaEndpoint(RootName: str, FilePath: str, request: Request):
|
||||
if not IsPathAccessible(RootName, FilePath, "", []):
|
||||
if not IsPathAccessible(RootName, FilePath, "", [GuestGroupName]):
|
||||
raise HTTPException(status_code=404, detail="Not found.")
|
||||
|
||||
try:
|
||||
|
||||
+29
-4
@@ -53,6 +53,8 @@ function ParseLocationPath(Pathname) {
|
||||
return { RootName: RootName || null, Path: RestSegments.join("/"), IsDirectory };
|
||||
}
|
||||
|
||||
const GuestUser = { Username: null, IsAdmin: false, IsGuest: true, Colour: "Purple" };
|
||||
|
||||
export default function App() {
|
||||
const [AuthState, SetAuthState] = useState("Loading");
|
||||
const [CurrentUser, SetCurrentUser] = useState(null);
|
||||
@@ -84,8 +86,10 @@ export default function App() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const IsUsable = AuthState === "LoggedIn" || AuthState === "Guest";
|
||||
|
||||
useEffect(() => {
|
||||
if (AuthState !== "LoggedIn") return;
|
||||
if (!IsUsable) return;
|
||||
|
||||
FetchRoots().then((Fetched) => {
|
||||
SetRoots(Fetched);
|
||||
@@ -100,7 +104,7 @@ export default function App() {
|
||||
}, [AuthState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (AuthState !== "LoggedIn" || !ActiveRoot) return;
|
||||
if (!IsUsable || !ActiveRoot) return;
|
||||
|
||||
const Url = BuildLocationPath(ActiveRoot, ViewPath, ViewIsDirectory);
|
||||
if (window.location.pathname !== Url) {
|
||||
@@ -109,7 +113,7 @@ export default function App() {
|
||||
}, [AuthState, ActiveRoot, ViewPath, ViewIsDirectory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (AuthState !== "LoggedIn") return;
|
||||
if (!IsUsable) return;
|
||||
|
||||
function HandlePopState() {
|
||||
RestoreFromLocation(Roots);
|
||||
@@ -174,6 +178,16 @@ export default function App() {
|
||||
SetAuthState("LoggedOut");
|
||||
}
|
||||
|
||||
function HandleContinueAsGuest() {
|
||||
SetCurrentUser(GuestUser);
|
||||
SetAuthState("Guest");
|
||||
}
|
||||
|
||||
function HandleRequestLogin() {
|
||||
SetCurrentUser(null);
|
||||
SetAuthState("LoggedOut");
|
||||
}
|
||||
|
||||
function HandleProfileUpdated(UpdatedUser) {
|
||||
SetCurrentUser((Existing) => ({ ...Existing, ...UpdatedUser }));
|
||||
}
|
||||
@@ -272,9 +286,16 @@ export default function App() {
|
||||
}
|
||||
|
||||
if (AuthState === "LoggedOut") {
|
||||
return <LoginScreen IsBootstrap={false} OnAuthenticated={HandleAuthenticated} />;
|
||||
return (
|
||||
<LoginScreen
|
||||
IsBootstrap={false}
|
||||
OnAuthenticated={HandleAuthenticated}
|
||||
OnContinueAsGuest={HandleContinueAsGuest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const IsGuest = AuthState === "Guest";
|
||||
const ShowTopBar = (Settings && Settings.ShowBreadcrumbs) || Roots.length > 1;
|
||||
const ConfirmBeforeDelete = Settings ? Settings.ConfirmBeforeDelete : true;
|
||||
|
||||
@@ -286,6 +307,7 @@ export default function App() {
|
||||
SelectedFile={ViewIsDirectory ? null : ViewPath}
|
||||
CurrentDirectoryPath={CurrentDirectoryPath}
|
||||
CurrentUser={CurrentUser}
|
||||
IsGuest={IsGuest}
|
||||
OnSelectFile={HandleSelectFile}
|
||||
OnSelectDirectory={OpenDirectoryNote}
|
||||
OnNavigateDirectory={HandleNavigateDirectory}
|
||||
@@ -293,6 +315,7 @@ export default function App() {
|
||||
OnEntryRenamed={HandleEntryRenamed}
|
||||
OnEntryDeleted={HandleEntryDeleted}
|
||||
OnOpenSettings={() => SetIsSettingsOpen(true)}
|
||||
OnRequestLogin={HandleRequestLogin}
|
||||
OnEntryCreated={RefreshSidebar}
|
||||
UseHyphenBullets={Settings ? Settings.UseHyphenBullets : true}
|
||||
UseAsciiFolderIcons={Settings ? Settings.UseAsciiFolderIcons : true}
|
||||
@@ -340,6 +363,7 @@ export default function App() {
|
||||
UseHyphenBullets={Settings ? Settings.UseHyphenBullets : true}
|
||||
UseAsciiFolderIcons={Settings ? Settings.UseAsciiFolderIcons : true}
|
||||
ConfirmBeforeDelete={ConfirmBeforeDelete}
|
||||
IsGuest={IsGuest}
|
||||
/>
|
||||
) : (
|
||||
<PreviewPane
|
||||
@@ -350,6 +374,7 @@ export default function App() {
|
||||
ShowGitBlame={Settings ? Settings.ShowGitBlame : false}
|
||||
ShowBlameAvatars={Settings ? Settings.ShowBlameAvatars : false}
|
||||
StickyToolbar={Settings ? Settings.StickyToolbar : true}
|
||||
IsGuest={IsGuest}
|
||||
/>
|
||||
)}
|
||||
{!ViewIsDirectory && !ViewNoteFilePath && (
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function DirectoryPane({
|
||||
UseHyphenBullets,
|
||||
UseAsciiFolderIcons,
|
||||
ConfirmBeforeDelete,
|
||||
IsGuest,
|
||||
}) {
|
||||
const [Entries, SetEntries] = useState([]);
|
||||
const [LoadError, SetLoadError] = useState(null);
|
||||
@@ -139,10 +140,12 @@ export default function DirectoryPane({
|
||||
|
||||
return (
|
||||
<div className="DirectoryPane">
|
||||
{!IsGuest && (
|
||||
<div className="PreviewToolbar">
|
||||
<button onClick={OpenCreateFileModal}>New File</button>
|
||||
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
|
||||
</div>
|
||||
)}
|
||||
{LoadError && <div className="SidebarError">{LoadError}</div>}
|
||||
<div className="DirectoryList">
|
||||
{Entries.map((Entry) => (
|
||||
@@ -159,6 +162,7 @@ export default function DirectoryPane({
|
||||
: GetFileIcon(UseHyphenBullets)}
|
||||
</span>
|
||||
<span className="TreeLabel">{Entry.Name}</span>
|
||||
{!IsGuest && (
|
||||
<span className="TreeRowActions">
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
@@ -182,6 +186,7 @@ export default function DirectoryPane({
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{Entries.length === 0 && !LoadError && (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
|
||||
import { Bootstrap, Login } from "../Api.js";
|
||||
|
||||
export default function LoginScreen({ IsBootstrap, OnAuthenticated }) {
|
||||
export default function LoginScreen({ IsBootstrap, OnAuthenticated, OnContinueAsGuest }) {
|
||||
const [Username, SetUsername] = useState("");
|
||||
const [Password, SetPassword] = useState("");
|
||||
const [ConfirmPassword, SetConfirmPassword] = useState("");
|
||||
@@ -74,6 +74,11 @@ export default function LoginScreen({ IsBootstrap, OnAuthenticated }) {
|
||||
<button type="submit" disabled={IsSubmitting}>
|
||||
{IsSubmitting ? "..." : IsBootstrap ? "Create account & log in" : "Log in"}
|
||||
</button>
|
||||
{!IsBootstrap && OnContinueAsGuest && (
|
||||
<button type="button" className="LoginGuestButton" onClick={OnContinueAsGuest}>
|
||||
Continue as Guest
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -170,6 +170,7 @@ export default function PreviewPane({
|
||||
ShowGitBlame,
|
||||
ShowBlameAvatars,
|
||||
StickyToolbar,
|
||||
IsGuest,
|
||||
}) {
|
||||
const [Content, SetContent] = useState("");
|
||||
const [ResolvedContent, SetResolvedContent] = useState("");
|
||||
@@ -259,7 +260,7 @@ export default function PreviewPane({
|
||||
return (
|
||||
<div className="PreviewFrame">
|
||||
<div className={ToolbarClass}>
|
||||
<button onClick={() => SetIsEditing(true)}>Edit</button>
|
||||
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
|
||||
</div>
|
||||
<iframe title={FilePath} sandbox="" src={GetRawFileUrl(RootName, FilePath)} />
|
||||
</div>
|
||||
@@ -270,7 +271,7 @@ export default function PreviewPane({
|
||||
return (
|
||||
<div className="PreviewMarkdown">
|
||||
<div className={ToolbarClass}>
|
||||
<button onClick={() => SetIsEditing(true)}>Edit</button>
|
||||
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
|
||||
</div>
|
||||
<div
|
||||
className="MarkdownBody"
|
||||
@@ -292,7 +293,7 @@ export default function PreviewPane({
|
||||
return (
|
||||
<div className="PreviewText">
|
||||
<div className={ToolbarClass}>
|
||||
<button onClick={() => SetIsEditing(true)}>Edit</button>
|
||||
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
|
||||
</div>
|
||||
<pre className="TextBody">{ResolvedContent}</pre>
|
||||
</div>
|
||||
@@ -306,14 +307,17 @@ export default function PreviewPane({
|
||||
{(FileType === "markdown" || FileType === "text" || FileType === "html") && (
|
||||
<button onClick={() => SetIsEditing(false)}>View</button>
|
||||
)}
|
||||
{!IsGuest && (
|
||||
<button onClick={HandleSave} disabled={IsSaving}>
|
||||
{IsSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
)}
|
||||
{SaveError && <span className="SaveErrorText">{SaveError}</span>}
|
||||
</div>
|
||||
<CodeMirror
|
||||
value={Content}
|
||||
height="100%"
|
||||
editable={!IsGuest}
|
||||
extensions={[
|
||||
...GetLanguageExtension(FileType),
|
||||
...(ShowGitBlame ? [BuildBlameGutter(BlameMap, ShowBlameAvatars)] : []),
|
||||
|
||||
@@ -30,6 +30,7 @@ function TreeNode({
|
||||
UseAsciiFolderIcons,
|
||||
NavigateIntoDirectories,
|
||||
ConfirmBeforeDelete,
|
||||
IsGuest,
|
||||
}) {
|
||||
const [IsExpanded, SetIsExpanded] = useState(false);
|
||||
const [Children, SetChildren] = useState(null);
|
||||
@@ -134,6 +135,7 @@ function TreeNode({
|
||||
: GetFileIcon(UseHyphenBullets)}
|
||||
</span>
|
||||
<span className="TreeLabel">{Entry.Name}</span>
|
||||
{!IsGuest && (
|
||||
<span className="TreeRowActions">
|
||||
<button className="TreeRowActionButton" onClick={HandleLockClick} title="Access control">
|
||||
🔒
|
||||
@@ -145,6 +147,7 @@ function TreeNode({
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{IsExpanded && Children && (
|
||||
<div className="TreeChildren">
|
||||
@@ -167,6 +170,7 @@ function TreeNode({
|
||||
UseAsciiFolderIcons={UseAsciiFolderIcons}
|
||||
NavigateIntoDirectories={NavigateIntoDirectories}
|
||||
ConfirmBeforeDelete={ConfirmBeforeDelete}
|
||||
IsGuest={IsGuest}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -175,7 +179,7 @@ function TreeNode({
|
||||
);
|
||||
}
|
||||
|
||||
function UserBadge({ CurrentUser, OnOpenSettings }) {
|
||||
function UserBadge({ CurrentUser, OnOpenSettings, OnRequestLogin }) {
|
||||
const [AvatarFailed, SetAvatarFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -184,6 +188,16 @@ function UserBadge({ CurrentUser, OnOpenSettings }) {
|
||||
|
||||
if (!CurrentUser) return null;
|
||||
|
||||
if (CurrentUser.IsGuest) {
|
||||
return (
|
||||
<span className="UserBadge" onClick={OnRequestLogin} title="Browsing as Guest - click to log in">
|
||||
<span className="UserBadgeInitial" style={{ background: `var(--${CurrentUser.Colour || "Purple"})` }}>
|
||||
G
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const AvatarUrl = CurrentUser.AvatarNonce
|
||||
? `${GetAvatarUrl(CurrentUser.Username)}&v=${CurrentUser.AvatarNonce}`
|
||||
: GetAvatarUrl(CurrentUser.Username);
|
||||
@@ -221,12 +235,14 @@ export default function Sidebar({
|
||||
OnEntryRenamed,
|
||||
OnEntryDeleted,
|
||||
OnOpenSettings,
|
||||
OnRequestLogin,
|
||||
OnEntryCreated,
|
||||
UseHyphenBullets,
|
||||
UseAsciiFolderIcons,
|
||||
NavigateIntoDirectories,
|
||||
NewEntryButtonsAtTop,
|
||||
ConfirmBeforeDelete,
|
||||
IsGuest,
|
||||
}) {
|
||||
const [RootEntries, SetRootEntries] = useState([]);
|
||||
const [LoadError, SetLoadError] = useState(null);
|
||||
@@ -299,13 +315,15 @@ export default function Sidebar({
|
||||
SaraView
|
||||
</span>
|
||||
<span className="SidebarHeaderRight">
|
||||
<UserBadge CurrentUser={CurrentUser} OnOpenSettings={OnOpenSettings} />
|
||||
<UserBadge CurrentUser={CurrentUser} OnOpenSettings={OnOpenSettings} OnRequestLogin={OnRequestLogin} />
|
||||
{!IsGuest && (
|
||||
<button className="SettingsButton" onClick={OnOpenSettings}>
|
||||
⚙
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{NewEntryButtonsAtTop && (
|
||||
{NewEntryButtonsAtTop && !IsGuest && (
|
||||
<div className="SidebarNewEntry SidebarNewEntryTop">
|
||||
<button onClick={OpenCreateFileModal}>New File</button>
|
||||
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
|
||||
@@ -332,10 +350,11 @@ export default function Sidebar({
|
||||
UseAsciiFolderIcons={UseAsciiFolderIcons}
|
||||
NavigateIntoDirectories={NavigateIntoDirectories}
|
||||
ConfirmBeforeDelete={ConfirmBeforeDelete}
|
||||
IsGuest={IsGuest}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!NewEntryButtonsAtTop && (
|
||||
{!NewEntryButtonsAtTop && !IsGuest && (
|
||||
<div className="SidebarNewEntry">
|
||||
<button onClick={OpenCreateFileModal}>New File</button>
|
||||
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
|
||||
|
||||
@@ -897,3 +897,8 @@ img,
|
||||
.LoginBox button:hover {
|
||||
background: var(--Border);
|
||||
}
|
||||
|
||||
.LoginGuestButton {
|
||||
border-color: var(--Border) !important;
|
||||
color: var(--TextMuted) !important;
|
||||
}
|
||||
Reference in new issue
Block a user