Initial commit
This commit is contained in:
commit
c578b91c2d
46 files changed
+3241
No files matched your search
@@ -0,0 +1,283 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"uPortal/internal/auth"
|
||||
)
|
||||
|
||||
// HandleSignin renders the sign-in page on GET and authenticates on POST.
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
redirect := r.URL.Query().Get("redirect")
|
||||
s.render(w, r, "auth/signin.html", &ViewData{Redirect: redirect})
|
||||
|
||||
case http.MethodPost:
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
s.render(w, r, "auth/signin.html", &ViewData{Error: "Invalid form, please try again"})
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
remember := r.FormValue("remember")
|
||||
|
||||
user, err := s.users.GetUserByLogin(r.Context(), username)
|
||||
if err != nil || !auth.CheckPasswordHash(password, user.Password) {
|
||||
s.render(w, r, "auth/signin.html", &ViewData{Error: "Invalid username or password"})
|
||||
return
|
||||
}
|
||||
|
||||
token, device, address, err := auth.CreateSession(r, user.Id)
|
||||
if err != nil {
|
||||
s.render(w, r, "auth/signin.html", &ViewData{Error: "Could not create session"})
|
||||
return
|
||||
}
|
||||
|
||||
// Session expires 24h from now; "remember" extends it to 30 days.
|
||||
expiresAt := time.Now().UTC().Add(24 * time.Hour)
|
||||
maxAge := 0 // zero => session cookie (until browser closes)
|
||||
if remember == "on" {
|
||||
expiresAt = time.Now().UTC().Add(30 * 24 * time.Hour)
|
||||
maxAge = 30 * 24 * 3600
|
||||
}
|
||||
|
||||
if _, err := s.auths.CreateSession(r.Context(), token, user.Id, device, address, expiresAt); err != nil {
|
||||
s.render(w, r, "auth/signin.html", &ViewData{Error: "Could not save session"})
|
||||
return
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: SessionCookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: CookieSecure,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
redirect := r.URL.Query().Get("redirect")
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Redirect: redirect})
|
||||
|
||||
case http.MethodPost:
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Invalid form, please try again"})
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
confirm := r.FormValue("confirm")
|
||||
inviteToken := r.FormValue("invite")
|
||||
|
||||
if password != confirm {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Passwords do not match"})
|
||||
return
|
||||
}
|
||||
if len(password) < 6 {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Password must be at least 6 characters"})
|
||||
return
|
||||
}
|
||||
if inviteToken == "" {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Invite code is required"})
|
||||
return
|
||||
}
|
||||
|
||||
invite, err := s.auths.GetInviteByToken(r.Context(), inviteToken)
|
||||
if err != nil {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Invalid invite code"})
|
||||
return
|
||||
}
|
||||
if !invite.IsValid() {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Invite code has expired or been used up"})
|
||||
return
|
||||
}
|
||||
|
||||
// Decrement uses; delete the invite once exhausted.
|
||||
invite.Uses--
|
||||
if invite.Uses <= 0 {
|
||||
if err := s.auths.DeleteInvite(r.Context(), invite.Token); err != nil {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Could not process invite"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := s.auths.UpdateInvite(r.Context(), invite); err != nil {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Could not process invite"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser stores the password as-is, so hash it first.
|
||||
hashed, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Could not create account"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.users.CreateUser(r.Context(), username, hashed)
|
||||
if err != nil {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Username already taken"})
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-login.
|
||||
token, device, address, err := auth.CreateSession(r, user.Id)
|
||||
if err != nil {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Could not create session"})
|
||||
return
|
||||
}
|
||||
|
||||
expiresAt := time.Now().UTC().Add(24 * time.Hour)
|
||||
if _, err := s.auths.CreateSession(r.Context(), token, user.Id, device, address, expiresAt); err != nil {
|
||||
s.render(w, r, "auth/signup.html", &ViewData{Error: "Could not save session"})
|
||||
return
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: SessionCookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
data, ok := s.basicViewData(w, r, "Sign Out")
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/signin?redirect="+url.PathEscape(r.URL.RequestURI()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.render(w, r, "auth/signout.html", data)
|
||||
|
||||
case http.MethodPost:
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
s.render(w, r, "auth/signout.html", &ViewData{Error: "Invalid form, please try again"})
|
||||
return
|
||||
}
|
||||
|
||||
if cookie, err := r.Cookie(SessionCookieName); err == nil {
|
||||
_ = s.auths.DeleteSession(r.Context(), cookie.Value)
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: SessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
})
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Trusted targets skip the warning and are redirected directly.
|
||||
if s.isTrustedTarget(target) {
|
||||
http.Redirect(w, r, target, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
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"}
|
||||
token, _ := auth.SetCsrfCookie(w, r, CsrfCookieName)
|
||||
data.FormToken = token
|
||||
}
|
||||
|
||||
data.Redirect = target
|
||||
s.render(w, r, "redirect/warning.html", data)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
invites, err := s.auths.GetInvites(r.Context(), 1000, 0)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data.Invites = invites
|
||||
|
||||
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:
|
||||
data, ok := s.basicViewData(w, r, "Create Invite")
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.render(w, r, "invite/create.html", data)
|
||||
|
||||
case http.MethodPost:
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
s.render(w, r, "invite/create.html", &ViewData{Error: "Invalid form, please try again"})
|
||||
return
|
||||
}
|
||||
|
||||
uses := int64(1)
|
||||
hours := int64(24)
|
||||
if v, err := strconv.ParseInt(r.FormValue("uses"), 10, 64); err == nil && v > 0 {
|
||||
uses = v
|
||||
}
|
||||
if v, err := strconv.ParseInt(r.FormValue("expires_hours"), 10, 64); err == nil && v > 0 {
|
||||
hours = v
|
||||
}
|
||||
|
||||
expiresAt := time.Now().UTC().Add(time.Duration(hours) * time.Hour)
|
||||
|
||||
token, err := auth.GenerateToken(32)
|
||||
if err != nil {
|
||||
s.render(w, r, "invite/create.html", &ViewData{Error: "Could not generate invite token"})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.auths.CreateInvite(r.Context(), token, uses, expiresAt); err != nil {
|
||||
s.render(w, r, "invite/create.html", &ViewData{Error: "Could not create invite"})
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/invites", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
||||
token := r.FormValue("token")
|
||||
if token != "" {
|
||||
_ = s.auths.DeleteInvite(r.Context(), token)
|
||||
}
|
||||
http.Redirect(w, r, "/invites", http.StatusSeeOther)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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 {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.render(w, r, "jitsi/token.html", data)
|
||||
|
||||
case http.MethodPost:
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
data.Error = "Invalid form, please try again"
|
||||
s.render(w, r, "jitsi/token.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
if s.jitsiCfg == nil || s.jitsiCfg.Secret == "" {
|
||||
data.Error = "Jitsi is not configured"
|
||||
s.render(w, r, "jitsi/token.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
room := r.FormValue("room")
|
||||
if room == "" {
|
||||
data.Error = "Room name is required"
|
||||
s.render(w, r, "jitsi/token.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
avatar := avatarURL(r, s, data.User.Id)
|
||||
|
||||
token, err := s.jitsiCfg.GenerateToken(
|
||||
strconv.FormatInt(data.User.Id, 10),
|
||||
data.User.EffectiveDisplayName(),
|
||||
room,
|
||||
data.User.SuperUser,
|
||||
avatar,
|
||||
)
|
||||
if err != nil {
|
||||
data.Error = "Could not generate token"
|
||||
s.render(w, r, "jitsi/token.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, s.jitsiCfg.MeetURL(room, token), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.currentUser(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
room := r.URL.Query().Get("room")
|
||||
if room == "" {
|
||||
http.Redirect(w, r, "/jitsi", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
avatar := avatarURL(r, s, user.Id)
|
||||
|
||||
token, err := s.jitsiCfg.GenerateToken(
|
||||
strconv.FormatInt(user.Id, 10),
|
||||
user.EffectiveDisplayName(),
|
||||
room,
|
||||
user.SuperUser,
|
||||
avatar,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
return ""
|
||||
}
|
||||
path := fmt.Sprintf("/avatar/%d", id)
|
||||
if s.jitsiCfg != nil && s.jitsiCfg.SiteDomain != "" {
|
||||
return fmt.Sprintf("https://%s%s", s.jitsiCfg.SiteDomain, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"uPortal/internal/auth"
|
||||
"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 {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
full, err := s.users.GetUserById(r.Context(), data.User.Id)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data.User = full
|
||||
|
||||
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 {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.render(w, r, "profile/edit.html", data)
|
||||
|
||||
case http.MethodPost:
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
data.Error = "Could not parse form"
|
||||
s.render(w, r, "profile/edit.html", data)
|
||||
return
|
||||
}
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
data.Error = "Invalid form, please try again"
|
||||
s.render(w, r, "profile/edit.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Display name: UpdateUser writes the displayname column via the user row.
|
||||
displayName := r.FormValue("display_name")
|
||||
if displayName != "" {
|
||||
data.User.DisplayName = &displayName
|
||||
if err := s.users.UpdateUser(r.Context(), data.User); err != nil {
|
||||
data.Error = "Could not save display name"
|
||||
s.render(w, r, "profile/edit.html", data)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar: upload a JPG/PNG <= 256x256 (validated by the user package).
|
||||
if _, _, err := r.FormFile("avatar"); err == nil {
|
||||
bytes, _, err := user.ExtractUploadedImageBytes(r, "avatar")
|
||||
if err != nil {
|
||||
data.Error = "Invalid avatar image: " + err.Error()
|
||||
s.render(w, r, "profile/edit.html", data)
|
||||
return
|
||||
}
|
||||
if err := s.users.CreateAvatar(r.Context(), data.User.Id, bytes); err != nil {
|
||||
data.Error = "Could not save avatar"
|
||||
s.render(w, r, "profile/edit.html", data)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/profile", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.render(w, r, "profile/password.html", data)
|
||||
|
||||
case http.MethodPost:
|
||||
if err := r.ParseForm(); err != nil {
|
||||
data.Error = "Could not parse form"
|
||||
s.render(w, r, "profile/password.html", data)
|
||||
return
|
||||
}
|
||||
if err := auth.ValidateCsrf(r, CsrfCookieName, CsrfFieldName); err != nil {
|
||||
data.Error = "Invalid form token"
|
||||
s.render(w, r, "profile/password.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
oldPassword := r.FormValue("old_password")
|
||||
newPassword := r.FormValue("new_password")
|
||||
confirmPassword := r.FormValue("confirm_password")
|
||||
|
||||
if !auth.CheckPasswordHash(oldPassword, data.User.Password) {
|
||||
data.Error = "Current password is incorrect"
|
||||
s.render(w, r, "profile/password.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
if len(newPassword) < 6 {
|
||||
data.Error = "New password must be at least 6 characters"
|
||||
s.render(w, r, "profile/password.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
if newPassword != confirmPassword {
|
||||
data.Error = "New passwords do not match"
|
||||
s.render(w, r, "profile/password.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
data.Error = "Could not hash password"
|
||||
s.render(w, r, "profile/password.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
data.User.Password = hashedPassword
|
||||
if err := s.users.UpdateUser(r.Context(), data.User); err != nil {
|
||||
data.Error = "Could not update password"
|
||||
s.render(w, r, "profile/password.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/profile", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := s.users.GetAvatar(r.Context(), id)
|
||||
if err != nil || len(data) == 0 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", http.DetectContentType(data))
|
||||
w.Write(data)
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"uPortal/internal/auth"
|
||||
"uPortal/internal/jitsi"
|
||||
"uPortal/internal/log"
|
||||
"uPortal/internal/template"
|
||||
"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
|
||||
FormToken string
|
||||
Error string
|
||||
User *user.User
|
||||
Users []user.User
|
||||
Sessions []auth.Session
|
||||
Invites []auth.Invite
|
||||
Redirect string
|
||||
}
|
||||
|
||||
type WebService struct {
|
||||
users *user.Repository
|
||||
auths *auth.Repository
|
||||
views *template.Service
|
||||
db *sql.DB
|
||||
jitsiCfg *jitsi.Config
|
||||
trustedDomains []string
|
||||
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,
|
||||
auths: auths,
|
||||
views: views,
|
||||
db: db,
|
||||
jitsiCfg: jitsiCfg,
|
||||
trustedDomains: trustedDomains,
|
||||
log: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
u, err := url.Parse(target)
|
||||
if err != nil || u.Hostname() == "" {
|
||||
return false
|
||||
}
|
||||
host := u.Hostname()
|
||||
|
||||
// Loopback addresses are always trusted for full URL redirects.
|
||||
if host == "localhost" || host == "127.0.0.1" {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, domain := range s.trustedDomains {
|
||||
if strings.EqualFold(host, strings.TrimSpace(domain)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// Password
|
||||
handle("GET /profile/password", s.requireAuth(s.HandleProfilePassword))
|
||||
handle("POST /profile/password", s.requireAuth(s.HandleProfilePassword))
|
||||
|
||||
// Profile
|
||||
// "/{$}" matches only the root path exactly; any other unmatched path
|
||||
// falls through to the mux default 404 instead of being served here.
|
||||
handle("GET /{$}", s.requireAuth(s.HandleProfile))
|
||||
handle("GET /profile", s.requireAuth(s.HandleProfile))
|
||||
handle("GET /profile/edit", s.requireAuth(s.HandleProfileEdit))
|
||||
handle("POST /profile/edit", s.requireAuth(s.HandleProfileEdit))
|
||||
handle("GET /avatar/{id}", s.HandleAvatar)
|
||||
|
||||
// Auth
|
||||
handle("GET /signin", s.HandleSignin)
|
||||
handle("POST /signin", s.HandleSignin)
|
||||
handle("GET /signup", s.HandleSignup)
|
||||
handle("POST /signup", s.HandleSignup)
|
||||
handle("GET /signout", s.requireAuth(s.HandleSignout))
|
||||
handle("POST /signout", s.requireAuth(s.HandleSignout))
|
||||
|
||||
// External redirect warning
|
||||
handle("GET /redirect", s.HandleExternalRedirect)
|
||||
|
||||
// Sessions
|
||||
handle("GET /sessions", s.requireAuth(s.HandleSessions))
|
||||
handle("POST /sessions/end", s.requireAuth(s.HandleSessionEnd))
|
||||
handle("POST /sessions/end-all", s.requireAuth(s.HandleSessionEndAll))
|
||||
|
||||
// Users (super user only)
|
||||
handle("GET /users", s.requireSuperUser(s.HandleUsers))
|
||||
handle("POST /users/reset-password", s.requireSuperUser(s.HandleUserResetPassword))
|
||||
handle("POST /users/end-sessions", s.requireSuperUser(s.HandleUserEndSessions))
|
||||
|
||||
// Invites (super user only)
|
||||
handle("GET /invites", s.requireSuperUser(s.HandleInvites))
|
||||
handle("GET /invites/create", s.requireSuperUser(s.HandleInviteCreate))
|
||||
handle("POST /invites/create", s.requireSuperUser(s.HandleInviteCreate))
|
||||
handle("POST /invites/delete", s.requireSuperUser(s.HandleInviteDelete))
|
||||
|
||||
// Jitsi
|
||||
handle("GET /jitsi", s.requireAuth(s.HandleJitsi))
|
||||
handle("POST /jitsi", s.requireAuth(s.HandleJitsi))
|
||||
handle("GET /jitsi/go", s.requireAuth(s.HandleJitsiGo))
|
||||
|
||||
// nginx auth_request endpoint — validates session, returns identity headers.
|
||||
handle("GET /auth", s.HandleAuthRequest)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
query := r.URL.RawQuery
|
||||
path := r.URL.Path
|
||||
if query != "" {
|
||||
path = path + "?" + query
|
||||
}
|
||||
|
||||
s.log.Info("%s %s -> %d in %s",
|
||||
r.Method, path, lrw.statusCode, time.Since(start).Round(time.Millisecond))
|
||||
}
|
||||
}
|
||||
|
||||
type loggingResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
||||
lrw.statusCode = code
|
||||
lrw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// render mints a CSRF token, fills ViewData defaults, and writes the response.
|
||||
func (s *WebService) render(w http.ResponseWriter, r *http.Request, page string, data *ViewData) {
|
||||
token, err := auth.SetCsrfCookie(w, r, CsrfCookieName)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if data == nil {
|
||||
data = &ViewData{}
|
||||
}
|
||||
data.FormToken = token
|
||||
if data.Title == "" {
|
||||
data.Title = page
|
||||
}
|
||||
if data.User != nil && data.User.SuperUser {
|
||||
data.IsSuperUser = true
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := s.views.Render(w, page, data); err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
session, err := s.auths.GetSessionByToken(r.Context(), cookie.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !session.IsValid() {
|
||||
return nil, fmt.Errorf("session expired")
|
||||
}
|
||||
return s.users.GetUserById(r.Context(), session.UserId)
|
||||
}
|
||||
|
||||
func (s *WebService) currentUser(r *http.Request) (*user.User, error) {
|
||||
return s.getUserInfo(r)
|
||||
}
|
||||
|
||||
// basicViewData builds a ViewData with the current user and a CSRF token.
|
||||
func (s *WebService) basicViewData(w http.ResponseWriter, r *http.Request, title string) (*ViewData, bool) {
|
||||
user, err := s.getUserInfo(r)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
token, err := auth.SetCsrfCookie(w, r, CsrfCookieName)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
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 {
|
||||
redirectTo := "/signin"
|
||||
if r.URL.Path != "/" && r.URL.Path != "" {
|
||||
redirectTo = "/signin?redirect=" + url.PathEscape(r.URL.RequestURI())
|
||||
}
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil || u == nil || !u.SuperUser {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"uPortal/internal/auth"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
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
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
currentUser, err := s.currentUser(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
token := r.FormValue("token")
|
||||
if token == "" {
|
||||
http.Redirect(w, r, "/sessions", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := s.auths.GetSessionByToken(r.Context(), token)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/sessions", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := s.currentUser(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/signin?redirect="+url.PathEscape(r.URL.RequestURI()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.auths.DeleteSessionsByUserId(r.Context(), u.Id)
|
||||
http.Redirect(w, r, "/sessions", http.StatusSeeOther)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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 {
|
||||
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := s.users.GetUsers(r.Context(), 1000, 0)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data.Users = users
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
userIdStr := r.FormValue("user_id")
|
||||
newPassword := r.FormValue("new_password")
|
||||
|
||||
userId, err := strconv.ParseInt(userIdStr, 10, 64)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if len(newPassword) < 6 {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.users.GetUserById(r.Context(), userId)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = hashedPassword
|
||||
s.users.UpdateUser(r.Context(), user)
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
userIdStr := r.FormValue("user_id")
|
||||
userId, err := strconv.ParseInt(userIdStr, 10, 64)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
s.auths.DeleteSessionsByUserId(r.Context(), userId)
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package web
|
||||
|
||||
import "net/http"
|
||||
|
||||
// HandleAuthRequest is an nginx auth_request endpoint. It validates the
|
||||
// session cookie and, on success, returns the authenticated user's identity
|
||||
// as response headers that nginx forwards to the upstream application.
|
||||
//
|
||||
// Expected usage in nginx:
|
||||
//
|
||||
// location / {
|
||||
// auth_request /auth;
|
||||
// auth_request_set $webauth_user $upstream_http_x_webauth_user;
|
||||
// auth_request_set $webauth_email $upstream_http_x_webauth_email;
|
||||
// auth_request_set $webauth_display_name $upstream_http_x_webauth_display_name;
|
||||
// proxy_set_header X-WEBAUTH-USER $webauth_user;
|
||||
// proxy_set_header X-WEBAUTH-EMAIL $webauth_email;
|
||||
// proxy_set_header X-WEBAUTH-DISPLAY-NAME $webauth_display_name;
|
||||
// proxy_pass http://backend;
|
||||
// }
|
||||
//
|
||||
// location = /auth {
|
||||
// internal;
|
||||
// proxy_pass http://backend/auth;
|
||||
// proxy_pass_request_body off;
|
||||
// proxy_set_header Content-Length "";
|
||||
// proxy_set_header X-Original-URI $request_uri;
|
||||
// }
|
||||
func (s *WebService) HandleAuthRequest(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(SessionCookieName)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := s.auths.GetSessionByToken(r.Context(), cookie.Value)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !session.IsValid() {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.users.GetUserById(r.Context(), session.UserId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("X-WEBAUTH-USER", user.Login)
|
||||
w.Header().Set("X-WEBAUTH-EMAIL", user.Login)
|
||||
w.Header().Set("X-WEBAUTH-DISPLAY-NAME", user.EffectiveDisplayName())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
Reference in new issue
Block a user