summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-03 18:30:54 +0530
committerBobby <[email protected]>2026-03-03 18:31:56 +0530
commitc0a03e523b4d46a4070ceb6a5d9bad8797ba376a (patch)
treeb5ff6b43721551aa4ba67eabf6616604a7bf9dfa
parentf6e671264734b8d82b48102041ad7f92ec2f1048 (diff)
downloadpagoda-c0a03e523b4d46a4070ceb6a5d9bad8797ba376a.tar.xz
pagoda-c0a03e523b4d46a4070ceb6a5d9bad8797ba376a.zip
feat: implement user authentication and registration with token support
-rw-r--r--shrine/config/env.go13
-rw-r--r--shrine/controllers/auth.go89
-rw-r--r--shrine/controllers/responses.go (renamed from shrine/controllers/errors.go)8
-rw-r--r--shrine/controllers/sample.go17
-rw-r--r--shrine/database/migrate.go4
-rw-r--r--shrine/enums/role.go9
-rw-r--r--shrine/go.mod2
-rw-r--r--shrine/models/token.go34
-rw-r--r--shrine/models/user.go149
-rw-r--r--shrine/repositories/token.go23
-rw-r--r--shrine/repositories/user.go26
-rw-r--r--shrine/router/auth.go17
-rw-r--r--shrine/router/sample.go14
-rw-r--r--shrine/types/request.go13
-rw-r--r--shrine/types/response.go26
-rw-r--r--shrine/utils/auth/auth.go89
-rw-r--r--shrine/utils/collections/set.go28
-rw-r--r--shrine/utils/meta/request.go6
-rw-r--r--shrine/utils/meta/types.go2
-rw-r--r--shrine/utils/meta/value.go4
-rw-r--r--shrine/utils/validators/email.go21
-rw-r--r--shrine/utils/validators/username.go43
22 files changed, 590 insertions, 47 deletions
diff --git a/shrine/config/env.go b/shrine/config/env.go
index f48cd90..931775b 100644
--- a/shrine/config/env.go
+++ b/shrine/config/env.go
@@ -1,11 +1,14 @@
package config
+import "time"
+
type server struct {
- Host string `env:"HOST" default:"0.0.0.0"`
- Port int `env:"PORT" default:"3000"`
- Secret string `env:"SECRET" default:"pagoda-secret"`
- Debug bool `env:"DEBUG" default:"false"`
- CorsOrigins string `env:"CORS_ORIGINS" default:"*"`
+ Host string `env:"HOST" default:"0.0.0.0"`
+ Port int `env:"PORT" default:"3000"`
+ Secret string `env:"SECRET" default:"pagoda-secret"`
+ Debug bool `env:"DEBUG" default:"false"`
+ CorsOrigins string `env:"CORS_ORIGINS" default:"*"`
+ TokenExpiry time.Duration `env:"TOKEN_EXPIRY" default:"720h"`
}
type database struct {
diff --git a/shrine/controllers/auth.go b/shrine/controllers/auth.go
new file mode 100644
index 0000000..c6c2b7a
--- /dev/null
+++ b/shrine/controllers/auth.go
@@ -0,0 +1,89 @@
+package controllers
+
+import (
+ "errors"
+ "shrine/models"
+ "shrine/repositories"
+ "shrine/types"
+ "shrine/utils/auth"
+ "shrine/utils/meta"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func RegisterController(context *fiber.Ctx) error {
+ body, err := meta.Body[types.RegisterRequest](context)
+ if err != nil {
+ return BadRequest(context, errors.New("invalid request body"))
+ }
+
+ user := models.User{
+ Username: body.Username,
+ Email: body.Email,
+ DisplayName: body.DisplayName,
+ }
+
+ if err := user.SetPassword(body.Password); err != nil {
+ return BadRequest(context, err)
+ }
+
+ if err := repositories.CreateUser(&user); err != nil {
+ return BadRequest(context, err)
+ }
+
+ token, err := auth.IssueToken(context, user.ID)
+ if err != nil {
+ return InternalServerError(context, errors.New("failed to create session"))
+ }
+
+ return Created(context, types.AuthResponse{
+ Token: token,
+ User: user.ToResponse(),
+ })
+}
+
+func LoginController(context *fiber.Ctx) error {
+ body, err := meta.Body[types.LoginRequest](context)
+ if err != nil {
+ return BadRequest(context, errors.New("invalid request body"))
+ }
+
+ user, err := repositories.FindUserByUsername(body.Username)
+ if err != nil {
+ return Unauthorized(context, errors.New("invalid credentials"))
+ }
+
+ if !user.CanAuthenticate() {
+ return Forbidden(context, errors.New("account is not eligible for authentication"))
+ }
+
+ if !user.CheckPassword(body.Password) {
+ return Unauthorized(context, errors.New("invalid credentials"))
+ }
+
+ token, err := auth.IssueToken(context, user.ID)
+ if err != nil {
+ return InternalServerError(context, errors.New("failed to create session"))
+ }
+
+ return Success(context, types.AuthResponse{
+ Token: token,
+ User: user.ToResponse(),
+ })
+}
+
+func LogoutController(context *fiber.Ctx) error {
+ tokenHash := auth.GetTokenHash(context)
+ if err := repositories.DeleteToken(tokenHash); err != nil {
+ return InternalServerError(context, errors.New("failed to end session"))
+ }
+
+ return Success(context, types.MessageResponse{
+ Message: "logged out",
+ })
+}
+
+func MeController(context *fiber.Ctx) error {
+ user := auth.GetUser(context)
+ return Success(context, user.ToResponse())
+} \ No newline at end of file
diff --git a/shrine/controllers/errors.go b/shrine/controllers/responses.go
index 55eb203..b3fa824 100644
--- a/shrine/controllers/errors.go
+++ b/shrine/controllers/responses.go
@@ -42,3 +42,11 @@ func DefaultError(context *fiber.Ctx, err error) error {
Error: err.Error(),
}).As(fiber.StatusInternalServerError)
}
+
+func Success(context *fiber.Ctx, data any) error {
+ return shortcuts.Response(context, data).As(fiber.StatusOK)
+}
+
+func Created(context *fiber.Ctx, data any) error {
+ return shortcuts.Response(context, data).As(fiber.StatusCreated)
+}
diff --git a/shrine/controllers/sample.go b/shrine/controllers/sample.go
deleted file mode 100644
index 542d86c..0000000
--- a/shrine/controllers/sample.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package controllers
-
-import (
- "fmt"
- "shrine/types"
- "shrine/utils/meta"
- "shrine/utils/shortcuts"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func HelloController(context *fiber.Ctx) error {
- name := meta.Request(context).Default("World").Query("name")
- return shortcuts.Response(context, types.HelloResponse{
- Message: fmt.Sprintf("Hello, %s!", name),
- }).As(fiber.StatusOK)
-}
diff --git a/shrine/database/migrate.go b/shrine/database/migrate.go
index 7d99788..5835a76 100644
--- a/shrine/database/migrate.go
+++ b/shrine/database/migrate.go
@@ -1,12 +1,14 @@
package database
import (
+ "shrine/models"
"shrine/utils/logger"
)
func migrate() {
err := DB.AutoMigrate(
- // Models will be added here as they are created
+ &models.User{},
+ &models.Token{},
)
if err != nil {
logger.Fatalf("Database", "Error during database migration: %v", err)
diff --git a/shrine/enums/role.go b/shrine/enums/role.go
new file mode 100644
index 0000000..3b38cbb
--- /dev/null
+++ b/shrine/enums/role.go
@@ -0,0 +1,9 @@
+package enums
+
+type UserRole string
+
+const (
+ Member UserRole = "member"
+ Moderator UserRole = "moderator"
+ Admin UserRole = "admin"
+) \ No newline at end of file
diff --git a/shrine/go.mod b/shrine/go.mod
index 137a175..d61e853 100644
--- a/shrine/go.mod
+++ b/shrine/go.mod
@@ -6,6 +6,7 @@ require (
github.com/gofiber/fiber/v2 v2.52.12
github.com/joho/godotenv v1.5.1
go.uber.org/zap v1.27.1
+ golang.org/x/crypto v0.31.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
@@ -31,7 +32,6 @@ require (
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
- golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
diff --git a/shrine/models/token.go b/shrine/models/token.go
new file mode 100644
index 0000000..2c25c4e
--- /dev/null
+++ b/shrine/models/token.go
@@ -0,0 +1,34 @@
+package models
+
+import (
+ "time"
+
+ "gorm.io/gorm"
+)
+
+type Token struct {
+ gorm.Model
+ TokenHash string `gorm:"uniqueIndex;size:64;not null"`
+ UserID uint `gorm:"index;not null"`
+ User User `gorm:"foreignKey:UserID"`
+ ExpiresAt time.Time
+ IPAddress string `gorm:"size:45"`
+ UserAgent string `gorm:"size:512"`
+}
+
+func (token *Token) IsExpired() bool {
+ return time.Now().After(token.ExpiresAt)
+}
+
+func (token *Token) BeforeCreate(tx *gorm.DB) error {
+ if token.TokenHash == "" {
+ return gorm.ErrInvalidData
+ }
+ if token.UserID == 0 {
+ return gorm.ErrInvalidData
+ }
+ if token.ExpiresAt.IsZero() {
+ return gorm.ErrInvalidData
+ }
+ return nil
+} \ No newline at end of file
diff --git a/shrine/models/user.go b/shrine/models/user.go
new file mode 100644
index 0000000..85bd0bf
--- /dev/null
+++ b/shrine/models/user.go
@@ -0,0 +1,149 @@
+package models
+
+import (
+ "fmt"
+ "shrine/enums"
+ "shrine/types"
+ "shrine/utils/validators"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/bcrypt"
+ "gorm.io/gorm"
+)
+
+type User struct {
+ gorm.Model
+ Username string `gorm:"uniqueIndex;size:32;not null"`
+ Email string `gorm:"uniqueIndex;size:255;not null"`
+ PasswordHash string `gorm:"size:255;not null"`
+ DisplayName string `gorm:"size:50;not null"`
+ Bio string `gorm:"size:500"`
+ Birthday *time.Time
+ AvatarURL string `gorm:"size:512"`
+ BlinkieURL string `gorm:"size:512"`
+ Website string `gorm:"size:255"`
+ Location string `gorm:"size:100"`
+ Pronouns string `gorm:"size:50"`
+ Signature string `gorm:"size:500"`
+ Role enums.UserRole `gorm:"size:20;not null;default:member"`
+ EmailVerified bool `gorm:"not null;default:false"`
+ AccountBanned bool `gorm:"not null;default:false"`
+ BannedAt *time.Time
+ BannedReason string `gorm:"size:500"`
+ BannedBy *uint `gorm:"index"`
+ AccountDisabled bool `gorm:"not null;default:false"`
+ DisabledAt *time.Time
+ DisabledReason string `gorm:"size:500"`
+ DisabledBy *uint `gorm:"index"`
+ LastSeenAt *time.Time
+ RegistrationIP string `gorm:"size:45"`
+}
+
+func (user *User) SetPassword(password string) error {
+ if len(password) < 8 {
+ return fmt.Errorf("password must be at least 8 characters")
+ }
+ if len(password) > 255 {
+ return fmt.Errorf("password must be at most 255 characters")
+ }
+ hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+ user.PasswordHash = string(hash)
+ return nil
+}
+
+func (user *User) CheckPassword(password string) bool {
+ return bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) == nil
+}
+
+func (user *User) IsAdmin() bool {
+ return user.Role == enums.Admin
+}
+
+func (user *User) IsModerator() bool {
+ return user.Role == enums.Moderator
+}
+
+func (user *User) IsStaff() bool {
+ return user.IsAdmin() || user.IsModerator()
+}
+
+func (user *User) IsBanned() bool {
+ return user.AccountBanned
+}
+
+func (user *User) IsDisabled() bool {
+ return user.AccountDisabled
+}
+
+func (user *User) CanAuthenticate() bool {
+ return !user.AccountBanned && !user.AccountDisabled && user.EmailVerified
+}
+
+func (user *User) ToResponse() types.UserResponse {
+ return types.UserResponse{
+ ID: user.ID,
+ Username: user.Username,
+ Email: user.Email,
+ DisplayName: user.DisplayName,
+ Bio: user.Bio,
+ Birthday: user.Birthday,
+ AvatarURL: user.AvatarURL,
+ BlinkieURL: user.BlinkieURL,
+ Website: user.Website,
+ Location: user.Location,
+ Pronouns: user.Pronouns,
+ Signature: user.Signature,
+ Role: string(user.Role),
+ CreatedAt: user.CreatedAt,
+ }
+}
+
+func (user *User) BeforeCreate(tx *gorm.DB) error {
+ _, bypassUsername := tx.Get("bypass_username_validation")
+
+ if !bypassUsername {
+ if !validators.IsValidUsername(user.Username, 3) {
+ return fmt.Errorf("username must be 3-32 characters, alphanumeric and underscores only")
+ }
+ if validators.IsReservedUsername(user.Username) {
+ return fmt.Errorf("username is reserved")
+ }
+ }
+
+ if !validators.IsValidEmail(user.Email) {
+ return fmt.Errorf("invalid email format")
+ }
+
+ if len(strings.TrimSpace(user.DisplayName)) < 1 || len(strings.TrimSpace(user.DisplayName)) > 50 {
+ return fmt.Errorf("display name must be between 1 and 50 characters")
+ }
+
+ if user.PasswordHash == "" {
+ return fmt.Errorf("password is required")
+ }
+
+ user.Email = strings.ToLower(strings.TrimSpace(user.Email))
+ user.DisplayName = strings.TrimSpace(user.DisplayName)
+ user.Username = strings.TrimSpace(user.Username)
+
+ return nil
+}
+
+func (user *User) BeforeUpdate(tx *gorm.DB) error {
+ if !validators.IsValidEmail(user.Email) {
+ return fmt.Errorf("invalid email format")
+ }
+
+ if len(strings.TrimSpace(user.DisplayName)) < 1 || len(strings.TrimSpace(user.DisplayName)) > 50 {
+ return fmt.Errorf("display name must be between 1 and 50 characters")
+ }
+
+ user.Email = strings.ToLower(strings.TrimSpace(user.Email))
+ user.DisplayName = strings.TrimSpace(user.DisplayName)
+
+ return nil
+} \ No newline at end of file
diff --git a/shrine/repositories/token.go b/shrine/repositories/token.go
new file mode 100644
index 0000000..ef671f8
--- /dev/null
+++ b/shrine/repositories/token.go
@@ -0,0 +1,23 @@
+package repositories
+
+import (
+ "shrine/database"
+ "shrine/models"
+ "time"
+)
+
+func CreateToken(token *models.Token) error {
+ return database.DB.Create(token).Error
+}
+
+func FindValidToken(tokenHash string) (*models.Token, error) {
+ var token models.Token
+ err := database.DB.Preload("User").
+ Where("token_hash = ? AND expires_at > ?", tokenHash, time.Now()).
+ First(&token).Error
+ return &token, err
+}
+
+func DeleteToken(tokenHash string) error {
+ return database.DB.Where("token_hash = ?", tokenHash).Delete(&models.Token{}).Error
+} \ No newline at end of file
diff --git a/shrine/repositories/user.go b/shrine/repositories/user.go
new file mode 100644
index 0000000..93205a0
--- /dev/null
+++ b/shrine/repositories/user.go
@@ -0,0 +1,26 @@
+package repositories
+
+import (
+ "shrine/database"
+ "shrine/enums"
+ "shrine/models"
+)
+
+func CreateUser(user *models.User) error {
+ var count int64
+ database.DB.Model(&models.User{}).Count(&count)
+
+ tx := database.DB
+ if count == 0 {
+ user.Role = enums.Admin
+ tx = tx.Set("bypass_username_validation", true)
+ }
+
+ return tx.Create(user).Error
+}
+
+func FindUserByUsername(username string) (*models.User, error) {
+ var user models.User
+ err := database.DB.Where("username = ?", username).First(&user).Error
+ return &user, err
+} \ No newline at end of file
diff --git a/shrine/router/auth.go b/shrine/router/auth.go
new file mode 100644
index 0000000..660ba7f
--- /dev/null
+++ b/shrine/router/auth.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "shrine/controllers"
+ "shrine/types"
+ "shrine/utils/auth"
+ "shrine/utils/urls"
+)
+
+func init() {
+ urls.SetNamespace("auth")
+
+ urls.Path(types.POST, "/register", controllers.RegisterController, "register")
+ urls.Path(types.POST, "/login", controllers.LoginController, "login")
+ urls.Path(types.POST, "/logout", auth.RequireAuthentication(controllers.LogoutController), "logout")
+ urls.Path(types.GET, "/me", auth.RequireAuthentication(controllers.MeController), "me")
+} \ No newline at end of file
diff --git a/shrine/router/sample.go b/shrine/router/sample.go
deleted file mode 100644
index e879371..0000000
--- a/shrine/router/sample.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package router
-
-import (
- "shrine/controllers"
- "shrine/types"
- "shrine/utils/auth"
- "shrine/utils/urls"
-)
-
-func init() {
- urls.SetNamespace("sample")
-
- urls.Path(types.GET, "/hello", auth.RequireAuthentication(controllers.HelloController), "hello")
-}
diff --git a/shrine/types/request.go b/shrine/types/request.go
new file mode 100644
index 0000000..096c23e
--- /dev/null
+++ b/shrine/types/request.go
@@ -0,0 +1,13 @@
+package types
+
+type RegisterRequest struct {
+ Username string `json:"username"`
+ Email string `json:"email"`
+ Password string `json:"password"`
+ DisplayName string `json:"display_name"`
+}
+
+type LoginRequest struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+} \ No newline at end of file
diff --git a/shrine/types/response.go b/shrine/types/response.go
index 3d267bc..7e15ecc 100644
--- a/shrine/types/response.go
+++ b/shrine/types/response.go
@@ -1,9 +1,33 @@
package types
+import "time"
+
type ErrorResponse struct {
Error string `json:"error"`
}
-type HelloResponse struct {
+type MessageResponse struct {
Message string `json:"message"`
}
+
+type UserResponse struct {
+ ID uint `json:"id"`
+ Username string `json:"username"`
+ Email string `json:"email"`
+ DisplayName string `json:"display_name"`
+ Bio string `json:"bio"`
+ Birthday *time.Time `json:"birthday"`
+ AvatarURL string `json:"avatar_url"`
+ BlinkieURL string `json:"blinkie_url"`
+ Website string `json:"website"`
+ Location string `json:"location"`
+ Pronouns string `json:"pronouns"`
+ Signature string `json:"signature"`
+ Role string `json:"role"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+type AuthResponse struct {
+ Token string `json:"token"`
+ User UserResponse `json:"user"`
+}
diff --git a/shrine/utils/auth/auth.go b/shrine/utils/auth/auth.go
index 068ddd7..488ba91 100644
--- a/shrine/utils/auth/auth.go
+++ b/shrine/utils/auth/auth.go
@@ -1,9 +1,60 @@
package auth
-import "github.com/gofiber/fiber/v2"
+import (
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/hex"
+ "shrine/config"
+ "shrine/models"
+ "shrine/repositories"
+ "shrine/utils/meta"
+ "strings"
+ "time"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+const (
+ userKey = "__auth_user"
+ tokenHashKey = "__auth_token_hash"
+)
+
+func GenerateToken() (string, error) {
+ bytes := make([]byte, 32)
+ _, err := rand.Read(bytes)
+ if err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(bytes), nil
+}
+
+func HashToken(rawToken string) string {
+ mac := hmac.New(sha256.New, []byte(config.Server.Secret))
+ mac.Write([]byte(rawToken))
+ return hex.EncodeToString(mac.Sum(nil))
+}
func IsAuthenticated(context *fiber.Ctx) bool {
- // We will implement token based authentication in the future
+ header, ok := meta.Request(context).Header("Authorization")
+ if !ok || !strings.HasPrefix(header, "Bearer ") {
+ return false
+ }
+
+ rawToken := strings.TrimPrefix(header, "Bearer ")
+ tokenHash := HashToken(rawToken)
+
+ token, err := repositories.FindValidToken(tokenHash)
+ if err != nil {
+ return false
+ }
+
+ if !token.User.CanAuthenticate() {
+ return false
+ }
+
+ context.Locals(userKey, &token.User)
+ context.Locals(tokenHashKey, tokenHash)
return true
}
@@ -15,3 +66,37 @@ func RequireAuthentication(handler fiber.Handler) fiber.Handler {
return handler(context)
}
}
+
+func GetUser(context *fiber.Ctx) *models.User {
+ user, _ := context.Locals(userKey).(*models.User)
+ return user
+}
+
+func GetTokenHash(context *fiber.Ctx) string {
+ hash, _ := context.Locals(tokenHashKey).(string)
+ return hash
+}
+
+func IssueToken(context *fiber.Ctx, userID uint) (string, error) {
+ token, err := GenerateToken()
+ if err != nil {
+ return "", err
+ }
+
+ request := meta.Request(context)
+ userAgent, _ := request.Header("User-Agent")
+
+ record := models.Token{
+ TokenHash: HashToken(token),
+ UserID: userID,
+ ExpiresAt: time.Now().Add(config.Server.TokenExpiry),
+ IPAddress: request.IP,
+ UserAgent: userAgent,
+ }
+
+ if err := repositories.CreateToken(&record); err != nil {
+ return "", err
+ }
+
+ return token, nil
+} \ No newline at end of file
diff --git a/shrine/utils/collections/set.go b/shrine/utils/collections/set.go
new file mode 100644
index 0000000..69e9de1
--- /dev/null
+++ b/shrine/utils/collections/set.go
@@ -0,0 +1,28 @@
+package collections
+
+type Set[T comparable] map[T]struct{}
+
+func SetOf[T comparable](values ...T) Set[T] {
+ set := make(Set[T], len(values))
+ for _, value := range values {
+ set[value] = struct{}{}
+ }
+ return set
+}
+
+func (set Set[T]) Has(value T) bool {
+ _, exists := set[value]
+ return exists
+}
+
+func (set Set[T]) Add(value T) {
+ set[value] = struct{}{}
+}
+
+func (set Set[T]) Remove(value T) {
+ delete(set, value)
+}
+
+func (set Set[T]) Len() int {
+ return len(set)
+} \ No newline at end of file
diff --git a/shrine/utils/meta/request.go b/shrine/utils/meta/request.go
index b7fd909..25d32a4 100644
--- a/shrine/utils/meta/request.go
+++ b/shrine/utils/meta/request.go
@@ -15,7 +15,7 @@ func Request(context *fiber.Ctx) facade {
logger.Errorf("META", "RequestContext missing in fiber locals")
return facade{}
}
- return facade{request: request, context: context}
+ return facade{Request: request, context: context}
}
func (f facade) Param(key string) (string, bool) {
@@ -29,7 +29,7 @@ func (f facade) Param(key string) (string, bool) {
}
func (f facade) Query(key string) (string, bool) {
- for _, q := range f.request.Query {
+ for _, q := range f.Request.Query {
if q.Key == key {
return q.Value, true
}
@@ -38,7 +38,7 @@ func (f facade) Query(key string) (string, bool) {
}
func (f facade) Header(key string) (string, bool) {
- for _, h := range f.request.Headers {
+ for _, h := range f.Request.Headers {
if h.Key == key {
return h.Value, true
}
diff --git a/shrine/utils/meta/types.go b/shrine/utils/meta/types.go
index 2f2f504..81b3579 100644
--- a/shrine/utils/meta/types.go
+++ b/shrine/utils/meta/types.go
@@ -7,7 +7,7 @@ import (
)
type facade struct {
- request types.Request
+ types.Request
context *fiber.Ctx
}
diff --git a/shrine/utils/meta/value.go b/shrine/utils/meta/value.go
index 5402b75..73972e9 100644
--- a/shrine/utils/meta/value.go
+++ b/shrine/utils/meta/value.go
@@ -1,9 +1,9 @@
package meta
func (f facade) MustHave() required {
- return required{request: f.request, context: f.context}
+ return required{request: f.Request, context: f.context}
}
func (f facade) Default(defaults string) withDefault {
- return withDefault{request: f.request, context: f.context, defaults: defaults}
+ return withDefault{request: f.Request, context: f.context, defaults: defaults}
}
diff --git a/shrine/utils/validators/email.go b/shrine/utils/validators/email.go
new file mode 100644
index 0000000..95608b2
--- /dev/null
+++ b/shrine/utils/validators/email.go
@@ -0,0 +1,21 @@
+package validators
+
+import "strings"
+
+func IsValidEmail(email string) bool {
+ if len(email) < 5 || len(email) > 255 {
+ return false
+ }
+
+ atIndex := strings.Index(email, "@")
+ if atIndex < 1 {
+ return false
+ }
+
+ dotIndex := strings.LastIndex(email, ".")
+ if dotIndex < atIndex+2 || dotIndex >= len(email)-1 {
+ return false
+ }
+
+ return true
+} \ No newline at end of file
diff --git a/shrine/utils/validators/username.go b/shrine/utils/validators/username.go
new file mode 100644
index 0000000..729548d
--- /dev/null
+++ b/shrine/utils/validators/username.go
@@ -0,0 +1,43 @@
+package validators
+
+import (
+ "regexp"
+ "shrine/utils/collections"
+ "strings"
+)
+
+var validUsernamePattern = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
+
+var reservedUsernames = collections.SetOf(
+ "admin", "administrator", "owner",
+ "mod", "moderator", "janitor", "staff",
+ "api", "www", "mail", "email",
+ "support", "help", "helpdesk",
+ "about", "contact", "privacy", "terms", "tos",
+ "null", "undefined", "system", "bot", "guest",
+ "login", "register", "signup", "signin", "logout",
+ "profile", "settings", "account", "dashboard",
+ "shifoo", "pagoda", "deleted", "anonymous",
+ "root", "webmaster", "postmaster", "hostmaster",
+ "noreply", "no_reply", "mailer", "daemon",
+ "abuse", "security", "info", "status",
+ "home", "search", "explore", "discover",
+ "forums", "forum", "chat", "bazaar", "districts",
+ "members", "online", "buttons", "webring",
+ "guestbook", "hitcounter", "letters",
+ "rules", "faq", "blog", "news", "feed",
+ "static", "assets", "uploads", "images", "media",
+ "test", "testing", "debug", "dev", "staging",
+ "everyone", "all", "here", "channel",
+)
+
+func IsValidUsername(username string, minimumLength int) bool {
+ if len(username) < minimumLength || len(username) > 32 {
+ return false
+ }
+ return validUsernamePattern.MatchString(username)
+}
+
+func IsReservedUsername(username string) bool {
+ return reservedUsernames.Has(strings.ToLower(username))
+} \ No newline at end of file