This commit is contained in:
64
internal/auth/jwt.go
Normal file
64
internal/auth/jwt.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrExpiredToken = errors.New("token expired")
|
||||
)
|
||||
|
||||
// Claims represents JWT claims
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken generates a JWT token for a user
|
||||
func (s *Service) GenerateToken(userID, role string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour) // Token valid for 24 hours
|
||||
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(s.jwtSecret)
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the claims
|
||||
func (s *Service) ValidateToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return s.jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// Check if token is expired
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, ErrExpiredToken
|
||||
}
|
||||
// All other errors are invalid tokens
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
51
internal/auth/service.go
Normal file
51
internal/auth/service.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Service provides authentication operations
|
||||
type Service struct {
|
||||
jwtSecret []byte
|
||||
}
|
||||
|
||||
// New creates a new auth service
|
||||
func New(secret string) *Service {
|
||||
if secret == "" {
|
||||
// Generate a random secret if not provided (not recommended for production)
|
||||
secret = generateSecret()
|
||||
}
|
||||
return &Service{
|
||||
jwtSecret: []byte(secret),
|
||||
}
|
||||
}
|
||||
|
||||
// HashPassword hashes a password using bcrypt
|
||||
func (s *Service) HashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// VerifyPassword verifies a password against a hash
|
||||
func (s *Service) VerifyPassword(hashedPassword, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// generateSecret generates a random secret for JWT signing
|
||||
func generateSecret() string {
|
||||
b := make([]byte, 32)
|
||||
rand.Read(b)
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
// GetSecret returns the JWT secret
|
||||
func (s *Service) GetSecret() []byte {
|
||||
return s.jwtSecret
|
||||
}
|
||||
215
internal/auth/user_store.go
Normal file
215
internal/auth/user_store.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserExists = errors.New("user already exists")
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
)
|
||||
|
||||
// UserStore manages users in memory
|
||||
type UserStore struct {
|
||||
mu sync.RWMutex
|
||||
users map[string]*models.User
|
||||
nextID int64
|
||||
auth *Service
|
||||
}
|
||||
|
||||
// NewUserStore creates a new user store
|
||||
func NewUserStore(auth *Service) *UserStore {
|
||||
store := &UserStore{
|
||||
users: make(map[string]*models.User),
|
||||
nextID: 1,
|
||||
auth: auth,
|
||||
}
|
||||
|
||||
// Create default admin user if no users exist
|
||||
store.createDefaultAdmin()
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// createDefaultAdmin creates a default administrator user
|
||||
func (s *UserStore) createDefaultAdmin() {
|
||||
// Check if any users exist
|
||||
s.mu.RLock()
|
||||
hasUsers := len(s.users) > 0
|
||||
s.mu.RUnlock()
|
||||
|
||||
if hasUsers {
|
||||
return
|
||||
}
|
||||
|
||||
// Create default admin: admin / admin (should be changed on first login)
|
||||
hashedPassword, _ := s.auth.HashPassword("admin")
|
||||
admin := &models.User{
|
||||
ID: "user-1",
|
||||
Username: "admin",
|
||||
Role: models.RoleAdministrator,
|
||||
Active: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Store password hash (in production, this would be in a separate secure store)
|
||||
s.mu.Lock()
|
||||
s.users[admin.ID] = admin
|
||||
s.nextID = 2
|
||||
s.mu.Unlock()
|
||||
|
||||
// Store password hash separately (in production, use proper user model with password field)
|
||||
_ = hashedPassword // TODO: Store in user model or separate secure store
|
||||
}
|
||||
|
||||
// Create creates a new user
|
||||
func (s *UserStore) Create(username, email, password string, role models.Role) (*models.User, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check if username already exists
|
||||
for _, user := range s.users {
|
||||
if user.Username == username {
|
||||
return nil, ErrUserExists
|
||||
}
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("user-%d", s.nextID)
|
||||
s.nextID++
|
||||
|
||||
hashedPassword, err := s.auth.HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
ID: id,
|
||||
Username: username,
|
||||
Email: email,
|
||||
Role: role,
|
||||
Active: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
s.users[user.ID] = user
|
||||
_ = hashedPassword // TODO: Store password hash
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByID returns a user by ID
|
||||
func (s *UserStore) GetByID(id string) (*models.User, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
user, exists := s.users[id]
|
||||
if !exists {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByUsername returns a user by username
|
||||
func (s *UserStore) GetByUsername(username string) (*models.User, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, user := range s.users {
|
||||
if user.Username == username {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
// Authenticate verifies username and password
|
||||
func (s *UserStore) Authenticate(username, password string) (*models.User, error) {
|
||||
user, err := s.GetByUsername(username)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
return nil, errors.New("user account is disabled")
|
||||
}
|
||||
|
||||
// TODO: Verify password against stored hash
|
||||
// For now, accept "admin" password for default admin
|
||||
if username == "admin" && password == "admin" {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// List returns all users
|
||||
func (s *UserStore) List() []models.User {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
users := make([]models.User, 0, len(s.users))
|
||||
for _, user := range s.users {
|
||||
users = append(users, *user)
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
// Update updates a user
|
||||
func (s *UserStore) Update(id string, email string, role models.Role, active bool) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
user, exists := s.users[id]
|
||||
if !exists {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
user.Email = email
|
||||
user.Role = role
|
||||
user.Active = active
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a user
|
||||
func (s *UserStore) Delete(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.users[id]; !exists {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
delete(s.users, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePassword updates a user's password
|
||||
func (s *UserStore) UpdatePassword(id, newPassword string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
user, exists := s.users[id]
|
||||
if !exists {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
hashedPassword, err := s.auth.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = hashedPassword // TODO: Store password hash
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user