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. // 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 { if err != nil {
panic(fmt.Errorf("Database setup failed database type: %s \n", err)) 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 go 1.18
require ( require (
github.com/go-playground/validator/v10 v10.13.0
github.com/iris-contrib/swagger v0.0.0-20230311205341-32127a753a68 github.com/iris-contrib/swagger v0.0.0-20230311205341-32127a753a68
github.com/joomcode/errorx v1.1.0 github.com/joomcode/errorx v1.1.0
github.com/kataras/iris/v12 v12.2.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/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.3 // 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/go-sql-driver/mysql v1.7.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // 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/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/reedsolomon v1.11.7 // 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/magiconair/properties v1.8.7 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.7.7 // 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.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 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 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.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 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/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.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 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 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= 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/db"
_ "git.lumeweb.com/LumeWeb/portal/docs" _ "git.lumeweb.com/LumeWeb/portal/docs"
"git.lumeweb.com/LumeWeb/portal/service" "git.lumeweb.com/LumeWeb/portal/service"
"git.lumeweb.com/LumeWeb/portal/validator"
"github.com/iris-contrib/swagger" "github.com/iris-contrib/swagger"
"github.com/iris-contrib/swagger/swaggerFiles" "github.com/iris-contrib/swagger/swaggerFiles"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
@ -17,19 +18,19 @@ import (
//go:embed app/* //go:embed app/*
var embedFrontend embed.FS var embedFrontend embed.FS
// @title Lume Web Portal // @title Lume Web Portal
// @version 1.0 // @version 1.0
// @description A decentralized data storage portal for the open web // @description A decentralized data storage portal for the open web
// @contact.name Lume Web Project // @contact.name Lume Web Project
// @contact.url https://lumeweb.com // @contact.url https://lumeweb.com
// @contact.email contact@lumeweb.com // @contact.email contact@lumeweb.com
// @license.name MIT // @license.name MIT
// @license.url https://opensource.org/license/mit/ // @license.url https://opensource.org/license/mit/
// @externalDocs.description OpenAPI // @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/ // @externalDocs.url https://swagger.io/resources/open-api/
func main() { func main() {
// Initialize the configuration settings // Initialize the configuration settings
config.Init() config.Init()
@ -40,22 +41,27 @@ func main() {
// Create a new Iris app instance // Create a new Iris app instance
app := iris.New() app := iris.New()
app.Validator = validator.Get()
// Enable Gzip compression for responses // Enable Gzip compression for responses
app.Use(iris.Compression) app.Use(iris.Compression)
// Serve static files from the embedded directory at the app's root path // Serve static files from the embedded directory at the app's root path
app.HandleDir("/", embedFrontend) 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 // 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)) 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)) app.Handle(new(service.AuthService))
}) })
config := swagger.Config{ swaggerConfig := swagger.Config{
// The url pointing to API definition. // The url pointing to API definition.
URL: "http://localhost:8080/swagger/doc.json", URL: "http://localhost:8080/swagger/doc.json",
DeepLinking: true, DeepLinking: true,
@ -64,7 +70,7 @@ func main() {
// The UI prefix URL (see route). // The UI prefix URL (see route).
Prefix: "/swagger", Prefix: "/swagger",
} }
swaggerUI := swagger.Handler(swaggerFiles.Handler, config) swaggerUI := swagger.Handler(swaggerFiles.Handler, swaggerConfig)
app.Get("/swagger", swaggerUI) app.Get("/swagger", swaggerUI)
// And the wildcard one for index.html, *.js, *.css and e.t.c. // And the wildcard one for index.html, *.js, *.css and e.t.c.
@ -74,5 +80,10 @@ func main() {
//go renterd.Main() //go renterd.Main()
// Start the Iris app and listen for incoming requests on port 80 // 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 { type Key struct {
gorm.Model gorm.Model
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
AccountID uint AccountID uint
Account Account Account Account
PublicKey string Pubkey string
PrivateKey string CreatedAt time.Time
CreatedAt time.Time UpdatedAt time.Time
UpdatedAt time.Time
} }

View File

@ -1,22 +1,55 @@
package service package service
import ( import (
"crypto/ed25519"
"encoding/hex"
"errors" "errors"
"git.lumeweb.com/LumeWeb/portal/db" "git.lumeweb.com/LumeWeb/portal/db"
"git.lumeweb.com/LumeWeb/portal/model" "git.lumeweb.com/LumeWeb/portal/model"
_validator "git.lumeweb.com/LumeWeb/portal/validator"
"github.com/go-playground/validator/v10"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
"reflect"
) )
type AccountService struct { type AccountService struct {
ctx iris.Context Ctx iris.Context
} }
type RegisterRequest struct { type RegisterRequest struct {
Email string `json:"email"` Email string `json:"email" validate:"required"`
Password string `json:"password"` 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) { func hashPassword(password string) (string, error) {
@ -34,46 +67,62 @@ func hashPassword(password string) (string, error) {
func (a *AccountService) PostRegister() { func (a *AccountService) PostRegister() {
var r RegisterRequest var r RegisterRequest
if err := a.ctx.ReadJSON(&r); err != nil { if err := a.Ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err) 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)
return return
} }
// Check if an account with the same email address already exists. // Check if an account with the same email address already exists.
existingAccount := model.Account{} 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 { if err == nil {
// An account with the same email address already exists. // An account with the same email address already exists.
// Return an error response to the client. // 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 return
} else if !errors.Is(err, gorm.ErrRecordNotFound) { } else if !errors.Is(err, gorm.ErrRecordNotFound) {
// An unexpected error occurred while querying the database. // An unexpected error occurred while querying the database.
// Return an error response to the client. // Return an error response to the client.
a.ctx.StopWithError(iris.StatusInternalServerError, err) a.Ctx.StopWithError(iris.StatusInternalServerError, err)
return return
} }
// Create a new Account model with the provided email and hashed password. // Create a new Account model with the provided email and hashed password.
account := model.Account{ account := model.Account{
Email: r.Email, Email: r.Email,
Password: &hashedPassword,
} }
// Save the new account to the database. // Hash the password before saving it to the database.
err = db.Get().Create(&account).Error 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 { if err != nil {
a.ctx.StopWithError(iris.StatusInternalServerError, err) a.Ctx.StopWithError(iris.StatusInternalServerError, err)
return return
} }
// Return a success response to the client. // 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 { type AuthService struct {
ctx iris.Context Ctx iris.Context
} }
type LoginRequest struct { type LoginRequest struct {
@ -136,101 +136,101 @@ func (a *AuthService) PostLogin() {
var r LoginRequest var r LoginRequest
// Read the login request from the client. // Read the login request from the client.
if err := a.ctx.ReadJSON(&r); err != nil { if err := a.Ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err) a.Ctx.StopWithError(iris.StatusBadRequest, err)
return return
} }
// Retrieve the account for the given email. // Retrieve the account for the given email.
account := model.Account{} account := model.Account{}
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil { 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 return
} }
// Verify the provided password against the hashed password stored in the database. // Verify the provided password against the hashed password stored in the database.
if err := verifyPassword(*account.Password, r.Password); err != nil { 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 return
} }
// Generate a JWT token for the authenticated user. // Generate a JWT token for the authenticated user.
token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour) token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour)
if err != nil { 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
} }
// Return the JWT token to the client. // Return the JWT token to the client.
err = a.ctx.JSON(&LoginResponse{Token: token}) err = a.Ctx.JSON(&LoginResponse{Token: token})
if err != nil { if err != nil {
panic(fmt.Errorf("Error with login attempt: %s \n", err)) 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() { func (a *AuthService) PostPubkeyChallenge() {
var r LoginRequest var r LoginRequest
// Read the login request from the client. // Read the login request from the client.
if err := a.ctx.ReadJSON(&r); err != nil { if err := a.Ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err) a.Ctx.StopWithError(iris.StatusBadRequest, err)
return return
} }
// Retrieve the account for the given email. // Retrieve the account for the given email.
account := model.Account{} account := model.Account{}
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil { 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 return
} }
// Generate a random challenge string. // Generate a random challenge string.
challenge, err := generateAndSaveChallengeToken(account.ID, time.Minute) challenge, err := generateAndSaveChallengeToken(account.ID, time.Minute)
if err != nil { 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
} }
// Return the challenge to the client. // Return the challenge to the client.
err = a.ctx.JSON(&ChallengeResponse{Challenge: challenge}) err = a.Ctx.JSON(&ChallengeResponse{Challenge: challenge})
if err != nil { if err != nil {
panic(fmt.Errorf("Error with challenge request: %s \n", err)) 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() { func (a *AuthService) PostPubkeyLogin() {
var r PubkeyLoginRequest var r PubkeyLoginRequest
// Read the key login request from the client. // Read the key login request from the client.
if err := a.ctx.ReadJSON(&r); err != nil { if err := a.Ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err) a.Ctx.StopWithError(iris.StatusBadRequest, err)
return return
} }
// Retrieve the key challenge for the given challenge. // Retrieve the key challenge for the given challenge.
challenge := model.KeyChallenge{} challenge := model.KeyChallenge{}
if err := db.Get().Where("challenge = ?", r.Challenge).Preload("Key").First(&challenge).Error; err != nil { 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 return
} }
verifiedToken, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Challenge), blocklist) verifiedToken, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Challenge), blocklist)
if err != nil { 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 return
} }
rawPubKey, err := hex.DecodeString(r.Pubkey) rawPubKey, err := hex.DecodeString(r.Pubkey)
if err != nil { if err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid pubkey")) a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid pubkey"))
return return
} }
rawSignature, err := hex.DecodeString(r.Signature) rawSignature, err := hex.DecodeString(r.Signature)
if err != nil { if err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid signature")) a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid signature"))
return return
} }
@ -238,29 +238,29 @@ func (a *AuthService) PostPubkeyLogin() {
// Verify the challenge signature. // Verify the challenge signature.
if !ed25519.Verify(publicKeyDecoded, []byte(r.Challenge), rawSignature) { 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. // Generate a JWT token for the authenticated user.
token, err := generateAndSaveLoginToken(challenge.AccountID, 24*time.Hour) token, err := generateAndSaveLoginToken(challenge.AccountID, 24*time.Hour)
if err != nil { 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 return
} }
err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims) err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
if err != nil { 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 return
} }
if err := db.Get().Delete(&challenge).Error; err != nil { 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
} }
// Return the JWT token to the client. // Return the JWT token to the client.
err = a.ctx.JSON(&LoginResponse{Token: token}) err = a.Ctx.JSON(&LoginResponse{Token: token})
if err != nil { if err != nil {
panic(fmt.Errorf("Error with login attempt: %s \n", err)) panic(fmt.Errorf("Error with login attempt: %s \n", err))
} }
@ -272,15 +272,15 @@ func (a *AuthService) PostLogout() {
var r LogoutRequest var r LogoutRequest
// Read the logout request from the client. // Read the logout request from the client.
if err := a.ctx.ReadJSON(&r); err != nil { if err := a.Ctx.ReadJSON(&r); err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, err) a.Ctx.StopWithError(iris.StatusBadRequest, err)
return return
} }
// Verify the provided token. // Verify the provided token.
claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Token), blocklist) claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Token), blocklist)
if err != nil { if err != nil {
a.ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid token")) a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid token"))
return return
} }
@ -290,5 +290,5 @@ func (a *AuthService) PostLogout() {
} }
// Return a success response to the client. // 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
}