09514a6946
- skip SPA fallback for `/install` and `/install/*` in main server - keep install wizard accessible only during `RunInstallServer` phase - prevent post-install access to install UI
200 lines
6.8 KiB
Go
200 lines
6.8 KiB
Go
package admin
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/assimon/luuu/model/data"
|
|
"github.com/assimon/luuu/model/mdb"
|
|
appjwt "github.com/assimon/luuu/util/jwt"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// LoginRequest is the payload for admin login.
|
|
type LoginRequest struct {
|
|
Username string `json:"username" validate:"required" example:"admin"`
|
|
Password string `json:"password" validate:"required" example:"password123"`
|
|
}
|
|
|
|
// LoginResponse is the response for a successful login.
|
|
type LoginResponse struct {
|
|
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."`
|
|
Username string `json:"username" example:"admin"`
|
|
UserID uint64 `json:"user_id" example:"1"`
|
|
}
|
|
|
|
// ChangePasswordRequest is the payload for changing admin password.
|
|
type ChangePasswordRequest struct {
|
|
OldPassword string `json:"old_password" validate:"required" example:"old_pass"`
|
|
NewPassword string `json:"new_password" validate:"required|minLen:6" example:"new_pass123"`
|
|
}
|
|
|
|
// MeResponse wraps AdminUser with a default-password flag.
|
|
type MeResponse struct {
|
|
mdb.AdminUser
|
|
PasswordIsDefault bool `json:"password_is_default" example:"true"`
|
|
}
|
|
|
|
// InitialPasswordResponse is returned by the one-time initial password API.
|
|
type InitialPasswordResponse struct {
|
|
Username string `json:"username" example:"admin"`
|
|
Password string `json:"password" example:"a1b2c3d4e5f6"`
|
|
}
|
|
|
|
// Login verifies credentials, stamps last_login_at, returns a signed JWT.
|
|
// @Summary Admin login
|
|
// @Description Verify credentials and return a signed JWT token
|
|
// @Tags Admin Auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body admin.LoginRequest true "Login credentials"
|
|
// @Success 200 {object} response.ApiResponse{data=admin.LoginResponse}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/auth/login [post]
|
|
func (c *BaseAdminController) Login(ctx echo.Context) error {
|
|
req := new(LoginRequest)
|
|
if err := ctx.Bind(req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := c.ValidateStruct(ctx, req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
user, err := data.GetAdminUserByUsername(req.Username)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if user.ID == 0 || !data.VerifyPassword(user.PasswordHash, req.Password) {
|
|
return c.FailJson(ctx, errors.New("invalid username or password"))
|
|
}
|
|
if user.Status != mdb.AdminUserStatusEnable {
|
|
return c.FailJson(ctx, errors.New("user disabled"))
|
|
}
|
|
token, err := appjwt.Sign(user.ID, user.Username)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
err = data.TouchAdminUserLastLogin(user.ID)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, LoginResponse{
|
|
Token: token,
|
|
Username: user.Username,
|
|
UserID: user.ID,
|
|
})
|
|
}
|
|
|
|
// GetInitialPassword returns the one-time initial admin password.
|
|
// @Summary Get initial admin password (one-time)
|
|
// @Description Returns the initial random admin password once, then invalidates it.
|
|
// @Tags Admin Auth
|
|
// @Produce json
|
|
// @Success 200 {object} response.ApiResponse{data=admin.InitialPasswordResponse}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/auth/init-password [get]
|
|
func (c *BaseAdminController) GetInitialPassword(ctx echo.Context) error {
|
|
password, err := data.ConsumeInitialAdminPassword()
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, InitialPasswordResponse{
|
|
Username: "admin",
|
|
Password: password,
|
|
})
|
|
}
|
|
|
|
// GetInitialPasswordHash returns the initial-password fingerprint so the
|
|
// frontend can warn until the password is changed.
|
|
// @Summary Get initial admin password hash
|
|
// @Description Returns the initial password hash and password-changed flag.
|
|
// @Tags Admin Auth
|
|
// @Produce json
|
|
// @Success 200 {object} response.ApiResponse{data=data.InitialAdminPasswordHashInfo}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/auth/init-password-hash [get]
|
|
func (c *BaseAdminController) GetInitialPasswordHash(ctx echo.Context) error {
|
|
info, err := data.GetInitialAdminPasswordHashInfo()
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, info)
|
|
}
|
|
|
|
// Logout is a no-op stub — tokens are stateless and the frontend
|
|
// discards them. Kept for API symmetry with the documented spec.
|
|
// @Summary Admin logout
|
|
// @Description Logout (no-op, frontend discards token)
|
|
// @Tags Admin Auth
|
|
// @Security AdminJWT
|
|
// @Produce json
|
|
// @Success 200 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/auth/logout [post]
|
|
func (c *BaseAdminController) Logout(ctx echo.Context) error {
|
|
return c.SucJson(ctx, nil)
|
|
}
|
|
|
|
// Me returns the currently authenticated admin user profile.
|
|
// @Summary Get current admin profile
|
|
// @Description Returns the currently authenticated admin user
|
|
// @Tags Admin Auth
|
|
// @Security AdminJWT
|
|
// @Produce json
|
|
// @Success 200 {object} response.ApiResponse{data=admin.MeResponse}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/auth/me [get]
|
|
func (c *BaseAdminController) Me(ctx echo.Context) error {
|
|
uid := currentAdminUserID(ctx)
|
|
if uid == 0 {
|
|
return c.FailJson(ctx, errors.New("unauthorized"))
|
|
}
|
|
user, err := data.GetAdminUserByID(uid)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if user.ID == 0 {
|
|
return c.FailJson(ctx, errors.New("user not found"))
|
|
}
|
|
// Warn the frontend when the operator hasn't changed the seeded
|
|
// initial password.
|
|
isDefault := data.IsUsingInitialAdminPassword()
|
|
return c.SucJson(ctx, MeResponse{
|
|
AdminUser: *user,
|
|
PasswordIsDefault: isDefault,
|
|
})
|
|
}
|
|
|
|
// ChangePassword requires the old password, updates to the new one.
|
|
// @Summary Change admin password
|
|
// @Description Change the current admin user's password
|
|
// @Tags Admin Auth
|
|
// @Security AdminJWT
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body admin.ChangePasswordRequest true "Password payload"
|
|
// @Success 200 {object} response.ApiResponse
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/auth/password [post]
|
|
func (c *BaseAdminController) ChangePassword(ctx echo.Context) error {
|
|
uid := currentAdminUserID(ctx)
|
|
if uid == 0 {
|
|
return c.FailJson(ctx, errors.New("unauthorized"))
|
|
}
|
|
req := new(ChangePasswordRequest)
|
|
if err := ctx.Bind(req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := c.ValidateStruct(ctx, req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
user, err := data.GetAdminUserByID(uid)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if user.ID == 0 || !data.VerifyPassword(user.PasswordHash, req.OldPassword) {
|
|
return c.FailJson(ctx, errors.New("old password incorrect"))
|
|
}
|
|
if err := data.UpdateAdminUserPassword(uid, req.NewPassword); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, nil)
|
|
}
|