Compare commits

..

6 Commits

Author SHA1 Message Date
Stavros 53301c7a9a fix: line leading and width tweaks 2026-06-25 23:21:39 +03:00
Stavros 5b321f2fc8 chore: update en-us translations 2026-06-25 23:12:20 +03:00
Stavros cd377ad361 refactor: rework quick actions to use provider icon instead of avatar 2026-06-25 23:09:18 +03:00
Stavros 0097ecc796 feat: animate account avatar to close icon 2026-06-25 17:30:54 +03:00
Stavros 93f882e460 fix: don't allow the reserved provider names to be used in oauth 2026-06-24 12:48:22 +03:00
Stavros aaa5f4cb2f feat: show provider badge in quick actions 2026-06-24 12:27:30 +03:00
10 changed files with 130 additions and 29 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
package_json_file: ./frontend/package.json package_json_file: ./frontend/package.json
- name: Setup go - name: Setup go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with: with:
go-version: "^1.26.4" go-version: "^1.26.4"
+2 -2
View File
@@ -65,7 +65,7 @@ jobs:
package_json_file: ./frontend/package.json package_json_file: ./frontend/package.json
- name: Install go - name: Install go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with: with:
go-version: "^1.26.4" go-version: "^1.26.4"
@@ -110,7 +110,7 @@ jobs:
package_json_file: ./frontend/package.json package_json_file: ./frontend/package.json
- name: Install go - name: Install go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with: with:
go-version: "^1.26.4" go-version: "^1.26.4"
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
package_json_file: ./frontend/package.json package_json_file: ./frontend/package.json
- name: Install go - name: Install go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with: with:
go-version: "^1.26.4" go-version: "^1.26.4"
@@ -83,7 +83,7 @@ jobs:
package_json_file: ./frontend/package.json package_json_file: ./frontend/package.json
- name: Install go - name: Install go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with: with:
go-version: "^1.26.4" go-version: "^1.26.4"
@@ -0,0 +1,22 @@
import type { SVGProps } from "react";
export function LocalAuthIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M6 21v-2a4 4 0 0 1 4-4h5m3.5 3.5L15 22l-1.5-1.5m5.054-2.086a2 2 0 1 1 2.828-2.828a2 2 0 0 1-2.828 2.828M16 19l1 1"
></path>
</svg>
);
}
@@ -25,6 +25,8 @@ import {
Palette, Palette,
Settings, Settings,
Sun, Sun,
UserRoundKey,
X,
} from "lucide-react"; } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
@@ -37,20 +39,26 @@ import { useMutation } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { toast } from "sonner"; import { toast } from "sonner";
import { useEffect } from "react"; import { useEffect } from "react";
import { GoogleIcon } from "../icons/google";
import { GithubIcon } from "../icons/github";
import { TailscaleIcon } from "../icons/tailscale";
import { MicrosoftIcon } from "../icons/microsoft";
import { PocketIDIcon } from "../icons/pocket-id";
import { OAuthIcon } from "../icons/oauth";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
function Avatar({ initial }: { initial: string }) { const iconStyles = "size-4";
return (
<span className="group relative grid size-10 place-items-center rounded-full"> const iconMap: Record<string, React.ReactNode> = {
<span className="absolute inset-0 overflow-hidden rounded-full bg-linear-to-b from-neutral-50 to-neutral-100 dark:from-neutral-700 dark:to-neutral-950 shadow-lg"></span> google: <GoogleIcon className={iconStyles} />,
<span className="relative text-sm font-semibold text-primary"> github: <GithubIcon className={iconStyles} />,
{initial} tailscale: <TailscaleIcon className={iconStyles} />,
</span> microsoft: <MicrosoftIcon className={iconStyles} />,
</span> pocketid: <PocketIDIcon className={iconStyles} />,
); };
}
export const QuickActions = () => { export const QuickActions = () => {
const { auth } = useUserContext(); const { auth, oauth, tailscale } = useUserContext();
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const { search } = useLocation(); const { search } = useLocation();
@@ -64,6 +72,49 @@ export const QuickActions = () => {
const screenParams = useScreenParams(searchParams); const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams); const compiledParams = recompileScreenParams(screenParams);
const [isOpen, setIsOpen] = useState(false);
const providerDetails = (():
| { name: string; icon: React.ReactNode }
| undefined => {
if (!auth.authenticated) {
return undefined;
}
if (auth.providerId === "local" || auth.providerId === "ldap") {
return {
name: t(
auth.providerId === "ldap"
? "quickActionsProviderLDAP"
: "quickActionsProviderLocal",
),
icon: (
<UserRoundKey
strokeWidth={1.5}
size={16}
className="text-muted-foreground ml-0.5"
/>
),
};
}
if (oauth.active) {
return {
name: t("quickActionsProviderOAuth", { provider: oauth.displayName }),
icon: iconMap[auth.providerId] || <OAuthIcon className={iconStyles} />,
};
}
if (auth.providerId === "tailscale") {
return {
name: `Tailscale (${tailscale.nodeName})`,
icon: <TailscaleIcon className={iconStyles} />,
};
}
return undefined;
})();
const logoutMutation = useMutation({ const logoutMutation = useMutation({
mutationFn: () => axios.post("/api/user/logout"), mutationFn: () => axios.post("/api/user/logout"),
mutationKey: ["logout"], mutationKey: ["logout"],
@@ -107,17 +158,29 @@ export const QuickActions = () => {
] as const; ] as const;
return ( return (
<DropdownMenu> <DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
aria-label={t("quickActionsTitle")} aria-label={t("quickActionsTitle")}
className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50" className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50"
> >
{auth.authenticated ? ( {auth.authenticated ? (
<Avatar initial={initial!} /> <div className="size-10 flex justify-center items-center p-2 rounded-full bg-card border border-border">
{isOpen ? (
<X className="size-4 text-primary rotate-0 transition-transform duration-200 starting:rotate-45" />
) : (
<span className="text-sm text-primary rotate-0 transition-transform duration-200 starting:-rotate-45">
{initial}
</span>
)}
</div>
) : ( ) : (
<span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg"> <span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg">
<Settings className="size-4" /> <Settings
className={`size-4 transition-transform duration-200 ${
isOpen ? "rotate-45" : "rotate-0"
}`}
/>
</span> </span>
)} )}
</button> </button>
@@ -126,19 +189,22 @@ export const QuickActions = () => {
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
sideOffset={8} sideOffset={8}
className="rounded-xl p-1" className="rounded-xl p-1 w-3xs"
> >
{auth.authenticated && ( {auth.authenticated && (
<> <>
<DropdownMenuLabel className="flex items-center gap-3 p-2"> <DropdownMenuLabel className="flex items-center gap-3 p-2">
<div className="bg-foreground text-background flex size-9 shrink-0 items-center justify-center rounded-full text-sm font-medium"> <Tooltip>
{initial} <TooltipTrigger className="size-9 rounded-full p-2 bg-muted border-border border flex items-center justify-center">
</div> {providerDetails!.icon}
<div className="flex min-w-0 flex-col"> </TooltipTrigger>
<TooltipContent>{providerDetails!.name}</TooltipContent>
</Tooltip>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="truncate text-sm font-medium"> <span className="truncate text-sm font-medium">
{auth.name} {auth.name}
</span> </span>
<span className="text-muted-foreground truncate text-xs font-normal"> <span className="text-muted-foreground truncate text-xs">
{auth.email} {auth.email}
</span> </span>
</div> </div>
@@ -197,7 +263,7 @@ export const QuickActions = () => {
onSelect={() => logoutMutation.mutate()} onSelect={() => logoutMutation.mutate()}
className="text-destructive" className="text-destructive"
> >
<DoorOpenIcon className="size-4" /> <DoorOpenIcon className="size-4 text-destructive" />
{t("quickActionsLogout")} {t("quickActionsLogout")}
</DropdownMenuItem> </DropdownMenuItem>
</> </>
+4 -1
View File
@@ -99,5 +99,8 @@
"quickActionsThemeDark": "Dark", "quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System", "quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout", "quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions" "quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+4 -1
View File
@@ -99,5 +99,8 @@
"quickActionsThemeDark": "Dark", "quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System", "quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout", "quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions" "quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
} }
+1 -1
View File
@@ -137,7 +137,7 @@ function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) {
</CardHeader> </CardHeader>
<CardFooter> <CardFooter>
<Button <Button
className="w-full" className="w-full text-destructive"
variant="outline" variant="outline"
loading={logoutMutation.isPending} loading={logoutMutation.isPending}
onClick={() => logoutMutation.mutate()} onClick={() => logoutMutation.mutate()}
+5
View File
@@ -11,6 +11,7 @@ import (
"net/url" "net/url"
"os" "os"
"os/signal" "os/signal"
"slices"
"sort" "sort"
"strings" "strings"
"syscall" "syscall"
@@ -131,6 +132,10 @@ func (app *BootstrapApp) Setup() error {
app.runtime.OAuthProviders = app.config.OAuth.Providers app.runtime.OAuthProviders = app.config.OAuth.Providers
for id, provider := range app.runtime.OAuthProviders { for id, provider := range app.runtime.OAuthProviders {
if slices.Contains(model.ReservedProviderNames, id) {
return fmt.Errorf("provider id %s is reserved and cannot be used", id)
}
providerWhitelist, err := utils.GetStringList(provider.Whitelist, provider.WhitelistFile) providerWhitelist, err := utils.GetStringList(provider.Whitelist, provider.WhitelistFile)
if err != nil { if err != nil {
return fmt.Errorf("failed to load oauth whitelist for provider %s: %w", id, err) return fmt.Errorf("failed to load oauth whitelist for provider %s: %w", id, err)
+2
View File
@@ -17,6 +17,8 @@ var OverrideProviders = map[string]string{
"github": "GitHub", "github": "GitHub",
} }
var ReservedProviderNames = []string{"local", "ldap", "tailscale"}
const SessionCookieName = "tinyauth-session" const SessionCookieName = "tinyauth-session"
const CSRFCookieName = "tinyauth-csrf" const CSRFCookieName = "tinyauth-csrf"
const RedirectCookieName = "tinyauth-redirect" const RedirectCookieName = "tinyauth-redirect"