feat(api/v2): add public auth routes (register, password, confirm, link-share)
Port the unauthenticated local-account flows and link-share auth to
/api/v2, delegating to the shared business logic:
- POST /register (404 when registration is disabled)
- POST /user/password/token, POST /user/password/reset
- POST /user/confirm
- POST /shares/{share}/auth
Local-account routes register only when local auth is enabled and the
link-share route only when link sharing is enabled, mirroring v1. Each
operation opts out of global auth and its path is added to
unauthenticatedAPIPaths.
This commit is contained in:
parent
22e3659153
commit
53ac0f73c1
|
|
@ -0,0 +1,175 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package apiv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/routes/api/shared"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
)
|
||||
|
||||
// publicSecurity is the empty security requirement that opts an operation out of
|
||||
// the globally-applied JWT/API-token auth. The matching Echo path must also be
|
||||
// listed in unauthenticatedAPIPaths so the token middleware lets it through.
|
||||
var publicSecurity = []map[string][]string{}
|
||||
|
||||
// registerUserBody is the response wrapper for the registration endpoint.
|
||||
type registerUserBody struct {
|
||||
Body *user.User
|
||||
}
|
||||
|
||||
// messageBody carries a human-readable confirmation for endpoints that report
|
||||
// success without returning a resource (password reset, email confirm).
|
||||
type messageBody struct {
|
||||
Body struct {
|
||||
Message string `json:"message" readOnly:"true" doc:"A human-readable confirmation message."`
|
||||
}
|
||||
}
|
||||
|
||||
// linkShareTokenBody wraps the issued link-share auth token and its share.
|
||||
type linkShareTokenBody struct {
|
||||
Body *shared.LinkShareToken
|
||||
}
|
||||
|
||||
func init() { AddRouteRegistrar(RegisterPublicAuthRoutes) }
|
||||
|
||||
// RegisterPublicAuthRoutes wires the unauthenticated local-account flows
|
||||
// (registration, password reset, email confirmation) and the link-share auth
|
||||
// endpoint. The local-account flows mirror v1 by only registering when local
|
||||
// auth is enabled; the link-share endpoint follows ServiceEnableLinkSharing.
|
||||
func RegisterPublicAuthRoutes(api huma.API) {
|
||||
if config.AuthLocalEnabled.GetBool() {
|
||||
registerLocalAuthRoutes(api)
|
||||
}
|
||||
|
||||
if config.ServiceEnableLinkSharing.GetBool() {
|
||||
Register(api, huma.Operation{
|
||||
OperationID: "auth-link-share",
|
||||
Summary: "Get an auth token for a link share",
|
||||
Description: "Exchanges a link share's public hash (and password, for password-protected shares) for a JWT auth token scoped to the shared project.",
|
||||
Method: http.MethodPost,
|
||||
Path: "/shares/{share}/auth",
|
||||
DefaultStatus: http.StatusOK,
|
||||
Tags: []string{"sharing"},
|
||||
Security: publicSecurity,
|
||||
}, authLinkShare)
|
||||
}
|
||||
}
|
||||
|
||||
func registerLocalAuthRoutes(api huma.API) {
|
||||
authTags := []string{"auth"}
|
||||
|
||||
Register(api, huma.Operation{
|
||||
OperationID: "auth-register",
|
||||
Summary: "Register",
|
||||
Description: "Creates a new local user account. Returns 404 when registration is disabled on this instance.",
|
||||
Method: http.MethodPost,
|
||||
Path: "/register",
|
||||
Tags: authTags,
|
||||
Security: publicSecurity,
|
||||
}, authRegister)
|
||||
|
||||
Register(api, huma.Operation{
|
||||
OperationID: "auth-password-token",
|
||||
Summary: "Request a password reset token",
|
||||
Description: "Requests a token to reset the password for the account with the given email. The token is sent to that email; the response is the same whether or not an account exists.",
|
||||
Method: http.MethodPost,
|
||||
Path: "/user/password/token",
|
||||
DefaultStatus: http.StatusOK,
|
||||
Tags: authTags,
|
||||
Security: publicSecurity,
|
||||
}, authRequestPasswordToken)
|
||||
|
||||
Register(api, huma.Operation{
|
||||
OperationID: "auth-password-reset",
|
||||
Summary: "Reset a password",
|
||||
Description: "Sets a new password using a previously issued reset token. All of the user's existing sessions are invalidated.",
|
||||
Method: http.MethodPost,
|
||||
Path: "/user/password/reset",
|
||||
DefaultStatus: http.StatusOK,
|
||||
Tags: authTags,
|
||||
Security: publicSecurity,
|
||||
}, authResetPassword)
|
||||
|
||||
Register(api, huma.Operation{
|
||||
OperationID: "auth-confirm-email",
|
||||
Summary: "Confirm an email address",
|
||||
Description: "Confirms the email address of a newly registered user using the token sent to that email.",
|
||||
Method: http.MethodPost,
|
||||
Path: "/user/confirm",
|
||||
DefaultStatus: http.StatusOK,
|
||||
Tags: authTags,
|
||||
Security: publicSecurity,
|
||||
}, authConfirmEmail)
|
||||
}
|
||||
|
||||
func authRegister(_ context.Context, in *struct{ Body shared.UserRegister }) (*registerUserBody, error) {
|
||||
if !config.ServiceEnableRegistration.GetBool() {
|
||||
return nil, huma.Error404NotFound("registration is disabled")
|
||||
}
|
||||
|
||||
newUser, err := shared.RegisterUser(&in.Body)
|
||||
if err != nil {
|
||||
return nil, translateDomainError(err)
|
||||
}
|
||||
return ®isterUserBody{Body: newUser}, nil
|
||||
}
|
||||
|
||||
func authRequestPasswordToken(_ context.Context, in *struct{ Body user.PasswordTokenRequest }) (*messageBody, error) {
|
||||
if err := shared.RequestPasswordResetToken(&in.Body); err != nil {
|
||||
return nil, translateDomainError(err)
|
||||
}
|
||||
out := &messageBody{}
|
||||
out.Body.Message = "Token was sent."
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func authResetPassword(_ context.Context, in *struct{ Body user.PasswordReset }) (*messageBody, error) {
|
||||
if err := shared.ResetPassword(&in.Body); err != nil {
|
||||
return nil, translateDomainError(err)
|
||||
}
|
||||
out := &messageBody{}
|
||||
out.Body.Message = "The password was updated successfully."
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func authConfirmEmail(_ context.Context, in *struct{ Body user.EmailConfirm }) (*messageBody, error) {
|
||||
if err := shared.ConfirmEmail(&in.Body); err != nil {
|
||||
return nil, translateDomainError(err)
|
||||
}
|
||||
out := &messageBody{}
|
||||
out.Body.Message = "The email was confirmed successfully."
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func authLinkShare(_ context.Context, in *struct {
|
||||
Share string `path:"share" doc:"The public hash of the link share."`
|
||||
Body struct {
|
||||
Password string `json:"password" doc:"The password for password-protected link shares. Ignored for shares without a password."`
|
||||
}
|
||||
}) (*linkShareTokenBody, error) {
|
||||
token, err := shared.AuthenticateLinkShare(in.Share, in.Body.Password)
|
||||
if err != nil {
|
||||
return nil, translateDomainError(err)
|
||||
}
|
||||
return &linkShareTokenBody{Body: token}, nil
|
||||
}
|
||||
|
|
@ -344,6 +344,13 @@ var unauthenticatedAPIPaths = map[string]bool{
|
|||
"/api/v2/docs/scalar.standalone.js": true,
|
||||
"/api/v2/schemas/:schema": true,
|
||||
"/api/v2/info": true,
|
||||
|
||||
"/api/v2/register": true,
|
||||
"/api/v2/user/password/token": true,
|
||||
"/api/v2/user/password/reset": true,
|
||||
"/api/v2/user/confirm": true,
|
||||
"/api/v2/shares/:share/auth": true,
|
||||
"/api/v2/oauth/token": true,
|
||||
}
|
||||
|
||||
// collectRoutesForAPITokens collects all routes for API token permission checking.
|
||||
|
|
|
|||
Loading…
Reference in New Issue