feat: initial open-source release of Pipely
OTA update server designed for high-concurrency, low-bandwidth deployments. GORM-backed PostgreSQL, JWT auth, device management, artefact versioning, deployment rollout with gated rate-limited downloads, and a React admin panel.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"onixbyte.com/message-converter-manifest/internal/database"
|
||||
"onixbyte.com/message-converter-manifest/internal/model"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user