mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-11 06:48:11 +00:00
feat: initial wip frontend
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user