package account import ( "context" "crypto/ed25519" "embed" _ "embed" "io/fs" "net/http" "strings" "github.com/rs/cors" "git.lumeweb.com/LumeWeb/portal/api/swagger" "git.lumeweb.com/LumeWeb/portal/api/router" "git.lumeweb.com/LumeWeb/portal/config" "go.uber.org/zap" "github.com/julienschmidt/httprouter" "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" ) //go:embed swagger.yaml var swagSpec []byte //go:embed all:app/build/client var appFs embed.FS var ( _ registry.API = (*AccountAPI)(nil) _ router.RoutableAPI = (*AccountAPI)(nil) ) type AccountAPI struct { config *config.Manager accounts *account.AccountServiceDefault identity ed25519.PrivateKey logger *zap.Logger } type AccountAPIParams struct { fx.In Config *config.Manager Accounts *account.AccountServiceDefault Identity ed25519.PrivateKey Logger *zap.Logger } func NewS5(params AccountAPIParams) AccountApiResult { api := &AccountAPI{ config: params.Config, accounts: params.Accounts, identity: params.Identity, logger: params.Logger, } 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 } 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, user, err := a.accounts.LoginPassword(request.Email, request.Password, jc.Request.RemoteAddr) if err != nil || user == nil { _ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, err), http.StatusUnauthorized) if err != nil { a.logger.Error("failed to login", zap.Error(err)) } return } account.SetAuthCookie(jc, jwt, "") account.SendJWT(jc, jwt) jc.Encode(&LoginResponse{ Token: jwt, Otp: user.OTPEnabled && user.OTPVerified, }) } func (a AccountAPI) register(jc jape.Context) { var request RegisterRequest if jc.Decode(&request) != nil { return } if len(request.FirstName) == 0 || len(request.LastName) == 0 { _ = jc.Error(account.NewAccountError(account.ErrKeyAccountCreationFailed, nil), http.StatusBadRequest) return } user, err := a.accounts.CreateAccount(request.Email, request.Password, false) 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 } } 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 } } 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.SetAuthCookie(jc, jwt, "") account.SendJWT(jc, jwt) jc.Encode(&LoginResponse{ Token: jwt, Otp: false, }) } 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 } } 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) } func (a AccountAPI) ping(jc jape.Context) { token := middleware.GetAuthTokenFromContext(jc.Request.Context()) account.EchoAuthCookie(jc, a.Name()) jc.Encode(&PongResponse{ Ping: "pong", Token: token, }) } 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, }) } func (a AccountAPI) logout(c jape.Context) { account.ClearAuthCookie(c, "") } func (a AccountAPI) uploadLimit(c jape.Context) { c.Encode(&UploadLimitResponse{ Limit: a.config.Config().Core.PostUploadLimit, }) } func (a AccountAPI) updateEmail(c jape.Context) { user := middleware.GetUserFromContext(c.Request.Context()) var request UpdateEmailRequest if c.Decode(&request) != nil { return } err := a.accounts.UpdateAccountEmail(user, request.Email, request.Password) if c.Check("failed to update email", err) != nil { return } } func (a *AccountAPI) Routes() (*httprouter.Router, error) { loginAuthMw2fa := authMiddleware(middleware.AuthMiddlewareOptions{ Identity: a.identity, Accounts: a.accounts, Config: a.config, Purpose: account.JWTPurpose2FA, EmptyAllowed: true, }) authMw := authMiddleware(middleware.AuthMiddlewareOptions{ Identity: a.identity, Accounts: a.accounts, Config: a.config, Purpose: account.JWTPurposeNone, }) pingAuthMw := authMiddleware(middleware.AuthMiddlewareOptions{ Identity: a.identity, Accounts: a.accounts, Config: a.config, Purpose: account.JWTPurposeLogin, }) appFiles, _ := fs.Sub(appFs, "app/build/client") appServ := http.FileServer(http.FS(appFiles)) appHandler := func(c jape.Context) { appServ.ServeHTTP(c.ResponseWriter, c.Request) } appServer := middleware.ApplyMiddlewares(appHandler, middleware.ProxyMiddleware) swaggerRoutes, err := swagger.Swagger(swagSpec, map[string]jape.Handler{}) if err != nil { return nil, err } swaggerJape := jape.Mux(swaggerRoutes) getApiJape := jape.Mux(map[string]jape.Handler{ "GET /api/auth/otp/generate": middleware.ApplyMiddlewares(a.otpGenerate, authMw, middleware.ProxyMiddleware), "GET /api/account": middleware.ApplyMiddlewares(a.accountInfo, authMw, middleware.ProxyMiddleware), "GET /api/upload-limit": middleware.ApplyMiddlewares(a.uploadLimit, middleware.ProxyMiddleware), }) getHandler := func(c jape.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api") { getApiJape.ServeHTTP(c.ResponseWriter, c.Request) return } if strings.HasPrefix(c.Request.URL.Path, "/swagger") { swaggerJape.ServeHTTP(c.ResponseWriter, c.Request) return } if !strings.HasPrefix(c.Request.URL.Path, "/assets") && c.Request.URL.Path != "favicon.ico" && c.Request.URL.Path != "/" && !strings.HasSuffix(c.Request.URL.Path, ".html") { c.Request.URL.Path = "/" } appServer(c) } corsMw := cors.New(cors.Options{ AllowedOrigins: []string{"*." + a.config.Config().Core.Domain}, AllowedMethods: []string{"*"}, }) routes := map[string]jape.Handler{ // Auth "POST /api/auth/ping": middleware.ApplyMiddlewares(a.ping, corsMw.Handler, pingAuthMw, middleware.ProxyMiddleware), "POST /api/auth/login": middleware.ApplyMiddlewares(a.login, corsMw.Handler, loginAuthMw2fa, middleware.ProxyMiddleware), "POST /api/auth/register": middleware.ApplyMiddlewares(a.register, corsMw.Handler, middleware.ProxyMiddleware), "POST /api/auth/otp/validate": middleware.ApplyMiddlewares(a.otpValidate, corsMw.Handler, authMw, middleware.ProxyMiddleware), "POST /api/auth/logout": middleware.ApplyMiddlewares(a.logout, corsMw.Handler, authMw, middleware.ProxyMiddleware), // Account "POST /api/account/verify-email": middleware.ApplyMiddlewares(a.verifyEmail, corsMw.Handler, middleware.ProxyMiddleware), "POST /api/account/otp/verify": middleware.ApplyMiddlewares(a.otpVerify, corsMw.Handler, authMw, middleware.ProxyMiddleware), "POST /api/account/otp/disable": middleware.ApplyMiddlewares(a.otpDisable, corsMw.Handler, authMw, middleware.ProxyMiddleware), "POST /api/account/password-reset/request": middleware.ApplyMiddlewares(a.passwordResetRequest, corsMw.Handler, middleware.ProxyMiddleware), "POST /api/account/password-reset/confirm": middleware.ApplyMiddlewares(a.passwordResetConfirm, corsMw.Handler, middleware.ProxyMiddleware), "POST /api/account/update-email": middleware.ApplyMiddlewares(a.updateEmail, corsMw.Handler, middleware.ProxyMiddleware), "GET /*path": middleware.ApplyMiddlewares(getHandler, corsMw.Handler), } return jape.Mux(routes), nil } func (a AccountAPI) Can(w http.ResponseWriter, r *http.Request) bool { return false } func (a AccountAPI) Handle(w http.ResponseWriter, r *http.Request) { } func (a *AccountAPI) Domain() string { return router.BuildSubdomain(a, a.config) } func (a AccountAPI) AuthTokenName() string { return account.AUTH_COOKIE_NAME }