implement graphQL and init postgres

This commit is contained in:
Fran Jurmanović
2025-07-06 19:19:36 +02:00
parent 016728532c
commit 26a0d33592
25 changed files with 1713 additions and 314 deletions

View File

@@ -13,42 +13,98 @@ import (
// User represents a user in the system
type User struct {
BaseModel
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
Username string `json:"username" gorm:"unique;not null;type:varchar(100)"`
Name string `json:"name" gorm:"not null;type:varchar(255)"`
PasswordHash string `json:"-" gorm:"not null;type:text"`
Active bool `json:"active" gorm:"default:true"`
EmailVerified bool `json:"emailVerified" gorm:"default:false"`
EmailVerificationToken string `json:"-" gorm:"type:varchar(255)"`
PasswordResetToken string `json:"-" gorm:"type:varchar(255)"`
PasswordResetExpires *time.Time `json:"-"`
LastLogin *time.Time `json:"lastLogin"`
LoginAttempts int `json:"-" gorm:"default:0"`
LockedUntil *time.Time `json:"-"`
TwoFactorEnabled bool `json:"twoFactorEnabled" gorm:"default:false"`
TwoFactorSecret string `json:"-" gorm:"type:varchar(255)"`
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
AuditLogs []AuditLog `json:"-" gorm:"foreignKey:UserID"`
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
PasswordHash string `json:"-" gorm:"not null;type:varchar(255)"`
FullName string `json:"full_name" gorm:"type:varchar(255)"`
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
}
// UserCreateRequest represents the request to create a new user
type UserCreateRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=50"`
Name string `json:"name" validate:"required,min=2,max=100"`
FullName string `json:"full_name" validate:"required,min=2,max=100"`
Password string `json:"password" validate:"required,min=8"`
RoleIDs []string `json:"roleIds"`
}
// ToUser converts UserCreateRequest to User domain model
func (req *UserCreateRequest) ToUser() (*User, error) {
user := &User{
Email: req.Email,
FullName: req.FullName,
}
// Handle password hashing
if err := user.SetPassword(req.Password); err != nil {
return nil, err
}
// Note: Roles will be set by the service layer after validation
return user, nil
}
// Validate validates the UserCreateRequest
func (req *UserCreateRequest) Validate() error {
if req.Email == "" {
return errors.New("email is required")
}
if !isValidEmail(req.Email) {
return errors.New("invalid email format")
}
if len(req.Password) < 8 {
return errors.New("password must be at least 8 characters")
}
if req.FullName == "" {
return errors.New("full name is required")
}
return nil
}
// UserUpdateRequest represents the request to update a user
type UserUpdateRequest struct {
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"`
Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"`
Active *bool `json:"active,omitempty"`
FullName *string `json:"full_name,omitempty" validate:"omitempty,min=2,max=100"`
RoleIDs []string `json:"roleIds,omitempty"`
}
// ApplyToUser applies the UserUpdateRequest to an existing User
func (req *UserUpdateRequest) ApplyToUser(user *User) error {
if req.Email != nil {
user.Email = *req.Email
}
if req.FullName != nil {
user.FullName = *req.FullName
}
// Note: Roles will be handled by the service layer
return nil
}
// Validate validates the UserUpdateRequest
func (req *UserUpdateRequest) Validate() error {
if req.Email != nil && !isValidEmail(*req.Email) {
return errors.New("invalid email format")
}
if req.FullName != nil && len(*req.FullName) == 0 {
return errors.New("full name cannot be empty")
}
if req.FullName != nil && len(*req.FullName) > 255 {
return errors.New("full name must not exceed 255 characters")
}
return nil
}
// UserResponse represents the response when returning user data
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
FullName string `json:"fullName"`
Roles []string `json:"roles"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// UserLoginRequest represents a login request
type UserLoginRequest struct {
Email string `json:"email" validate:"required,email"`
@@ -65,16 +121,13 @@ type UserLoginResponse struct {
// UserInfo represents public user information
type UserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
Name string `json:"name"`
Active bool `json:"active"`
EmailVerified bool `json:"emailVerified"`
LastLogin *time.Time `json:"lastLogin"`
Roles []RoleInfo `json:"roles"`
Permissions []string `json:"permissions"`
DateCreated time.Time `json:"dateCreated"`
ID string `json:"id"`
Email string `json:"email"`
FullName string `json:"full_name"`
Roles []RoleInfo `json:"roles"`
Permissions []string `json:"permissions"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ChangePasswordRequest represents a password change request
@@ -98,10 +151,9 @@ type ResetPasswordConfirmRequest struct {
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.BaseModel.BeforeCreate()
// Normalize email and username
// Normalize email and full name
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
u.Name = strings.TrimSpace(u.Name)
u.FullName = strings.TrimSpace(u.FullName)
return u.Validate()
}
@@ -114,11 +166,8 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
if u.Email != "" {
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
}
if u.Username != "" {
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
}
if u.Name != "" {
u.Name = strings.TrimSpace(u.Name)
if u.FullName != "" {
u.FullName = strings.TrimSpace(u.FullName)
}
return u.Validate()
@@ -134,24 +183,8 @@ func (u *User) Validate() error {
return errors.New("invalid email format")
}
if u.Username == "" {
return errors.New("username is required")
}
if len(u.Username) < 3 || len(u.Username) > 50 {
return errors.New("username must be between 3 and 50 characters")
}
if !isValidUsername(u.Username) {
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
}
if u.Name == "" {
return errors.New("name is required")
}
if len(u.Name) < 2 || len(u.Name) > 100 {
return errors.New("name must be between 2 and 100 characters")
if u.FullName != "" && len(u.FullName) > 255 {
return errors.New("full name must not exceed 255 characters")
}
return nil
@@ -182,55 +215,16 @@ func (u *User) VerifyPassword(plainPassword string) bool {
return u.CheckPassword(plainPassword)
}
// IsLocked checks if the user account is locked
func (u *User) IsLocked() bool {
if u.LockedUntil == nil {
return false
}
return time.Now().Before(*u.LockedUntil)
}
// Lock locks the user account for the specified duration
func (u *User) Lock(duration time.Duration) {
lockUntil := time.Now().Add(duration)
u.LockedUntil = &lockUntil
}
// Unlock unlocks the user account
func (u *User) Unlock() {
u.LockedUntil = nil
u.LoginAttempts = 0
}
// IncrementLoginAttempts increments the login attempt counter
func (u *User) IncrementLoginAttempts() {
u.LoginAttempts++
}
// ResetLoginAttempts resets the login attempt counter
func (u *User) ResetLoginAttempts() {
u.LoginAttempts = 0
}
// UpdateLastLogin updates the last login timestamp
func (u *User) UpdateLastLogin() {
now := time.Now()
u.LastLogin = &now
}
// ToUserInfo converts User to UserInfo (public information)
func (u *User) ToUserInfo() UserInfo {
userInfo := UserInfo{
ID: u.ID,
Email: u.Email,
Username: u.Username,
Name: u.Name,
Active: u.Active,
EmailVerified: u.EmailVerified,
LastLogin: u.LastLogin,
DateCreated: u.DateCreated,
Roles: make([]RoleInfo, len(u.Roles)),
Permissions: []string{},
ID: u.ID,
Email: u.Email,
FullName: u.FullName,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
Roles: make([]RoleInfo, len(u.Roles)),
Permissions: []string{},
}
// Convert roles and collect permissions
@@ -250,6 +244,23 @@ func (u *User) ToUserInfo() UserInfo {
return userInfo
}
// ToResponse converts User to UserResponse (for API responses)
func (u *User) ToResponse() *UserResponse {
roleNames := make([]string, len(u.Roles))
for i, role := range u.Roles {
roleNames[i] = role.Name
}
return &UserResponse{
ID: u.ID,
Email: u.Email,
FullName: u.FullName,
Roles: roleNames,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// HasRole checks if the user has a specific role
func (u *User) HasRole(roleName string) bool {
for _, role := range u.Roles {
@@ -278,12 +289,6 @@ func isValidEmail(email string) bool {
return emailRegex.MatchString(email)
}
// isValidUsername validates username format
func isValidUsername(username string) bool {
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
return usernameRegex.MatchString(username)
}
// validatePassword validates password strength
func validatePassword(password string) error {
if len(password) < 8 {
@@ -294,25 +299,5 @@ func validatePassword(password string) error {
return errors.New("password must not exceed 128 characters")
}
// Check for at least one lowercase letter
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
return errors.New("password must contain at least one lowercase letter")
}
// Check for at least one uppercase letter
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
return errors.New("password must contain at least one uppercase letter")
}
// Check for at least one digit
if matched, _ := regexp.MatchString(`\d`, password); !matched {
return errors.New("password must contain at least one digit")
}
// Check for at least one special character
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
return errors.New("password must contain at least one special character")
}
return nil
}