Files
agent-desk-ng/internal/handlers/api/auth_handler.go

202 lines
5.4 KiB
Go

package api
import (
"agent-desk/internal/builders"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/httpx"
"agent-desk/internal/pkg/httpx/params"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/pkg/loginaudience"
"agent-desk/internal/services"
"log/slog"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
)
func Login(ctx *gin.Context) {
cfg := config.Current()
if !cfg.Auth.IsPasswordLoginEnabled() {
httpx.WriteJSON(ctx, errorsx.ForbiddenI18n("error.auth.passwordLoginDisabled"))
return
}
req := request.LoginRequest{}
if err := params.ReadJSON(ctx, &req); err != nil {
httpx.WriteJSON(ctx, err)
return
}
ret, err := services.AuthService.Login(req, cfg.Auth, ctx.ClientIP(), ctx.GetHeader("User-Agent"))
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, ret)
}
func PublicConfig(ctx *gin.Context) {
cfg := config.Current()
// Saved branding wins over configuration, so an operator can rename a
// deployment from the dashboard without touching the file it is running from.
branding := services.ResolveBranding()
httpx.WriteJSON(ctx, &response.PublicConfigResponse{
Language: cfg.LanguageOrDefault(),
CompanyName: branding.CompanyName,
CompanyLogoURL: branding.CompanyLogoURL,
PasswordLoginEnabled: cfg.Auth.IsPasswordLoginEnabled(),
OIDCEnabled: cfg.OIDC.Enabled,
})
}
// redirectPortalSSORejected sends a portal-initiated enterprise SSO attempt back
// to the portal login with a reason, instead of starting a flow that would hand
// a visitor a staff session.
func redirectPortalSSORejected(ctx *gin.Context, errorParam string) {
ctx.Redirect(
http.StatusFound,
"/support/login?"+errorParam+"="+url.QueryEscape(i18nx.T(ctx, "error.auth.staffSsoOnPortal")),
)
}
func rejectPortalSSO(ctx *gin.Context, errorParam string) bool {
if !loginaudience.Resolve(ctx.Query("next")).IsPortal() {
return false
}
slog.Warn("rejected enterprise SSO started from the visitor portal",
"provider", errorParam,
"next", ctx.Query("next"),
"clientIp", ctx.ClientIP(),
)
redirectPortalSSORejected(ctx, errorParam)
return true
}
func OIDCLogin(ctx *gin.Context) {
if rejectPortalSSO(ctx, "oidcError") {
return
}
loginURL, err := services.OIDCLoginService.BuildOIDCLoginURL(ctx.Query("next"))
if err != nil {
ctx.Redirect(http.StatusFound, "/dashboard/login?oidcError="+url.QueryEscape(loginErrorMessage(err.Error())))
return
}
ctx.Redirect(http.StatusFound, loginURL)
}
func OIDCCallback(ctx *gin.Context) {
if oauthErr := ctx.Query("error"); oauthErr != "" {
desc := ctx.Query("error_description")
slog.Warn("oidc callback returned oauth error", "error", oauthErr, "description", desc)
errMsg := oauthErr
if desc != "" {
errMsg += ": " + desc
}
ctx.Redirect(http.StatusFound, "/dashboard/login?oidcError="+url.QueryEscape(errMsg))
return
}
code := ctx.Query("code")
state := ctx.Query("state")
if strings.TrimSpace(code) == "" {
slog.Warn("oidc callback missing code", "rawQuery", ctx.Request.URL.RawQuery)
}
cfg := config.Current()
ticket, next, err := services.OIDCLoginService.LoginByOIDC(
ctx.Request.Context(),
code,
state,
cfg.Auth,
ctx.ClientIP(),
ctx.GetHeader("User-Agent"),
)
if err != nil {
slog.Error("oidc callback login failed", "error", err)
ctx.Redirect(http.StatusFound, "/dashboard/login?oidcError="+url.QueryEscape(loginErrorMessage(err.Error())))
return
}
ctx.Redirect(http.StatusFound, "/dashboard/login/oidc/callback?ticket="+url.QueryEscape(ticket)+"&next="+url.QueryEscape(next))
}
func OIDCExchange(ctx *gin.Context) {
req := request.OIDCExchangeRequest{}
if err := params.ReadJSON(ctx, &req); err != nil {
httpx.WriteJSON(ctx, err)
return
}
ret, err := services.OIDCLoginService.ExchangeOIDCLoginTicket(req.Ticket)
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, ret)
}
func Logout(ctx *gin.Context) {
if err := services.AuthService.Logout(ctx.GetHeader("Authorization")); err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, nil)
}
func Profile(ctx *gin.Context) {
ret, err := services.AuthService.CurrentProfile(ctx)
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, ret)
}
func UpdateProfile(ctx *gin.Context) {
req := request.UpdateProfileRequest{}
if err := params.ReadJSON(ctx, &req); err != nil {
httpx.WriteJSON(ctx, err)
return
}
ret, err := services.AuthService.UpdateProfile(ctx, req)
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, ret)
}
func UploadProfileAvatar(ctx *gin.Context) {
principal, err := services.AuthService.Authenticate(ctx)
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
header, err := ctx.FormFile("file")
if err != nil {
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0323"))
return
}
if !strings.HasPrefix(strings.ToLower(header.Header.Get("Content-Type")), "image/") {
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0090"))
return
}
item, err := services.AssetService.UploadFile(header, "avatars", principal)
if err != nil {
httpx.WriteJSON(ctx, err)
return
}
httpx.WriteJSON(ctx, builders.BuildAsset(item))
}
func loginErrorMessage(message string) string {
if idx := strings.Index(message, ": "); idx >= 0 {
message = message[idx+2:]
}
return message
}