feat: initial wip frontend

This commit is contained in:
Stavros
2026-04-28 19:11:36 +03:00
parent a5677d2558
commit 3971710e87
7 changed files with 158 additions and 12 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ root = "/tinyauth"
tmp_dir = "tmp" tmp_dir = "tmp"
[build] [build]
pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data", "echo 'backend running' > internal/assets/dist/index.html"] pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data"] # "echo 'backend running' > internal/assets/dist/index.html"
cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ./cmd/tinyauth" cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ./cmd/tinyauth"
bin = "tmp/tinyauth" bin = "tmp/tinyauth"
full_bin = "dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false" full_bin = "dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false"
+69 -1
View File
@@ -36,12 +36,13 @@ const iconMap: Record<string, React.ReactNode> = {
}; };
export const LoginPage = () => { export const LoginPage = () => {
const { isLoggedIn } = useUserContext(); const { isLoggedIn, tailscaleNodeName } = useUserContext();
const { providers, title, oauthAutoRedirect } = useAppContext(); const { providers, title, oauthAutoRedirect } = useAppContext();
const { search } = useLocation(); const { search } = useLocation();
const { t } = useTranslation(); const { t } = useTranslation();
const [showRedirectButton, setShowRedirectButton] = useState(false); const [showRedirectButton, setShowRedirectButton] = useState(false);
const [useTailscale, setUseTailscale] = useState(tailscaleNodeName !== "");
const hasAutoRedirectedRef = useRef(false); const hasAutoRedirectedRef = useRef(false);
@@ -148,6 +149,32 @@ export const LoginPage = () => {
}, },
}); });
const { mutate: tailscaleMutate, isPending: tailscaleIsPending } =
useMutation({
mutationFn: () => axios.post("/api/user/tailscale"),
mutationKey: ["tailscale"],
onSuccess: () => {
toast.success("Logged in", {
description: t("Tailscale session confirmed"),
});
redirectTimer.current = window.setTimeout(() => {
if (oidcParams.isOidc) {
window.location.replace(`/authorize?${oidcParams.compiled}`);
return;
}
window.location.replace(
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
);
}, 500);
},
onError: () => {
toast.error("Failed to login", {
description: "Failed to authenticate with Tailscale.",
});
},
});
useEffect(() => { useEffect(() => {
if ( if (
!isLoggedIn && !isLoggedIn &&
@@ -228,6 +255,47 @@ export const LoginPage = () => {
</Card> </Card>
); );
} }
if (useTailscale) {
return (
<Card>
<CardHeader className="gap-3">
<TailscaleIcon className="mx-auto h-8 w-8" />
<CardTitle className="text-center text-xl">
Tinyauth x Tailscale
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="text-muted-foreground text-sm">
We detected that you are accessing Tinyauth from an authorized
Tailscale device. Would you like to continue with your Tailscale
credentials?
</div>
<div className="text-muted-foreground text-sm">
Machine Name: <code>{tailscaleNodeName}</code>
</div>
</CardContent>
<CardFooter className="flex flex-col items-stretch gap-3">
<Button
className="w-full"
onClick={() => tailscaleMutate()}
loading={tailscaleIsPending}
>
Continue with Tailscale
</Button>
<Button
className="w-full"
variant="outline"
onClick={() => setUseTailscale(false)}
disabled={tailscaleIsPending}
>
Use other login method
</Button>
</CardFooter>
</Card>
);
}
return ( return (
<Card> <Card>
<CardHeader className="gap-1.5"> <CardHeader className="gap-1.5">
@@ -9,6 +9,7 @@ export const userContextSchema = z.object({
oauth: z.boolean(), oauth: z.boolean(),
totpPending: z.boolean(), totpPending: z.boolean(),
oauthName: z.string(), oauthName: z.string(),
tailscaleNodeName: z.string(),
}); });
export type UserContextSchema = z.infer<typeof userContextSchema>; export type UserContextSchema = z.infer<typeof userContextSchema>;
+15 -10
View File
@@ -11,16 +11,17 @@ import (
) )
type UserContextResponse struct { type UserContextResponse struct {
Status int `json:"status"` Status int `json:"status"`
Message string `json:"message"` Message string `json:"message"`
IsLoggedIn bool `json:"isLoggedIn"` IsLoggedIn bool `json:"isLoggedIn"`
Username string `json:"username"` Username string `json:"username"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Provider string `json:"provider"` Provider string `json:"provider"`
OAuth bool `json:"oauth"` OAuth bool `json:"oauth"`
TotpPending bool `json:"totpPending"` TotpPending bool `json:"totpPending"`
OAuthName string `json:"oauthName"` OAuthName string `json:"oauthName"`
TailscaleNodeName string `json:"tailscaleNodeName"`
} }
type AppContextResponse struct { type AppContextResponse struct {
@@ -91,6 +92,10 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
OAuthName: context.OAuthName, OAuthName: context.OAuthName,
} }
if context.Tailscale != nil {
userContext.TailscaleNodeName = context.Tailscale.NodeName
}
if err != nil { if err != nil {
tlog.App.Debug().Err(err).Msg("No user context found in request") tlog.App.Debug().Err(err).Msg("No user context found in request")
userContext.Status = 401 userContext.Status = 401
+48
View File
@@ -46,6 +46,7 @@ func (controller *UserController) SetupRoutes() {
userGroup.POST("/login", controller.loginHandler) userGroup.POST("/login", controller.loginHandler)
userGroup.POST("/logout", controller.logoutHandler) userGroup.POST("/logout", controller.logoutHandler)
userGroup.POST("/totp", controller.totpHandler) userGroup.POST("/totp", controller.totpHandler)
userGroup.POST("/tailscale", controller.tailscaleHandler)
} }
func (controller *UserController) loginHandler(c *gin.Context) { func (controller *UserController) loginHandler(c *gin.Context) {
@@ -309,3 +310,50 @@ func (controller *UserController) totpHandler(c *gin.Context) {
"message": "Login successful", "message": "Login successful",
}) })
} }
func (controller *UserController) tailscaleHandler(c *gin.Context) {
context, err := utils.GetContext(c)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to get user context")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
if context.Tailscale == nil {
tlog.App.Warn().Msg("Tailscale session requested but Tailscale device not found")
c.JSON(404, gin.H{
"status": 404,
"message": "Not Found",
})
return
}
sessionCookie := repository.Session{
Username: context.Tailscale.LoginName,
Name: context.Tailscale.DisplayName,
Email: context.Tailscale.LoginName,
Provider: "tailscale",
}
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
c.JSON(200, gin.H{
"status": 200,
"message": "Login successful",
})
}
+14
View File
@@ -63,6 +63,8 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
return return
} }
tlog.App.Trace().Interface("cookies", c.Request.Cookies()).Msg("cookies")
cookie, err := m.auth.GetSessionCookie(c) cookie, err := m.auth.GetSessionCookie(c)
if err != nil { if err != nil {
@@ -134,6 +136,18 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
c.Set("context", &ctx) c.Set("context", &ctx)
c.Next() c.Next()
return return
case "tailscale":
m.auth.RefreshSessionCookie(c)
ctx := m.addTailscaleContext(c, config.UserContext{
Username: cookie.Username,
Name: cookie.Name,
Email: cookie.Email,
Provider: cookie.Provider,
IsLoggedIn: true,
})
c.Set("context", &ctx)
c.Next()
return
default: default:
_, exists := m.broker.GetService(cookie.Provider) _, exists := m.broker.GetService(cookie.Provider)
+10
View File
@@ -327,6 +327,16 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Se
return err return err
} }
if data.Provider == "tailscale" {
// TODO: use domain from tailscale to set cookie, this is mostly a hack for now
tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", c.Request.Host))
if err != nil {
return err
}
c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", tsCookieDomain), auth.config.SecureCookie, true)
return nil
}
c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
return nil return nil