Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions frontend/src/components/icons/local-auth.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
72 changes: 66 additions & 6 deletions frontend/src/components/quick-actions/quick-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "sonner";
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 { LocalAuthIcon } from "../icons/local-auth";
import { OAuthIcon } from "../icons/oauth";

function Avatar({ initial }: { initial: string }) {
return (
Expand All @@ -49,8 +56,18 @@ function Avatar({ initial }: { initial: string }) {
);
}

const iconStyles = "size-4";

const iconMap: Record<string, React.ReactNode> = {
google: <GoogleIcon className={iconStyles} />,
github: <GithubIcon className={iconStyles} />,
tailscale: <TailscaleIcon className={iconStyles} />,
microsoft: <MicrosoftIcon className={iconStyles} />,
pocketid: <PocketIDIcon className={iconStyles} />,
};

export const QuickActions = () => {
const { auth } = useUserContext();
const { auth, oauth, tailscale } = useUserContext();
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const { search } = useLocation();
Expand All @@ -64,6 +81,41 @@ export const QuickActions = () => {
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);

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: <LocalAuthIcon className={iconStyles} />,
};
}

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({
mutationFn: () => axios.post("/api/user/logout"),
mutationKey: ["logout"],
Expand Down Expand Up @@ -134,11 +186,19 @@ export const QuickActions = () => {
<div className="bg-foreground text-background flex size-9 shrink-0 items-center justify-center rounded-full text-sm font-medium">
{initial}
</div>
<div className="flex min-w-0 flex-col">
<span className="truncate text-sm font-medium">
{auth.name}
</span>
<span className="text-muted-foreground truncate text-xs font-normal">
<div className="flex min-w-0 flex-col gap-1.5">
<div className="flex items-center gap-2">
<span className="truncate text-sm font-medium leading-none">
{auth.name}
</span>
{providerDetails && (
<span className="text-muted-foreground inline-flex shrink-0 items-center gap-1 rounded-lg bg-secondary px-2 py-1.25 text-[10px] font-medium leading-none [&_svg]:size-3">
{providerDetails.icon}
{providerDetails.name}
</span>
)}
</div>
<span className="text-muted-foreground truncate text-xs leading-none">
{auth.email}
</span>
</div>
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,8 @@
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions"
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
}
5 changes: 5 additions & 0 deletions internal/bootstrap/app_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/url"
"os"
"os/signal"
"slices"
"sort"
"strings"
"syscall"
Expand Down Expand Up @@ -131,6 +132,10 @@ func (app *BootstrapApp) Setup() error {
app.runtime.OAuthProviders = app.config.OAuth.Providers

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)
if err != nil {
return fmt.Errorf("failed to load oauth whitelist for provider %s: %w", id, err)
Expand Down
2 changes: 2 additions & 0 deletions internal/model/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var OverrideProviders = map[string]string{
"github": "GitHub",
}

var ReservedProviderNames = []string{"local", "ldap", "tailscale"}

const SessionCookieName = "tinyauth-session"
const CSRFCookieName = "tinyauth-csrf"
const RedirectCookieName = "tinyauth-redirect"
Expand Down