diff --git a/account/account.go b/account/account.go index 54c5cec..3421f9a 100644 --- a/account/account.go +++ b/account/account.go @@ -2,6 +2,7 @@ package account import ( "crypto/ed25519" + "errors" "git.lumeweb.com/LumeWeb/portal/db/models" "github.com/spf13/viper" "go.uber.org/fx" @@ -54,6 +55,17 @@ func (s AccountServiceDefault) AccountExists(id uint64) (bool, models.User) { return result.RowsAffected > 0, model } + +func (s AccountServiceDefault) AccountExistsByEmail(email string) (bool, models.User) { + var model models.User + + model.Email = email + + result := s.db.Model(&models.User{}).Where(&model).First(&model) + + return result.RowsAffected > 0, model +} + func (s AccountServiceDefault) CreateAccount(email string, password string) (*models.User, error) { var user models.User @@ -73,6 +85,29 @@ func (s AccountServiceDefault) CreateAccount(email string, password string) (*mo return &user, nil } + +func (s AccountServiceDefault) UpdateAccountName(userId uint, firstName string, lastName string) error { + var user models.User + + user.ID = userId + + if len(firstName) == 0 { + return errors.New("First name cannot be empty") + } + + if len(lastName) == 0 { + return errors.New("Last name cannot be empty") + } + + result := s.db.Model(&models.User{}).Where(&user).Updates(&models.User{FirstName: firstName, LastName: lastName}) + + if result.Error != nil { + return result.Error + } + + return nil +} + func (s AccountServiceDefault) AddPubkeyToAccount(user models.User, pubkey string) error { var model models.PublicKey @@ -87,26 +122,40 @@ func (s AccountServiceDefault) AddPubkeyToAccount(user models.User, pubkey strin return nil } -func (s AccountServiceDefault) LoginPassword(email string, password string) (string, error) { +func (s AccountServiceDefault) LoginPassword(email string, password string) (string, *models.User, error) { + valid, user, err := s.ValidLogin(email, password) + + if err != nil { + return "", nil, err + } + + if !valid { + return "", nil, nil + } + + token, err := GenerateToken(s.config.GetString("core.domain"), s.identity, user.ID) + if err != nil { + return "", nil, err + } + + return token, nil, nil +} + +func (s AccountServiceDefault) ValidLogin(email string, password string) (bool, *models.User, error) { var user models.User result := s.db.Model(&models.User{}).Where(&models.User{Email: email}).First(&user) if result.RowsAffected == 0 || result.Error != nil { - return "", result.Error + return false, nil, result.Error } err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) if err != nil { - return "", err + return false, nil, err } - token, err := GenerateToken(s.identity, user.ID) - if err != nil { - return "", err - } - - return token, nil + return true, nil, nil } func (s AccountServiceDefault) LoginPubkey(pubkey string) (string, error) { @@ -118,7 +167,7 @@ func (s AccountServiceDefault) LoginPubkey(pubkey string) (string, error) { return "", result.Error } - token, err := GenerateToken(s.identity, model.UserID) + token, err := GenerateToken(s.config.GetString("core.domain"), s.identity, model.UserID) if err != nil { return "", err } diff --git a/account/jwt.go b/account/jwt.go index 67ce7e0..a847606 100644 --- a/account/jwt.go +++ b/account/jwt.go @@ -6,13 +6,13 @@ import ( "time" ) -func GenerateToken(privateKey ed25519.PrivateKey, userID uint) (string, error) { - return GenerateTokenWithDuration(privateKey, userID, time.Hour*24) +func GenerateToken(domain string, privateKey ed25519.PrivateKey, userID uint) (string, error) { + return GenerateTokenWithDuration(domain, privateKey, userID, time.Hour*24) } -func GenerateTokenWithDuration(privateKey ed25519.PrivateKey, userID uint, duration time.Duration) (string, error) { +func GenerateTokenWithDuration(domain string, privateKey ed25519.PrivateKey, userID uint, duration time.Duration) (string, error) { // Define the claims claims := jwt.MapClaims{ - "iss": "portal", + "iss": domain, "sub": userID, "exp": time.Now().Add(duration).Unix(), } diff --git a/api/account/account.go b/api/account/account.go new file mode 100644 index 0000000..1cb4f56 --- /dev/null +++ b/api/account/account.go @@ -0,0 +1,80 @@ +package account + +import ( + "context" + "git.lumeweb.com/LumeWeb/portal/account" + "git.lumeweb.com/LumeWeb/portal/api/middleware" + "git.lumeweb.com/LumeWeb/portal/api/registry" + "github.com/spf13/viper" + "go.sia.tech/jape" + "go.uber.org/fx" +) + +var ( + _ registry.API = (*AccountAPI)(nil) +) + +type AccountAPI struct { + config *viper.Viper + accounts *account.AccountServiceDefault + httpHandler *HttpHandler +} + +type AccountAPIParams struct { + fx.In + Config *viper.Viper + Accounts *account.AccountServiceDefault + HttpHandler *HttpHandler +} + +func NewS5(params AccountAPIParams) AccountApiResult { + api := &AccountAPI{ + config: params.Config, + accounts: params.Accounts, + httpHandler: params.HttpHandler, + } + + return AccountApiResult{ + API: api, + AccountAPI: api, + } +} + +func InitAPI(api *AccountAPI) error { + return api.Init() +} + +var Module = fx.Module("s5_api", + fx.Provide(NewS5), + fx.Provide(NewHttpHandler), +) + +type AccountApiResult struct { + fx.Out + API registry.API `group:"api"` + AccountAPI *AccountAPI +} + +func (a AccountAPI) Name() string { + return "account" +} + +func (a *AccountAPI) Init() error { + middleware.RegisterProtocolSubdomain(a.config, jape.Mux(getRoutes(a)), "s5") + return nil +} + +func (a AccountAPI) Start(ctx context.Context) error { + return nil +} + +func (a AccountAPI) Stop(ctx context.Context) error { + return nil +} + +func getRoutes(a *AccountAPI) map[string]jape.Handler { + return map[string]jape.Handler{ + "/api/login": a.httpHandler.login, + "api/register": a.httpHandler.register, + } +} diff --git a/api/account/http.go b/api/account/http.go new file mode 100644 index 0000000..7a680e0 --- /dev/null +++ b/api/account/http.go @@ -0,0 +1,73 @@ +package account + +import ( + "errors" + "git.lumeweb.com/LumeWeb/portal/account" + "go.sia.tech/jape" + "go.uber.org/fx" + "net/http" +) + +var ( + errInvalidLogin = errors.New("invalid login") + errFailedToCreateAccount = errors.New("failed to create account") +) + +type HttpHandler struct { + accounts *account.AccountServiceDefault +} + +type HttpHandlerParams struct { + fx.In + Accounts *account.AccountServiceDefault +} + +func NewHttpHandler(params HttpHandlerParams) *HttpHandler { + return &HttpHandler{ + accounts: params.Accounts, + } +} + +func (h *HttpHandler) login(jc jape.Context) { + var request LoginRequest + + if jc.Decode(&request) != nil { + return + } + + exists, _ := h.accounts.AccountExistsByEmail(request.Email) + + if !exists { + _ = jc.Error(errInvalidLogin, http.StatusUnauthorized) + return + } + + jwt, _, err := h.accounts.LoginPassword(request.Email, request.Password) + if err != nil { + return + } + + jc.ResponseWriter.Header().Set("Authorization", "Bearer "+jwt) + jc.ResponseWriter.WriteHeader(http.StatusOK) +} + +func (h *HttpHandler) register(jc jape.Context) { + var request RegisterRequest + + if jc.Decode(&request) != nil { + return + } + + user, err := h.accounts.CreateAccount(request.Email, request.Password) + if err != nil { + _ = jc.Error(errFailedToCreateAccount, http.StatusBadRequest) + return + } + + err = h.accounts.UpdateAccountName(user.ID, request.FirstName, request.LastName) + + if err != nil { + _ = jc.Error(errors.Join(errFailedToCreateAccount, err), http.StatusBadRequest) + return + } +} diff --git a/api/account/messages.go b/api/account/messages.go new file mode 100644 index 0000000..b511720 --- /dev/null +++ b/api/account/messages.go @@ -0,0 +1,13 @@ +package account + +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type RegisterRequest struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Password string `json:"password"` +} diff --git a/api/api.go b/api/api.go index 1881486..928d01d 100644 --- a/api/api.go +++ b/api/api.go @@ -2,6 +2,7 @@ package api import ( "context" + "git.lumeweb.com/LumeWeb/portal/api/account" "git.lumeweb.com/LumeWeb/portal/api/registry" "git.lumeweb.com/LumeWeb/portal/api/s5" "github.com/samber/lo" @@ -15,6 +16,11 @@ func RegisterApis() { Module: s5.Module, InitFunc: s5.InitAPI, }) + registry.Register(registry.APIEntry{ + Key: "account", + Module: account.Module, + InitFunc: account.InitAPI, + }) } func BuildApis(config *viper.Viper) fx.Option {