diff --git a/cmd/gogs/internal/web/api.go b/cmd/gogs/internal/web/api.go index 78f77c1fe..344cd5ed4 100644 --- a/cmd/gogs/internal/web/api.go +++ b/cmd/gogs/internal/web/api.go @@ -6,23 +6,36 @@ import ( "net/http" "github.com/flamego/flamego" + "github.com/go-macaron/session" + "gopkg.in/macaron.v1" + "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/context" "gogs.io/gogs/internal/database" ) -type webAPIBridgeKey struct{} +type ( + webAPIUserKey struct{} + webAPISessionKey struct{} + webAPIMacaronKey struct{} +) func bridgeToWebAPI(webHandler http.Handler) func(c *context.Context) { return func(c *context.Context) { - ctx := stdctx.WithValue(c.Req.Context(), webAPIBridgeKey{}, c.User) + ctx := c.Req.Context() + ctx = stdctx.WithValue(ctx, webAPIUserKey{}, c.User) + ctx = stdctx.WithValue(ctx, webAPISessionKey{}, c.Session) + ctx = stdctx.WithValue(ctx, webAPIMacaronKey{}, c.Context) webHandler.ServeHTTP(c.Resp, c.Req.WithContext(ctx)) } } func webAPIInjector(c flamego.Context) { - user, _ := c.Request().Context().Value(webAPIBridgeKey{}).(*database.User) - c.Map(user) + ctx := c.Request().Context() + user, _ := ctx.Value(webAPIUserKey{}).(*database.User) + sess, _ := ctx.Value(webAPISessionKey{}).(session.Store) + mc, _ := ctx.Value(webAPIMacaronKey{}).(*macaron.Context) + c.Map(user, sess, mc) } func mountWebAPIRoutes(f *flamego.Flame) { @@ -45,7 +58,10 @@ func mountWebAPIRoutes(f *flamego.Flame) { }) f.Group("/api/web", func() { - f.Get("/user-info", userInfoHandler) + f.Group("/user", func() { + f.Get("/info", userInfoHandler) + f.Post("/sign-out", userSignOutHandler) + }) }, webAPIInjector) } @@ -69,3 +85,12 @@ func userInfoHandler(user *database.User) (statusCode int, resp *userInfo, err e }, nil } + +func userSignOutHandler(sess session.Store, mc *macaron.Context) (statusCode int, resp any, err error) { + _ = sess.Flush() + _ = sess.Destory(mc) + mc.SetCookie(conf.Security.CookieUsername, "", -1, conf.Server.Subpath) + mc.SetCookie(conf.Security.CookieRememberName, "", -1, conf.Server.Subpath) + mc.SetCookie(conf.Session.CSRFCookieName, "", -1, conf.Server.Subpath) + return http.StatusNoContent, nil, nil +} diff --git a/web/src/components/Navbar.tsx b/web/src/components/Navbar.tsx index b3745b2e2..0d818bb0e 100644 --- a/web/src/components/Navbar.tsx +++ b/web/src/components/Navbar.tsx @@ -305,12 +305,32 @@ function NavLink({ href, external, children }: { href: string; external?: boolea function SignOutForm({ children }: { children: React.ReactNode }) { return ( -
+ { + event.preventDefault(); + void signOut(); + }} + > {children}
); } +async function signOut() { + try { + await fetch(subUrl("/api/web/user/sign-out"), { + method: "POST", + credentials: "same-origin", + }); + } catch (err) { + console.error("signOut: request failed", err); + } + window.location.assign(subUrl("/")); +} + function MobileLink({ href, external, diff --git a/web/src/lib/user-info.ts b/web/src/lib/user-info.ts index 43a2be189..59614af55 100644 --- a/web/src/lib/user-info.ts +++ b/web/src/lib/user-info.ts @@ -9,7 +9,7 @@ export interface UserInfo { export async function fetchUserInfo(): Promise { try { - const res = await fetch(subUrl("/api/web/user-info"), { credentials: "same-origin" }); + const res = await fetch(subUrl("/api/web/user/info"), { credentials: "same-origin" }); if (res.status === 204) return null; if (!res.ok) { console.error(`fetchUserInfo: unexpected status ${res.status}`);