Skip to content

Instantly share code, notes, and snippets.

@matevarga
Last active August 23, 2024 05:52
Show Gist options
  • Save matevarga/daf2273ddea0644ba6a6e91ae22442e0 to your computer and use it in GitHub Desktop.
Save matevarga/daf2273ddea0644ba6a6e91ae22442e0 to your computer and use it in GitHub Desktop.
func initUser(app *pocketbase.PocketBase, email string) (*models.Record, error) {
log.Println("initUser", email)
// Check if the user already exists
existingUser, _ := app.Dao().FindAuthRecordByEmail("users", email)
if existingUser != nil {
// User already exists
return existingUser, nil
}
// Create a new user
userCollection, err := app.Dao().FindCollectionByNameOrId("users")
if err != nil {
return nil, err
}
newUser := models.NewRecord(userCollection)
newUser.Set("email", email)
newUser.Set("verified", false)
newUser.Set("username", email)
// Save the new user
if err := app.Dao().SaveRecord(newUser); err != nil {
return nil, err
}
return newUser, nil
}
func registerOtpHandler(app *pocketbase.PocketBase) {
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// TODO: add a cron job to delete expired otp_auth records
// This is not a security related and can be done occasionally.
// Step 1: User requests an OTP token
//
// The otp-auth endpoint is used to generate an otp_auth record. If the user's email
// is found, the OTP is stored, emailed, and verifyToken (this record's ID) is returned
// to the caller.
e.Router.POST("/otp-auth", func(c echo.Context) error {
data := apis.RequestInfo(c).Data
log.Println("data", data)
email, ok := data["email"].(string)
if !ok {
return badEmailErr
}
user, err := app.Dao().FindAuthRecordByEmail("users", email)
if user == nil {
user, err = initUser(app, email)
if err != nil {
return err
}
}
collection, err := app.Dao().FindCollectionByNameOrId("otp_auth")
if err != nil {
log.Println("find collection error", err)
return apis.NewBadRequestError("", nil)
}
record := models.NewRecord(collection)
record.Set("user", user.GetId())
record.Set("expiration", time.Now().Add(time.Minute*60))
record.Set("creation", time.Now())
otp := security.RandomStringWithAlphabet(6, "0123456789")
record.Set("otp", otp)
log.Println("otp", otp) // For testing purposes. Normally, you would send this to the user's email.
if err := app.Dao().SaveRecord(record); err != nil {
log.Println("save error", err)
return apis.NewBadRequestError("", nil)
}
return c.JSON(200, echo.Map{
"verifyToken": record.GetId(),
})
})
// Step 2: User submits the OTP token for verification
//
// The otp-verify endpoint is used to verify the OTP token. If the token is valid,
// the user is authenticated and the standard RecordAuthResponse is returned.
e.Router.POST("/otp-verify", func(c echo.Context) error {
data := struct {
VerifyToken string `json:"verifyToken"`
OTP string `json:"otp"`
}{}
if err := c.Bind(&data); err != nil {
log.Println("bind error", err)
return unauthorizedErr
}
record, err := app.Dao().FindRecordById("otp_auth", data.VerifyToken)
if err != nil {
return unauthorizedErr
}
// validate expiration
if record.GetDateTime("expiration").Time().Before(time.Now()) {
app.Dao().DeleteRecord(record)
return unauthorizedErr
}
// validate otp
if !security.Equal(record.GetString("otp"), data.OTP) {
attempts := record.GetInt("attempts") + 1
if attempts > 3 {
app.Dao().DeleteRecord(record)
return unauthorizedErr
}
record.Set("attempts", attempts)
if err := app.Dao().SaveRecord(record); err != nil {
log.Println("save error", err)
}
return unauthorizedErr
}
// At this point the OTP record is consumed and should be deleted
defer app.Dao().DeleteRecord(record)
if err := app.Dao().ExpandRecord(record, []string{"user"}, nil); len(err) > 0 {
log.Println("expand error", err)
return unauthorizedErr
}
user := record.ExpandedOne("user")
if user == nil {
return unauthorizedErr
}
user.SetVerified(true)
app.Dao().SaveRecord(user)
return apis.RecordAuthResponse(app, c, user, nil)
})
return nil
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment