2026-07-07 10:29:28 +08:00
|
|
|
package auth
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"crypto/subtle"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
|
2026-07-07 14:40:09 +08:00
|
|
|
"onixbyte.com/pipely/internal/database"
|
|
|
|
|
"onixbyte.com/pipely/internal/model"
|
2026-07-07 10:29:28 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
ErrInvalidCredentials = errors.New("invalid username or password")
|
|
|
|
|
ErrUserExists = errors.New("username already exists")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Service struct {
|
|
|
|
|
db *gorm.DB
|
|
|
|
|
secret []byte
|
|
|
|
|
expiry time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewService(db *gorm.DB, secret []byte, expiryHrs int) *Service {
|
|
|
|
|
return &Service{
|
|
|
|
|
db: db,
|
|
|
|
|
secret: secret,
|
|
|
|
|
expiry: time.Duration(expiryHrs) * time.Hour,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 15:57:26 +08:00
|
|
|
func (s *Service) ExpiryHrs() int {
|
|
|
|
|
return int(s.expiry.Hours())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 10:29:28 +08:00
|
|
|
func (s *Service) Login(username, password string) (*model.LoginResponse, error) {
|
|
|
|
|
user, err := database.GetAdminUserByUsername(s.db, username)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, ErrInvalidCredentials
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hash := hashPassword(password)
|
|
|
|
|
if subtle.ConstantTimeCompare([]byte(hash), []byte(user.PasswordHash)) != 1 {
|
|
|
|
|
return nil, ErrInvalidCredentials
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token, expiresAt, err := s.generateToken(user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &model.LoginResponse{
|
|
|
|
|
Token: token,
|
|
|
|
|
ExpiresAt: expiresAt,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) CreateInitialAdmin(username, password string) error {
|
|
|
|
|
_, err := database.GetAdminUserByUsername(s.db, username)
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil // admin already exists
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user := &model.AdminUser{
|
|
|
|
|
Username: username,
|
|
|
|
|
PasswordHash: hashPassword(password),
|
|
|
|
|
Role: "admin",
|
|
|
|
|
}
|
|
|
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
return database.CreateAdminUser(tx, user)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) ValidateToken(tokenString string) (*model.AdminUser, error) {
|
|
|
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
|
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
|
|
|
return nil, errors.New("unexpected signing method")
|
|
|
|
|
}
|
|
|
|
|
return s.secret, nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
|
|
|
if !ok || !token.Valid {
|
|
|
|
|
return nil, errors.New("invalid token")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
username, _ := claims["sub"].(string)
|
|
|
|
|
if username == "" {
|
|
|
|
|
return nil, errors.New("invalid token subject")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return database.GetAdminUserByUsername(s.db, username)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) generateToken(user *model.AdminUser) (string, int64, error) {
|
|
|
|
|
expiresAt := time.Now().Add(s.expiry)
|
|
|
|
|
claims := jwt.MapClaims{
|
|
|
|
|
"sub": user.Username,
|
|
|
|
|
"role": user.Role,
|
|
|
|
|
"iat": time.Now().Unix(),
|
|
|
|
|
"exp": expiresAt.Unix(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
|
tokenStr, err := token.SignedString(s.secret)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tokenStr, expiresAt.Unix(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func hashPassword(password string) string {
|
|
|
|
|
h := sha256.Sum256([]byte(password))
|
|
|
|
|
return hex.EncodeToString(h[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) ListUsers() ([]model.AdminUser, error) {
|
|
|
|
|
return database.ListAdminUsers(s.db)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) CreateUser(username, password, role string) (*model.AdminUser, error) {
|
|
|
|
|
_, err := database.GetAdminUserByUsername(s.db, username)
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil, ErrUserExists
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if role == "" {
|
|
|
|
|
role = "admin"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user := &model.AdminUser{
|
|
|
|
|
Username: username,
|
|
|
|
|
PasswordHash: hashPassword(password),
|
|
|
|
|
Role: role,
|
|
|
|
|
}
|
|
|
|
|
if err := database.CreateAdminUser(s.db, user); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return user, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) DeleteUser(id int64) error {
|
|
|
|
|
return database.DeleteAdminUser(s.db, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) ChangePassword(id int64, newPassword string) error {
|
|
|
|
|
return database.UpdateAdminPassword(s.db, id, hashPassword(newPassword))
|
|
|
|
|
}
|