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:
2026-07-07 10:29:28 +08:00
commit e070a3fb5f
59 changed files with 6515 additions and 0 deletions
+155
View File
@@ -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))
}