Compare commits

..

10 Commits

8 changed files with 161 additions and 73 deletions

View File

@ -53,7 +53,7 @@ func Init() {
}
// Automatically migrate the database schema based on the model definitions.
err = db.Migrator().AutoMigrate(&model.Account{}, &model.Key{}, &model.KeyChallenge{})
err = db.Migrator().AutoMigrate(&model.Account{}, &model.Key{}, &model.KeyChallenge{}, &model.LoginSession{})
if err != nil {
panic(fmt.Errorf("Database setup failed database type: %s \n", err))
}

4
go.mod
View File

@ -3,6 +3,7 @@ module git.lumeweb.com/LumeWeb/portal
go 1.18
require (
github.com/go-playground/validator/v10 v10.13.0
github.com/iris-contrib/swagger v0.0.0-20230311205341-32127a753a68
github.com/joomcode/errorx v1.1.0
github.com/kataras/iris/v12 v12.2.0
@ -49,6 +50,8 @@ require (
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
@ -76,6 +79,7 @@ require (
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/reedsolomon v1.11.7 // indirect
github.com/leodido/go-urn v1.2.3 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.7.7 // indirect

10
go.sum
View File

@ -150,6 +150,13 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
@ -316,6 +323,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
@ -420,6 +429,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=

41
main.go
View File

@ -6,6 +6,7 @@ import (
"git.lumeweb.com/LumeWeb/portal/db"
_ "git.lumeweb.com/LumeWeb/portal/docs"
"git.lumeweb.com/LumeWeb/portal/service"
"git.lumeweb.com/LumeWeb/portal/validator"
"github.com/iris-contrib/swagger"
"github.com/iris-contrib/swagger/swaggerFiles"
"github.com/kataras/iris/v12"
@ -17,19 +18,19 @@ import (
//go:embed app/*
var embedFrontend embed.FS
// @title Lume Web Portal
// @version 1.0
// @description A decentralized data storage portal for the open web
// @title Lume Web Portal
// @version 1.0
// @description A decentralized data storage portal for the open web
// @contact.name Lume Web Project
// @contact.url https://lumeweb.com
// @contact.email contact@lumeweb.com
// @contact.name Lume Web Project
// @contact.url https://lumeweb.com
// @contact.email contact@lumeweb.com
// @license.name MIT
// @license.url https://opensource.org/license/mit/
// @license.name MIT
// @license.url https://opensource.org/license/mit/
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
func main() {
// Initialize the configuration settings
config.Init()
@ -40,22 +41,27 @@ func main() {
// Create a new Iris app instance
app := iris.New()
app.Validator = validator.Get()
// Enable Gzip compression for responses
app.Use(iris.Compression)
// Serve static files from the embedded directory at the app's root path
app.HandleDir("/", embedFrontend)
api := app.Party("/api")
v1 := api.Party("/v1")
// Register the AccountService with the MVC framework and attach it to the "/api/account" path
mvc.Configure(app.Party("/api/account"), func(app *mvc.Application) {
mvc.Configure(v1.Party("/account"), func(app *mvc.Application) {
app.Handle(new(service.AccountService))
})
mvc.Configure(app.Party("/api/auth"), func(app *mvc.Application) {
mvc.Configure(v1.Party("/auth"), func(app *mvc.Application) {
app.Handle(new(service.AuthService))
})
config := swagger.Config{
swaggerConfig := swagger.Config{
// The url pointing to API definition.
URL: "http://localhost:8080/swagger/doc.json",
DeepLinking: true,
@ -64,7 +70,7 @@ func main() {
// The UI prefix URL (see route).
Prefix: "/swagger",
}
swaggerUI := swagger.Handler(swaggerFiles.Handler, config)
swaggerUI := swagger.Handler(swaggerFiles.Handler, swaggerConfig)
app.Get("/swagger", swaggerUI)
// And the wildcard one for index.html, *.js, *.css and e.t.c.
@ -74,5 +80,10 @@ func main() {
//go renterd.Main()
// Start the Iris app and listen for incoming requests on port 80
log.Fatal(app.Listen(":80"))
log.Fatal(app.Listen(":8080", func(app *iris.Application) {
routes := app.GetRoutes()
for _, route := range routes {
log.Println(route)
}
}))
}

View File

@ -7,11 +7,10 @@ import (
type Key struct {
gorm.Model
ID uint `gorm:"primaryKey"`
AccountID uint
Account Account
PublicKey string
PrivateKey string
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey"`
AccountID uint
Account Account
Pubkey string
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -1,22 +1,55 @@
package service
import (
"crypto/ed25519"
"encoding/hex"
"errors"
"git.lumeweb.com/LumeWeb/portal/db"
"git.lumeweb.com/LumeWeb/portal/model"
_validator "git.lumeweb.com/LumeWeb/portal/validator"
"github.com/go-playground/validator/v10"
"github.com/kataras/iris/v12"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"reflect"
)
type AccountService struct {
ctx iris.Context
Ctx iris.Context
}
type RegisterRequest struct {
Email string `json:"email"`
Email string `json:"email" validate:"required"`
Password string `json:"password"`
Pubkey []byte `json:"pubkey"`
Pubkey string `json:"pubkey"`
}
func init() {
jsonValidator := _validator.Get()
jsonValidator.RegisterStructValidation(ValidateRegisterRequest, RegisterRequest{})
}
func ValidateRegisterRequest(structLevel validator.StructLevel) {
request := structLevel.Current().Interface().(RegisterRequest)
pubkey := len(request.Pubkey) == 0
pass := len(request.Password) == 0
if pubkey == pass {
structLevel.ReportError(reflect.ValueOf(request.Email), "email", "Email", "emailorpubkey", "")
structLevel.ReportError(reflect.ValueOf(request.Pubkey), "pubkey", "Pubkey", "emailorpubkey", "")
}
if !pubkey {
pubkeyBytes, err := hex.DecodeString(request.Pubkey)
if err != nil || len(pubkeyBytes) != ed25519.PublicKeySize {
structLevel.ReportError(reflect.ValueOf(request.Pubkey), "pubkey", "Pubkey", "pubkey", "")
return
}
}
}
func hashPassword(password string) (string, error) {
@ -34,46 +67,62 @@ func hashPassword(password string) (string, error) {
func (a *AccountService) PostRegister() {
var r RegisterRequest
if err := a.ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err)
return
}
// Hash the password before saving it to the database.
hashedPassword, err := hashPassword(r.Password)
if err != nil {
a.ctx.StopWithError(iris.StatusInternalServerError, err)
if err := a.Ctx.ReadJSON(&r); err != nil {
a.Ctx.StopWithError(iris.StatusBadRequest, err)
return
}
// Check if an account with the same email address already exists.
existingAccount := model.Account{}
err = db.Get().Where("email = ?", r.Email).First(&existingAccount).Error
err := db.Get().Where("email = ?", r.Email).First(&existingAccount).Error
if err == nil {
// An account with the same email address already exists.
// Return an error response to the client.
a.ctx.StopWithError(iris.StatusConflict, errors.New("an account with this email address already exists"))
a.Ctx.StopWithError(iris.StatusConflict, errors.New("an account with this email address already exists"))
return
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
// An unexpected error occurred while querying the database.
// Return an error response to the client.
a.ctx.StopWithError(iris.StatusInternalServerError, err)
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
// Create a new Account model with the provided email and hashed password.
account := model.Account{
Email: r.Email,
Password: &hashedPassword,
Email: r.Email,
}
// Save the new account to the database.
err = db.Get().Create(&account).Error
// Hash the password before saving it to the database.
if len(r.Password) > 0 {
hashedPassword, err := hashPassword(r.Password)
if err != nil {
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
account.Password = &hashedPassword
}
err = db.Get().Transaction(func(tx *gorm.DB) error {
// do some database operations in the transaction (use 'tx' from this point, not 'db')
if err := tx.Create(&account).Error; err != nil {
return err
}
if len(r.Pubkey) > 0 {
if err := tx.Create(&model.Key{Account: account, Pubkey: r.Pubkey}).Error; err != nil {
return err
}
}
// return nil will commit the whole transaction
return nil
})
if err != nil {
a.ctx.StopWithError(iris.StatusInternalServerError, err)
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
// Return a success response to the client.
a.ctx.StatusCode(iris.StatusCreated)
a.Ctx.StatusCode(iris.StatusCreated)
}

View File

@ -23,7 +23,7 @@ func init() {
}
type AuthService struct {
ctx iris.Context
Ctx iris.Context
}
type LoginRequest struct {
@ -136,101 +136,101 @@ func (a *AuthService) PostLogin() {
var r LoginRequest
// Read the login request from the client.
if err := a.ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err)
if err := a.Ctx.ReadJSON(&r); err != nil {
a.Ctx.StopWithError(iris.StatusBadRequest, err)
return
}
// Retrieve the account for the given email.
account := model.Account{}
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
return
}
// Verify the provided password against the hashed password stored in the database.
if err := verifyPassword(*account.Password, r.Password); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
return
}
// Generate a JWT token for the authenticated user.
token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour)
if err != nil {
a.ctx.StopWithError(iris.StatusInternalServerError, fmt.Errorf("failed to generate token: %s", err))
a.Ctx.StopWithError(iris.StatusInternalServerError, fmt.Errorf("failed to generate token: %s", err))
return
}
// Return the JWT token to the client.
err = a.ctx.JSON(&LoginResponse{Token: token})
err = a.Ctx.JSON(&LoginResponse{Token: token})
if err != nil {
panic(fmt.Errorf("Error with login attempt: %s \n", err))
}
}
// PostChallenge handles the POST /api/auth/pubkey-challenge request to generate a challenge for a user's public key.
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
func (a *AuthService) PostPubkeyChallenge() {
var r LoginRequest
// Read the login request from the client.
if err := a.ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err)
if err := a.Ctx.ReadJSON(&r); err != nil {
a.Ctx.StopWithError(iris.StatusBadRequest, err)
return
}
// Retrieve the account for the given email.
account := model.Account{}
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
return
}
// Generate a random challenge string.
challenge, err := generateAndSaveChallengeToken(account.ID, time.Minute)
if err != nil {
a.ctx.StopWithError(iris.StatusInternalServerError, errors.New("failed to generate challenge"))
a.Ctx.StopWithError(iris.StatusInternalServerError, errors.New("failed to generate challenge"))
return
}
// Return the challenge to the client.
err = a.ctx.JSON(&ChallengeResponse{Challenge: challenge})
err = a.Ctx.JSON(&ChallengeResponse{Challenge: challenge})
if err != nil {
panic(fmt.Errorf("Error with challenge request: %s \n", err))
}
}
// PostKeyLogin handles the POST /api/auth/pubkey-login request to authenticate a user using a public key challenge and return a JWT token.
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
func (a *AuthService) PostPubkeyLogin() {
var r PubkeyLoginRequest
// Read the key login request from the client.
if err := a.ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err)
if err := a.Ctx.ReadJSON(&r); err != nil {
a.Ctx.StopWithError(iris.StatusBadRequest, err)
return
}
// Retrieve the key challenge for the given challenge.
challenge := model.KeyChallenge{}
if err := db.Get().Where("challenge = ?", r.Challenge).Preload("Key").First(&challenge).Error; err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid key challenge"))
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid key challenge"))
return
}
verifiedToken, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Challenge), blocklist)
if err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid key challenge"))
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid key challenge"))
return
}
rawPubKey, err := hex.DecodeString(r.Pubkey)
if err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid pubkey"))
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid pubkey"))
return
}
rawSignature, err := hex.DecodeString(r.Signature)
if err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid signature"))
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid signature"))
return
}
@ -238,29 +238,29 @@ func (a *AuthService) PostPubkeyLogin() {
// Verify the challenge signature.
if !ed25519.Verify(publicKeyDecoded, []byte(r.Challenge), rawSignature) {
a.ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid challenge"))
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid challenge"))
}
// Generate a JWT token for the authenticated user.
token, err := generateAndSaveLoginToken(challenge.AccountID, 24*time.Hour)
if err != nil {
a.ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, "failed to generate token"))
a.Ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, "failed to generate token"))
return
}
err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
if err != nil {
a.ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, "failed to invalidate token"))
a.Ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, "failed to invalidate token"))
return
}
if err := db.Get().Delete(&challenge).Error; err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("failed to delete key challenge"))
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("failed to delete key challenge"))
return
}
// Return the JWT token to the client.
err = a.ctx.JSON(&LoginResponse{Token: token})
err = a.Ctx.JSON(&LoginResponse{Token: token})
if err != nil {
panic(fmt.Errorf("Error with login attempt: %s \n", err))
}
@ -272,15 +272,15 @@ func (a *AuthService) PostLogout() {
var r LogoutRequest
// Read the logout request from the client.
if err := a.ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err)
if err := a.Ctx.ReadJSON(&r); err != nil {
a.Ctx.StopWithError(iris.StatusBadRequest, err)
return
}
// Verify the provided token.
claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Token), blocklist)
if err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid token"))
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid token"))
return
}
@ -290,5 +290,5 @@ func (a *AuthService) PostLogout() {
}
// Return a success response to the client.
a.ctx.StatusCode(iris.StatusNoContent)
a.Ctx.StatusCode(iris.StatusNoContent)
}

15
validator/validator.go Normal file
View File

@ -0,0 +1,15 @@
package validator
import (
"github.com/go-playground/validator/v10"
)
var v *validator.Validate
func init() {
v = validator.New()
}
func Get() *validator.Validate {
return v
}