mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
223 lines
7.8 KiB
Go
223 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
|
|
"github.com/abhinavxd/libredesk/internal/envelope"
|
|
"github.com/abhinavxd/libredesk/internal/user/models"
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/zerodha/fastglue"
|
|
"github.com/zerodha/simplesessions/v3"
|
|
)
|
|
|
|
// authenticateUser handles both API key and session-based authentication
|
|
// Returns the authenticated user or an error
|
|
// For session-based auth, CSRF is checked for POST/PUT/DELETE requests
|
|
func authenticateUser(r *fastglue.Request, app *App) (models.User, error) {
|
|
var user models.User
|
|
|
|
// Check for Authorization header first (API key authentication)
|
|
apiKey, apiSecret, err := r.ParseAuthHeader(fastglue.AuthBasic | fastglue.AuthToken)
|
|
if err == nil && len(apiKey) > 0 && len(apiSecret) > 0 {
|
|
user, err = app.user.ValidateAPIKey(string(apiKey), string(apiSecret))
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// Session-based authentication - Check CSRF first.
|
|
method := string(r.RequestCtx.Method())
|
|
if method == "POST" || method == "PUT" || method == "DELETE" {
|
|
cookieToken := string(r.RequestCtx.Request.Header.Cookie("csrf_token"))
|
|
hdrToken := string(r.RequestCtx.Request.Header.Peek("X-CSRFTOKEN"))
|
|
|
|
// Match CSRF token from cookie and header.
|
|
if cookieToken == "" || hdrToken == "" || cookieToken != hdrToken {
|
|
app.lo.Error("csrf token mismatch", "method", method, "cookie_token", cookieToken, "header_token", hdrToken)
|
|
return user, envelope.NewError(envelope.PermissionError, app.i18n.T("auth.csrfTokenMismatch"), nil)
|
|
}
|
|
}
|
|
|
|
// Validate session and fetch user.
|
|
sessUser, err := app.auth.ValidateSession(r)
|
|
if err != nil || sessUser.ID <= 0 {
|
|
app.lo.Error("error validating session", "error", err)
|
|
return user, envelope.NewError(envelope.GeneralError, app.i18n.T("auth.invalidOrExpiredSession"), nil)
|
|
}
|
|
|
|
// Get agent user from cache or load it.
|
|
user, err = app.user.GetAgentCachedOrLoad(sessUser.ID)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
|
|
// Destroy session if user is disabled.
|
|
if !user.Enabled {
|
|
if err := app.auth.DestroySession(r); err != nil {
|
|
app.lo.Error("error destroying session", "error", err)
|
|
}
|
|
return user, envelope.NewError(envelope.PermissionError, app.i18n.T("user.accountDisabled"), nil)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// tryAuth attempts to authenticate the user and add them to the context but doesn't enforce authentication.
|
|
// Handlers can check if user exists in context optionally.
|
|
// Supports both API key authentication (Authorization header) and session-based authentication.
|
|
func tryAuth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
|
|
return func(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
|
|
// Try to authenticate user using shared authentication logic, but don't return errors
|
|
user, err := authenticateUser(r, app)
|
|
if err != nil {
|
|
// Authentication failed, but this is optional, so continue without user
|
|
return handler(r)
|
|
}
|
|
|
|
// Set user in context if authentication succeeded.
|
|
r.RequestCtx.SetUserValue("user", amodels.User{
|
|
ID: user.ID,
|
|
Email: user.Email.String,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
})
|
|
|
|
return handler(r)
|
|
}
|
|
}
|
|
|
|
// auth validates the session or API key and adds the user to the request context.
|
|
// Supports both API key authentication (Authorization header) and session-based authentication.
|
|
func auth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
|
|
return func(r *fastglue.Request) error {
|
|
var app = r.Context.(*App)
|
|
|
|
// Authenticate user using shared authentication logic
|
|
user, err := authenticateUser(r, app)
|
|
if err != nil {
|
|
if envErr, ok := err.(envelope.Error); ok {
|
|
if envErr.ErrorType == envelope.PermissionError {
|
|
return r.SendErrorEnvelope(http.StatusForbidden, envErr.Message, nil, envelope.PermissionError)
|
|
}
|
|
return r.SendErrorEnvelope(http.StatusUnauthorized, envErr.Message, nil, envelope.GeneralError)
|
|
}
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Set user in the request context.
|
|
r.RequestCtx.SetUserValue("user", amodels.User{
|
|
ID: user.ID,
|
|
Email: user.Email.String,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
})
|
|
|
|
return handler(r)
|
|
}
|
|
}
|
|
|
|
// perm checks if the user has the required permission to access the endpoint.
|
|
// Supports both API key authentication (Authorization header) and session-based authentication.
|
|
func perm(handler fastglue.FastRequestHandler, perm string) fastglue.FastRequestHandler {
|
|
return func(r *fastglue.Request) error {
|
|
var app = r.Context.(*App)
|
|
|
|
// Authenticate user using shared authentication logic
|
|
user, err := authenticateUser(r, app)
|
|
if err != nil {
|
|
if envErr, ok := err.(envelope.Error); ok {
|
|
if envErr.ErrorType == envelope.PermissionError {
|
|
return r.SendErrorEnvelope(http.StatusForbidden, envErr.Message, nil, envelope.PermissionError)
|
|
}
|
|
return r.SendErrorEnvelope(http.StatusUnauthorized, envErr.Message, nil, envelope.GeneralError)
|
|
}
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Split the permission string into object and action and enforce it.
|
|
parts := strings.Split(perm, ":")
|
|
if len(parts) != 2 {
|
|
return r.SendErrorEnvelope(http.StatusInternalServerError, app.i18n.Ts("globals.messages.invalid", "name", "{globals.terms.permission}"), nil, envelope.GeneralError)
|
|
}
|
|
object, action := parts[0], parts[1]
|
|
ok, err := app.authz.Enforce(user, object, action)
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(http.StatusInternalServerError, app.i18n.Ts("globals.messages.errorChecking", "name", "{globals.terms.permission}"), nil, envelope.GeneralError)
|
|
}
|
|
if !ok {
|
|
return r.SendErrorEnvelope(http.StatusForbidden, app.i18n.Ts("globals.messages.denied", "name", "{globals.terms.permission}"), nil, envelope.PermissionError)
|
|
}
|
|
|
|
// Set user in the request context.
|
|
r.RequestCtx.SetUserValue("user", amodels.User{
|
|
ID: user.ID,
|
|
Email: user.Email.String,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
})
|
|
|
|
return handler(r)
|
|
}
|
|
}
|
|
|
|
// authPage ensures the user is logged in; otherwise, redirects to the login page.
|
|
func authPage(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
|
|
return func(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
|
|
// Validate session.
|
|
user, err := app.auth.ValidateSession(r)
|
|
if err != nil {
|
|
// Session is not valid, destroy it and redirect to login.
|
|
if err != simplesessions.ErrInvalidSession {
|
|
app.lo.Error("error validating session", "error", err)
|
|
return r.SendErrorEnvelope(http.StatusUnauthorized, app.i18n.Ts("globals.messages.errorValidating", "name", "{globals.terms.session}"), nil, envelope.GeneralError)
|
|
}
|
|
if err := app.auth.DestroySession(r); err != nil {
|
|
app.lo.Error("error destroying session", "error", err)
|
|
}
|
|
}
|
|
|
|
// User is authenticated.
|
|
if user.ID > 0 {
|
|
return handler(r)
|
|
}
|
|
|
|
nextURI := r.RequestCtx.QueryArgs().Peek("next")
|
|
if len(nextURI) == 0 {
|
|
nextURI = r.RequestCtx.RequestURI()
|
|
}
|
|
return r.RedirectURI("/", fasthttp.StatusFound, map[string]any{
|
|
"next": string(nextURI),
|
|
}, "")
|
|
}
|
|
}
|
|
|
|
// notAuthPage allows access only if the user is not authenticated; otherwise, redirects to the user inbox.
|
|
func notAuthPage(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
|
|
return func(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
|
|
// Validate session.
|
|
user, err := app.auth.ValidateSession(r)
|
|
if err != nil {
|
|
app.lo.Error("error validating session", "error", err)
|
|
return r.SendErrorEnvelope(http.StatusUnauthorized, app.i18n.T("auth.invalidOrExpiredSessionClearCookie"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
if user.ID != 0 {
|
|
nextURI := string(r.RequestCtx.QueryArgs().Peek("next"))
|
|
if nextURI == "" {
|
|
nextURI = "/inboxes/assigned"
|
|
}
|
|
return r.RedirectURI(nextURI, fasthttp.StatusFound, nil, "")
|
|
}
|
|
return handler(r)
|
|
}
|
|
}
|