2024-02-14 00:07:24 +00:00
|
|
|
package account
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-02-14 05:41:02 +00:00
|
|
|
"crypto/ed25519"
|
2024-03-06 09:58:04 +00:00
|
|
|
_ "embed"
|
2024-02-16 13:52:30 +00:00
|
|
|
"net/http"
|
|
|
|
|
2024-03-06 09:58:04 +00:00
|
|
|
"git.lumeweb.com/LumeWeb/portal/api/swagger"
|
|
|
|
|
2024-02-25 14:47:40 +00:00
|
|
|
"git.lumeweb.com/LumeWeb/portal/api/router"
|
|
|
|
|
2024-02-22 07:09:31 +00:00
|
|
|
"git.lumeweb.com/LumeWeb/portal/config"
|
|
|
|
|
2024-02-16 13:52:30 +00:00
|
|
|
"go.uber.org/zap"
|
2024-02-14 05:41:02 +00:00
|
|
|
|
2024-02-16 13:39:55 +00:00
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
|
2024-02-14 00:07:24 +00:00
|
|
|
"git.lumeweb.com/LumeWeb/portal/account"
|
|
|
|
"git.lumeweb.com/LumeWeb/portal/api/middleware"
|
|
|
|
"git.lumeweb.com/LumeWeb/portal/api/registry"
|
|
|
|
"go.sia.tech/jape"
|
|
|
|
"go.uber.org/fx"
|
|
|
|
)
|
|
|
|
|
2024-03-06 09:58:04 +00:00
|
|
|
//go:embed swagger.yaml
|
|
|
|
var swagSpec []byte
|
|
|
|
|
2024-02-14 00:07:24 +00:00
|
|
|
var (
|
2024-02-25 14:47:40 +00:00
|
|
|
_ registry.API = (*AccountAPI)(nil)
|
|
|
|
_ router.RoutableAPI = (*AccountAPI)(nil)
|
2024-02-14 00:07:24 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type AccountAPI struct {
|
2024-02-22 07:09:31 +00:00
|
|
|
config *config.Manager
|
2024-02-16 13:52:30 +00:00
|
|
|
accounts *account.AccountServiceDefault
|
|
|
|
identity ed25519.PrivateKey
|
|
|
|
logger *zap.Logger
|
2024-02-14 00:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type AccountAPIParams struct {
|
|
|
|
fx.In
|
2024-02-22 07:09:31 +00:00
|
|
|
Config *config.Manager
|
2024-02-16 13:52:30 +00:00
|
|
|
Accounts *account.AccountServiceDefault
|
|
|
|
Identity ed25519.PrivateKey
|
|
|
|
Logger *zap.Logger
|
2024-02-14 00:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewS5(params AccountAPIParams) AccountApiResult {
|
|
|
|
api := &AccountAPI{
|
2024-02-16 13:52:30 +00:00
|
|
|
config: params.Config,
|
|
|
|
accounts: params.Accounts,
|
|
|
|
identity: params.Identity,
|
|
|
|
logger: params.Logger,
|
2024-02-14 00:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return AccountApiResult{
|
|
|
|
API: api,
|
|
|
|
AccountAPI: api,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var Module = fx.Module("s5_api",
|
|
|
|
fx.Provide(NewS5),
|
|
|
|
)
|
|
|
|
|
|
|
|
type AccountApiResult struct {
|
|
|
|
fx.Out
|
|
|
|
API registry.API `group:"api"`
|
|
|
|
AccountAPI *AccountAPI
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) Name() string {
|
|
|
|
return "account"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *AccountAPI) Init() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) Start(ctx context.Context) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) Stop(ctx context.Context) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-02-16 13:52:30 +00:00
|
|
|
func (a AccountAPI) login(jc jape.Context) {
|
|
|
|
var request LoginRequest
|
|
|
|
|
|
|
|
if jc.Decode(&request) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
exists, _, err := a.accounts.EmailExists(request.Email)
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
_ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, nil), http.StatusUnauthorized)
|
|
|
|
if err != nil {
|
|
|
|
a.logger.Error("failed to check if email exists", zap.Error(err))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
jwt, _, err := a.accounts.LoginPassword(request.Email, request.Password, jc.Request.RemoteAddr)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-06 23:11:34 +00:00
|
|
|
jc.Encode(&LoginResponse{
|
|
|
|
Token: jwt,
|
|
|
|
})
|
2024-02-16 13:52:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) register(jc jape.Context) {
|
|
|
|
var request RegisterRequest
|
|
|
|
|
|
|
|
if jc.Decode(&request) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-17 13:45:06 +00:00
|
|
|
if len(request.FirstName) == 0 || len(request.LastName) == 0 {
|
|
|
|
_ = jc.Error(account.NewAccountError(account.ErrKeyAccountCreationFailed, nil), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-26 13:23:53 +00:00
|
|
|
user, err := a.accounts.CreateAccount(request.Email, request.Password, true)
|
2024-02-16 13:52:30 +00:00
|
|
|
if err != nil {
|
|
|
|
_ = jc.Error(err, http.StatusUnauthorized)
|
|
|
|
a.logger.Error("failed to update account name", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = a.accounts.UpdateAccountName(user.ID, request.FirstName, request.LastName)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
_ = jc.Error(account.NewAccountError(account.ErrKeyAccountCreationFailed, err), http.StatusBadRequest)
|
|
|
|
a.logger.Error("failed to update account name", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-26 15:47:47 +00:00
|
|
|
func (a AccountAPI) verifyEmail(jc jape.Context) {
|
|
|
|
var request VerifyEmailRequest
|
|
|
|
|
|
|
|
if jc.Decode(&request) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err := a.accounts.VerifyEmail(request.Email, request.Token)
|
|
|
|
|
|
|
|
if jc.Check("failed to verify email", err) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2024-02-16 13:52:30 +00:00
|
|
|
func (a AccountAPI) otpGenerate(jc jape.Context) {
|
|
|
|
user := middleware.GetUserFromContext(jc.Request.Context())
|
|
|
|
|
|
|
|
otp, err := a.accounts.OTPGenerate(user)
|
|
|
|
if jc.Check("failed to generate otp", err) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
jc.Encode(&OTPGenerateResponse{
|
|
|
|
OTP: otp,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) otpVerify(jc jape.Context) {
|
|
|
|
user := middleware.GetUserFromContext(jc.Request.Context())
|
|
|
|
|
|
|
|
var request OTPVerifyRequest
|
|
|
|
|
|
|
|
if jc.Decode(&request) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err := a.accounts.OTPEnable(user, request.OTP)
|
|
|
|
|
|
|
|
if jc.Check("failed to verify otp", err) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) otpValidate(jc jape.Context) {
|
|
|
|
user := middleware.GetUserFromContext(jc.Request.Context())
|
|
|
|
|
|
|
|
var request OTPValidateRequest
|
|
|
|
|
|
|
|
if jc.Decode(&request) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
jwt, err := a.accounts.LoginOTP(user, request.OTP)
|
|
|
|
if jc.Check("failed to validate otp", err) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
account.SendJWT(jc, jwt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) otpDisable(jc jape.Context) {
|
|
|
|
user := middleware.GetUserFromContext(jc.Request.Context())
|
|
|
|
|
|
|
|
var request OTPDisableRequest
|
|
|
|
|
|
|
|
if jc.Decode(&request) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
valid, _, err := a.accounts.ValidLoginByUserID(user, request.Password)
|
|
|
|
|
|
|
|
if !valid {
|
|
|
|
_ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, nil), http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = a.accounts.OTPDisable(user)
|
|
|
|
if jc.Check("failed to disable otp", err) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-26 16:04:05 +00:00
|
|
|
func (a AccountAPI) passwordResetRequest(jc jape.Context) {
|
|
|
|
var request PasswordResetRequest
|
|
|
|
|
|
|
|
if jc.Decode(&request) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
exists, user, err := a.accounts.EmailExists(request.Email)
|
|
|
|
if jc.Check("invalid request", err) != nil || !exists {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = a.accounts.SendPasswordReset(user)
|
|
|
|
if jc.Check("failed to request password reset", err) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
jc.ResponseWriter.WriteHeader(http.StatusOK)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) passwordResetConfirm(jc jape.Context) {
|
|
|
|
var request PasswordResetVerifyRequest
|
|
|
|
|
|
|
|
if jc.Decode(&request) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
exists, _, err := a.accounts.EmailExists(request.Email)
|
|
|
|
if jc.Check("invalid request", err) != nil || !exists {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = a.accounts.ResetPassword(request.Email, request.Password, request.Token)
|
|
|
|
if jc.Check("failed to reset password", err) != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
jc.ResponseWriter.WriteHeader(http.StatusOK)
|
|
|
|
}
|
|
|
|
|
2024-03-13 21:34:10 +00:00
|
|
|
func (a AccountAPI) ping(jc jape.Context) {
|
2024-03-13 16:26:38 +00:00
|
|
|
jc.Encode(&PongResponse{
|
|
|
|
Ping: "pong",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-03-14 10:42:38 +00:00
|
|
|
func (a AccountAPI) accountInfo(jc jape.Context) {
|
|
|
|
user := middleware.GetUserFromContext(jc.Request.Context())
|
|
|
|
|
|
|
|
_, acct, _ := a.accounts.AccountExists(user)
|
|
|
|
|
|
|
|
jc.Encode(&AccountInfoResponse{
|
|
|
|
ID: acct.ID,
|
|
|
|
Email: acct.Email,
|
|
|
|
FirstName: acct.FirstName,
|
|
|
|
LastName: acct.LastName,
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2024-02-17 08:14:17 +00:00
|
|
|
func (a AccountAPI) Routes() (*httprouter.Router, error) {
|
2024-03-13 21:35:26 +00:00
|
|
|
loginAuthMw2fa := authMiddleware(middleware.AuthMiddlewareOptions{
|
2024-03-13 18:01:08 +00:00
|
|
|
Identity: a.identity,
|
|
|
|
Accounts: a.accounts,
|
|
|
|
Config: a.config,
|
|
|
|
Purpose: account.JWTPurpose2FA,
|
|
|
|
EmptyAllowed: true,
|
2024-02-14 05:41:02 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
authMw := authMiddleware(middleware.AuthMiddlewareOptions{
|
2024-03-13 22:45:14 +00:00
|
|
|
Identity: a.identity,
|
|
|
|
Accounts: a.accounts,
|
|
|
|
Config: a.config,
|
|
|
|
Purpose: account.JWTPurposeNone,
|
|
|
|
})
|
|
|
|
|
|
|
|
pingAuthMw := authMiddleware(middleware.AuthMiddlewareOptions{
|
2024-02-14 05:41:02 +00:00
|
|
|
Identity: a.identity,
|
|
|
|
Accounts: a.accounts,
|
|
|
|
Config: a.config,
|
|
|
|
Purpose: account.JWTPurposeLogin,
|
|
|
|
})
|
|
|
|
|
2024-03-06 09:58:04 +00:00
|
|
|
routes := map[string]jape.Handler{
|
2024-03-13 22:45:14 +00:00
|
|
|
"POST /api/auth/ping": middleware.ApplyMiddlewares(a.ping, pingAuthMw, middleware.ProxyMiddleware),
|
2024-03-13 21:35:26 +00:00
|
|
|
"POST /api/auth/login": middleware.ApplyMiddlewares(a.login, loginAuthMw2fa, middleware.ProxyMiddleware),
|
2024-02-26 16:04:05 +00:00
|
|
|
"POST /api/auth/register": middleware.ApplyMiddlewares(a.register, middleware.ProxyMiddleware),
|
|
|
|
"POST /api/auth/verify-email": middleware.ApplyMiddlewares(a.verifyEmail, middleware.ProxyMiddleware),
|
|
|
|
"GET /api/auth/otp/generate": middleware.ApplyMiddlewares(a.otpGenerate, authMw, middleware.ProxyMiddleware),
|
|
|
|
"POST /api/auth/otp/verify": middleware.ApplyMiddlewares(a.otpVerify, authMw, middleware.ProxyMiddleware),
|
|
|
|
"POST /api/auth/otp/validate": middleware.ApplyMiddlewares(a.otpValidate, authMw, middleware.ProxyMiddleware),
|
|
|
|
"POST /api/auth/otp/disable": middleware.ApplyMiddlewares(a.otpDisable, authMw, middleware.ProxyMiddleware),
|
|
|
|
"POST /api/auth/password-reset/request": middleware.ApplyMiddlewares(a.passwordResetRequest, middleware.ProxyMiddleware),
|
|
|
|
"POST /api/auth/password-reset/confirm": middleware.ApplyMiddlewares(a.passwordResetConfirm, middleware.ProxyMiddleware),
|
2024-03-14 10:42:38 +00:00
|
|
|
"GET /api/account": middleware.ApplyMiddlewares(a.accountInfo, authMw, middleware.ProxyMiddleware),
|
2024-03-06 09:58:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
routes, err := swagger.Swagger(swagSpec, routes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return jape.Mux(routes), nil
|
2024-02-14 00:07:24 +00:00
|
|
|
}
|
2024-02-25 14:47:40 +00:00
|
|
|
func (a AccountAPI) Can(w http.ResponseWriter, r *http.Request) bool {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a AccountAPI) Handle(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// noop
|
|
|
|
}
|