Files
NexaVPN/backend/internal/auth/service.go
nessi 830491cb0d chore: initial project scaffold with admin web, backend, desktop client, and deployment setup
Add monorepo structure for NexaVPN WireGuard control plane including:
- .gitignore for node_modules, build artifacts, and environment files
- README with project overview, monorepo layout, and quick start guide
- Admin web UI with React, Vite, TypeScript, and nginx reverse proxy
- API client with type definitions for users, devices, policies, gateways, and audit logs
- Admin pages for dashboard, users, devices, policies, g
2026-03-15 16:32:34 +01:00

156 lines
4.2 KiB
Go

package auth
import (
"context"
"errors"
"time"
"github.com/google/uuid"
)
var ErrInvalidCredentials = errors.New("invalid credentials")
type UserRecord struct {
ID uuid.UUID
Username string
DisplayName string
Role string
PasswordHash string
IsActive bool
}
type Repository interface {
FindUserByUsername(ctx context.Context, username string) (UserRecord, error)
CreateSession(ctx context.Context, userID uuid.UUID, expiresAt time.Time, ipAddress string, userAgent string) (uuid.UUID, error)
StoreRefreshToken(ctx context.Context, sessionID uuid.UUID, userID uuid.UUID, tokenHash string, expiresAt time.Time) error
FindRefreshToken(ctx context.Context, tokenHash string) (UserRecord, uuid.UUID, error)
RevokeRefreshToken(ctx context.Context, tokenHash string) error
HasUsers(ctx context.Context) (bool, error)
CreateBootstrapAdmin(ctx context.Context, username, displayName, passwordHash string) (UserRecord, error)
}
type Service struct {
repo Repository
jwtSecret string
jwtIssuer string
accessTokenTTL time.Duration
refreshTokenTTL time.Duration
}
func NewService(repo Repository, jwtSecret, jwtIssuer string, accessTokenTTL, refreshTokenTTL time.Duration) *Service {
return &Service{
repo: repo,
jwtSecret: jwtSecret,
jwtIssuer: jwtIssuer,
accessTokenTTL: accessTokenTTL,
refreshTokenTTL: refreshTokenTTL,
}
}
func (s *Service) Login(ctx context.Context, username, password, ipAddress, userAgent string) (LoginResponse, error) {
record, err := s.repo.FindUserByUsername(ctx, username)
if err != nil || !record.IsActive || !VerifyPassword(record.PasswordHash, password) {
return LoginResponse{}, ErrInvalidCredentials
}
sessionID, err := s.repo.CreateSession(ctx, record.ID, time.Now().Add(s.refreshTokenTTL), ipAddress, userAgent)
if err != nil {
return LoginResponse{}, err
}
plainRefresh, hashedRefresh, err := NewRefreshToken()
if err != nil {
return LoginResponse{}, err
}
if err := s.repo.StoreRefreshToken(ctx, sessionID, record.ID, hashedRefresh, time.Now().Add(s.refreshTokenTTL)); err != nil {
return LoginResponse{}, err
}
access, err := SignAccessToken(s.jwtSecret, s.jwtIssuer, s.accessTokenTTL, Claims{
UserID: record.ID,
Username: record.Username,
Role: record.Role,
Session: sessionID,
})
if err != nil {
return LoginResponse{}, err
}
return LoginResponse{
AccessToken: access,
RefreshToken: plainRefresh,
ExpiresIn: int64(s.accessTokenTTL.Seconds()),
User: UserView{
ID: record.ID,
Username: record.Username,
DisplayName: record.DisplayName,
Role: record.Role,
},
}, nil
}
func (s *Service) Refresh(ctx context.Context, refreshToken string) (LoginResponse, error) {
record, sessionID, err := s.repo.FindRefreshToken(ctx, hashToken(refreshToken))
if err != nil {
return LoginResponse{}, ErrInvalidCredentials
}
access, err := SignAccessToken(s.jwtSecret, s.jwtIssuer, s.accessTokenTTL, Claims{
UserID: record.ID,
Username: record.Username,
Role: record.Role,
Session: sessionID,
})
if err != nil {
return LoginResponse{}, err
}
return LoginResponse{
AccessToken: access,
RefreshToken: refreshToken,
ExpiresIn: int64(s.accessTokenTTL.Seconds()),
User: UserView{
ID: record.ID,
Username: record.Username,
DisplayName: record.DisplayName,
Role: record.Role,
},
}, nil
}
func (s *Service) Logout(ctx context.Context, refreshToken string) error {
return s.repo.RevokeRefreshToken(ctx, hashToken(refreshToken))
}
func (s *Service) BootstrapAdmin(ctx context.Context, username, displayName, password string) (UserView, error) {
hasUsers, err := s.repo.HasUsers(ctx)
if err != nil {
return UserView{}, err
}
if hasUsers {
return UserView{}, errors.New("bootstrap already completed")
}
passwordHash, err := HashPassword(password)
if err != nil {
return UserView{}, err
}
record, err := s.repo.CreateBootstrapAdmin(ctx, username, displayName, passwordHash)
if err != nil {
return UserView{}, err
}
return UserView{
ID: record.ID,
Username: record.Username,
DisplayName: record.DisplayName,
Role: record.Role,
}, nil
}
func hashToken(plain string) string {
return base64Hash(plain)
}