A bunch of fixes
This commit is contained in:
1 parent
65534ecfd5
commit
2a4e03e611
20 files changed
+146
-160
No files matched your search
+23
-63
@@ -9,21 +9,29 @@ import (
|
||||
"uPortal/internal/auth"
|
||||
)
|
||||
|
||||
// HandleSignin renders the sign-in page on GET and authenticates on POST.
|
||||
// For relative paths, redirect directly; for external URLs, route
|
||||
// through the warning page unless the domain is trusted.
|
||||
func (s *WebService) handleRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
redirectTo := "/"
|
||||
if r.FormValue("redirect") != "" {
|
||||
redirectTo = r.FormValue("redirect")
|
||||
} else if r.URL.Query().Get("redirect") != "" {
|
||||
redirectTo = r.URL.Query().Get("redirect")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(redirectTo, "/") && !strings.Contains(redirectTo, "://") {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
} else if s.isTrustedTarget(redirectTo) {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, "/redirect?to="+url.QueryEscape(redirectTo), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebService) HandleSignin(w http.ResponseWriter, r *http.Request) {
|
||||
// If already authenticated, redirect
|
||||
if _, err := s.currentUser(r); err == nil {
|
||||
redirectTo := "/"
|
||||
if r.URL.Query().Get("redirect") != "" {
|
||||
redirectTo = r.URL.Query().Get("redirect")
|
||||
}
|
||||
// For relative paths, redirect directly; for external URLs, route
|
||||
// through the warning page.
|
||||
if strings.HasPrefix(redirectTo, "/") && !strings.Contains(redirectTo, "://") {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, "/redirect?to="+url.QueryEscape(redirectTo), http.StatusSeeOther)
|
||||
}
|
||||
s.handleRedirect(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,41 +85,14 @@ func (s *WebService) HandleSignin(w http.ResponseWriter, r *http.Request) {
|
||||
MaxAge: maxAge,
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
redirectTo := "/"
|
||||
if r.FormValue("redirect") != "" {
|
||||
redirectTo = r.FormValue("redirect")
|
||||
} else if r.URL.Query().Get("redirect") != "" {
|
||||
redirectTo = r.URL.Query().Get("redirect")
|
||||
}
|
||||
// For relative paths, redirect directly; for external URLs, route
|
||||
// through the warning page unless the domain is trusted.
|
||||
if strings.HasPrefix(redirectTo, "/") && !strings.Contains(redirectTo, "://") {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
} else if s.isTrustedTarget(redirectTo) {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, "/redirect?to="+url.QueryEscape(redirectTo), http.StatusSeeOther)
|
||||
}
|
||||
s.handleRedirect(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSignup renders the sign-up page on GET and creates an account on POST.
|
||||
// Invite codes are one-time-use unless they carry multiple uses.
|
||||
func (s *WebService) HandleSignup(w http.ResponseWriter, r *http.Request) {
|
||||
// If already authenticated, redirect
|
||||
if _, err := s.currentUser(r); err == nil {
|
||||
redirectTo := "/"
|
||||
if r.URL.Query().Get("redirect") != "" {
|
||||
redirectTo = r.URL.Query().Get("redirect")
|
||||
}
|
||||
// For relative paths, redirect directly; for external URLs, route
|
||||
// through the warning page.
|
||||
if strings.HasPrefix(redirectTo, "/") && !strings.Contains(redirectTo, "://") {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, "/redirect?to="+url.QueryEscape(redirectTo), http.StatusSeeOther)
|
||||
}
|
||||
s.handleRedirect(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -203,27 +184,10 @@ func (s *WebService) HandleSignup(w http.ResponseWriter, r *http.Request) {
|
||||
Secure: CookieSecure,
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
redirectTo := "/"
|
||||
if r.FormValue("redirect") != "" {
|
||||
redirectTo = r.FormValue("redirect")
|
||||
} else if r.URL.Query().Get("redirect") != "" {
|
||||
redirectTo = r.URL.Query().Get("redirect")
|
||||
}
|
||||
// For relative paths, redirect directly; for external URLs, route
|
||||
// through the warning page unless the domain is trusted.
|
||||
if strings.HasPrefix(redirectTo, "/") && !strings.Contains(redirectTo, "://") {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
} else if s.isTrustedTarget(redirectTo) {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, "/redirect?to="+url.QueryEscape(redirectTo), http.StatusSeeOther)
|
||||
}
|
||||
s.handleRedirect(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSignout renders a confirmation page on GET and deletes the session on POST.
|
||||
// It is wrapped in requireAuth for the POST route; the GET route may show the page
|
||||
// even for anonymous visitors (the confirm form just redirects to /signin).
|
||||
func (s *WebService) HandleSignout(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
@@ -255,8 +219,6 @@ func (s *WebService) HandleSignout(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleExternalRedirect shows a warning page when the user is about to
|
||||
// leave the site via an external redirect.
|
||||
func (s *WebService) HandleExternalRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
target := r.URL.Query().Get("to")
|
||||
if target == "" {
|
||||
@@ -264,7 +226,6 @@ func (s *WebService) HandleExternalRedirect(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// Trusted targets skip the warning and are redirected directly.
|
||||
if s.isTrustedTarget(target) {
|
||||
http.Redirect(w, r, target, http.StatusSeeOther)
|
||||
return
|
||||
@@ -272,7 +233,6 @@ func (s *WebService) HandleExternalRedirect(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
data, ok := s.basicViewData(w, r, "External Redirect")
|
||||
if !ok {
|
||||
// Not authenticated is fine — just show the warning without nav
|
||||
data = &ViewData{Title: "External Redirect"}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"uPortal/internal/auth"
|
||||
)
|
||||
|
||||
// HandleInvites lists all invite codes. Requires a super user.
|
||||
func (s *WebService) HandleInvites(w http.ResponseWriter, r *http.Request) {
|
||||
data, ok := s.basicViewData(w, r, "Invites")
|
||||
if !ok {
|
||||
@@ -26,7 +25,6 @@ func (s *WebService) HandleInvites(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, r, "invite/view.html", data)
|
||||
}
|
||||
|
||||
// HandleInviteCreate renders the creation form on GET and mints an invite on POST.
|
||||
func (s *WebService) HandleInviteCreate(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
@@ -69,7 +67,6 @@ func (s *WebService) HandleInviteCreate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleInviteDelete removes the invite identified by the form field "token".
|
||||
func (s *WebService) HandleInviteDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
http.Redirect(w, r, "/invites", http.StatusSeeOther)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"uPortal/internal/auth"
|
||||
)
|
||||
|
||||
// HandleJitsi renders the Jitsi room form on GET and redirects to Jitsi Meet on POST.
|
||||
func (s *WebService) HandleJitsi(w http.ResponseWriter, r *http.Request) {
|
||||
data, ok := s.basicViewData(w, r, "Jitsi")
|
||||
if !ok {
|
||||
@@ -59,8 +58,6 @@ func (s *WebService) HandleJitsi(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleJitsiGo is a GET endpoint that reads a "room" query parameter,
|
||||
// generates a JWT token, and redirects to the Jitsi Meet URL.
|
||||
func (s *WebService) HandleJitsiGo(w http.ResponseWriter, r *http.Request) {
|
||||
if s.jitsiCfg == nil || s.jitsiCfg.Secret == "" {
|
||||
http.Error(w, "Jitsi is not configured", http.StatusInternalServerError)
|
||||
@@ -96,10 +93,6 @@ func (s *WebService) HandleJitsiGo(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, s.jitsiCfg.MeetURL(room, token), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// avatarURL returns the app's avatar URL for the given user id, or an empty
|
||||
// string if the user has no stored avatar. If the Jitsi site domain is
|
||||
// configured, an absolute https:// URL is returned so Jitsi (running on a
|
||||
// different domain) can load the avatar; otherwise a relative URL is used.
|
||||
func avatarURL(r *http.Request, s *WebService, id int64) string {
|
||||
data, err := s.users.GetAvatar(r.Context(), id)
|
||||
if err != nil || len(data) == 0 {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"uPortal/internal/user"
|
||||
)
|
||||
|
||||
// HandleProfile shows the current user's own profile.
|
||||
func (s *WebService) HandleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
data, ok := s.basicViewData(w, r, "Profile")
|
||||
if !ok {
|
||||
@@ -26,7 +25,6 @@ func (s *WebService) HandleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, r, "profile/view.html", data)
|
||||
}
|
||||
|
||||
// HandleProfileEdit renders the edit form on GET and applies changes on POST.
|
||||
func (s *WebService) HandleProfileEdit(w http.ResponseWriter, r *http.Request) {
|
||||
data, ok := s.basicViewData(w, r, "Edit Profile")
|
||||
if !ok {
|
||||
@@ -50,7 +48,6 @@ func (s *WebService) HandleProfileEdit(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Display name: UpdateUser writes the displayname column via the user row.
|
||||
displayName := r.FormValue("display_name")
|
||||
if displayName != "" {
|
||||
data.User.DisplayName = &displayName
|
||||
@@ -80,8 +77,6 @@ func (s *WebService) HandleProfileEdit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleProfilePassword renders the password change form on GET and
|
||||
// applies the new password on POST.
|
||||
func (s *WebService) HandleProfilePassword(w http.ResponseWriter, r *http.Request) {
|
||||
data, ok := s.basicViewData(w, r, "Change Password")
|
||||
if !ok {
|
||||
@@ -145,7 +140,6 @@ func (s *WebService) HandleProfilePassword(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
// HandleAvatar serves a stored avatar image by user id.
|
||||
func (s *WebService) HandleAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,22 +15,14 @@ import (
|
||||
"uPortal/internal/user"
|
||||
)
|
||||
|
||||
// Cookie names and the CSRF form field name. These match the layout
|
||||
// template (templates/layouts/csrf.html) and the auth package helpers.
|
||||
const (
|
||||
SessionCookieName = "sessiontoken"
|
||||
CsrfCookieName = "csrftoken"
|
||||
CsrfFieldName = "formtoken"
|
||||
)
|
||||
|
||||
// CookieSecure controls the Secure attribute of the session cookie.
|
||||
// It is disabled by default so the app works over plain http://127.0.0.1
|
||||
// during development (browsers drop Secure cookies over plain http).
|
||||
// Production deployments behind HTTPS must flip this to true.
|
||||
var CookieSecure = false
|
||||
|
||||
// ViewData is the data bag passed to every page template. It carries the
|
||||
// CSRF token, the current user, and any collections the pages display.
|
||||
type ViewData struct {
|
||||
Title string
|
||||
IsSuperUser bool
|
||||
@@ -53,7 +45,6 @@ type WebService struct {
|
||||
log *log.Logger
|
||||
}
|
||||
|
||||
// New returns a WebService ready to be registered onto an http.ServeMux.
|
||||
func New(users *user.Repository, auths *auth.Repository, views *template.Service, db *sql.DB, jitsiCfg *jitsi.Config, trustedDomains []string, logger *log.Logger) *WebService {
|
||||
return &WebService{
|
||||
users: users,
|
||||
@@ -66,11 +57,7 @@ func New(users *user.Repository, auths *auth.Repository, views *template.Service
|
||||
}
|
||||
}
|
||||
|
||||
// isTrustedTarget reports whether target may skip the /redirect warning page.
|
||||
// Relative paths, empty hostnames and unknown hosts are never trusted;
|
||||
// localhost and 127.0.0.1 are always trusted.
|
||||
func (s *WebService) isTrustedTarget(target string) bool {
|
||||
// Handle URLs without a scheme (e.g. "meet.jit.si/room").
|
||||
if !strings.Contains(target, "://") {
|
||||
target = "https://" + target
|
||||
}
|
||||
@@ -80,7 +67,6 @@ func (s *WebService) isTrustedTarget(target string) bool {
|
||||
}
|
||||
host := u.Hostname()
|
||||
|
||||
// Loopback addresses are always trusted for full URL redirects.
|
||||
if host == "localhost" || host == "127.0.0.1" {
|
||||
return true
|
||||
}
|
||||
@@ -95,7 +81,6 @@ func (s *WebService) isTrustedTarget(target string) bool {
|
||||
|
||||
// Register wires every route (method + path) onto the mux.
|
||||
func (s *WebService) Register(mux *http.ServeMux) {
|
||||
// Helper to wrap every handler with logging middleware.
|
||||
handle := func(pattern string, handler http.HandlerFunc) {
|
||||
mux.HandleFunc(pattern, s.loggingMiddleware(handler))
|
||||
}
|
||||
@@ -152,15 +137,12 @@ func (s *WebService) Register(mux *http.ServeMux) {
|
||||
// loggingMiddleware logs every HTTP request. Static file endpoints are skipped.
|
||||
func (s *WebService) loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip logging for static files and health checks.
|
||||
if strings.HasPrefix(r.URL.Path, "/static/") {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Wrap the ResponseWriter to capture the status code.
|
||||
lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
next(lrw, r)
|
||||
|
||||
@@ -185,9 +167,6 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
||||
lrw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// render fills ViewData defaults and writes the response.
|
||||
// If data.FormToken is empty (e.g. for call sites that don't use
|
||||
// basicViewData), a CSRF token is minted as a fallback.
|
||||
func (s *WebService) render(w http.ResponseWriter, r *http.Request, page string, data *ViewData) {
|
||||
if data == nil {
|
||||
data = &ViewData{}
|
||||
@@ -212,7 +191,6 @@ func (s *WebService) render(w http.ResponseWriter, r *http.Request, page string,
|
||||
}
|
||||
}
|
||||
|
||||
// getUserInfo reads the session cookie and looks up the associated user.
|
||||
func (s *WebService) getUserInfo(r *http.Request) (*user.User, error) {
|
||||
cookie, err := r.Cookie(SessionCookieName)
|
||||
if err != nil {
|
||||
@@ -232,7 +210,6 @@ func (s *WebService) currentUser(r *http.Request) (*user.User, error) {
|
||||
return s.getUserInfo(r)
|
||||
}
|
||||
|
||||
// basicViewData builds a ViewData with the current user and CSRF token.
|
||||
func (s *WebService) basicViewData(w http.ResponseWriter, r *http.Request, title string) (*ViewData, bool) {
|
||||
user, err := s.getUserInfo(r)
|
||||
if err != nil {
|
||||
@@ -245,8 +222,6 @@ func (s *WebService) basicViewData(w http.ResponseWriter, r *http.Request, title
|
||||
return &ViewData{Title: title, User: user, IsSuperUser: user.SuperUser, FormToken: token}, true
|
||||
}
|
||||
|
||||
// requireAuth redirects unauthenticated visitors to /signin with a ?redirect=
|
||||
// parameter so they return to their intended page after login.
|
||||
func (s *WebService) requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := s.currentUser(r); err != nil {
|
||||
@@ -261,7 +236,6 @@ func (s *WebService) requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// requireSuperUser wraps a handler, requiring an authenticated super user.
|
||||
func (s *WebService) requireSuperUser(next http.HandlerFunc) http.HandlerFunc {
|
||||
return s.requireAuth(func(w http.ResponseWriter, r *http.Request) {
|
||||
u, err := s.currentUser(r)
|
||||
|
||||
+28
-8
@@ -1,13 +1,15 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"uPortal/internal/auth"
|
||||
|
||||
"github.com/mileusna/useragent"
|
||||
)
|
||||
|
||||
// HandleSessions lists the current user's active sessions.
|
||||
func (s *WebService) HandleSessions(w http.ResponseWriter, r *http.Request) {
|
||||
data, ok := s.basicViewData(w, r, "Sessions")
|
||||
if !ok {
|
||||
@@ -15,20 +17,41 @@ func (s *WebService) HandleSessions(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// A generous limit; the page renders whatever comes back.
|
||||
sessions, err := s.auths.GetSessionByUserId(r.Context(), data.User.Id, 100, 0)
|
||||
if err != nil {
|
||||
s.log.Error("failed to get sessions: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data.Sessions = sessions
|
||||
for _, session := range sessions {
|
||||
ua := useragent.Parse(session.Device)
|
||||
var class string
|
||||
if ua.Mobile {
|
||||
class = "Mobile"
|
||||
}
|
||||
if ua.Tablet {
|
||||
class = "Tablet"
|
||||
}
|
||||
if ua.Desktop {
|
||||
class = "Desktop"
|
||||
}
|
||||
if ua.Bot {
|
||||
class = "Bot"
|
||||
}
|
||||
|
||||
newSession := auth.Session{
|
||||
Device: fmt.Sprintf("%s, %s %s, %s %s %s", class, ua.Name, ua.Version, ua.OS, ua.OSVersion, ua.Device),
|
||||
Address: session.Address,
|
||||
Token: session.Token,
|
||||
CreatedAt: session.CreatedAt,
|
||||
ExpiresAt: session.ExpiresAt,
|
||||
UserId: session.UserId,
|
||||
}
|
||||
data.Sessions = append(data.Sessions, newSession)
|
||||
}
|
||||
s.render(w, r, "session/view.html", data)
|
||||
}
|
||||
|
||||
// HandleSessionEnd deletes a single session identified by the form field "token".
|
||||
// It enforces ownership: a user can only delete their own session unless they are a superuser.
|
||||
func (s *WebService) HandleSessionEnd(w http.ResponseWriter, r *http.Request) {
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
http.Redirect(w, r, "/sessions", http.StatusSeeOther)
|
||||
@@ -54,10 +77,8 @@ func (s *WebService) HandleSessionEnd(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if session.UserId == currentUser.Id {
|
||||
// User deleting their own session — use defense-in-depth method
|
||||
_ = s.auths.DeleteSessionByTokenAndUserId(r.Context(), token, currentUser.Id)
|
||||
} else if currentUser.SuperUser {
|
||||
// Superuser overriding — can delete any session
|
||||
_ = s.auths.DeleteSession(r.Context(), token)
|
||||
} else {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
@@ -67,7 +88,6 @@ func (s *WebService) HandleSessionEnd(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/sessions", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// HandleSessionEndAll ends every session belonging to the current user.
|
||||
func (s *WebService) HandleSessionEndAll(w http.ResponseWriter, r *http.Request) {
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
http.Redirect(w, r, "/sessions", http.StatusSeeOther)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"uPortal/internal/auth"
|
||||
)
|
||||
|
||||
// HandleUsers lists every user in the system. Requires a super user.
|
||||
func (s *WebService) HandleUsers(w http.ResponseWriter, r *http.Request) {
|
||||
data, ok := s.basicViewData(w, r, "Users")
|
||||
if !ok {
|
||||
@@ -25,7 +24,6 @@ func (s *WebService) HandleUsers(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, r, "user/view.html", data)
|
||||
}
|
||||
|
||||
// HandleUserResetPassword lets a super admin reset another user's password.
|
||||
func (s *WebService) HandleUserResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
@@ -69,7 +67,6 @@ func (s *WebService) HandleUserResetPassword(w http.ResponseWriter, r *http.Requ
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// HandleUserEndSessions ends all sessions for a given user. Requires a super user.
|
||||
func (s *WebService) HandleUserEndSessions(w http.ResponseWriter, r *http.Request) {
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
|
||||
Reference in new issue
Block a user