Files
OpenFlare/backend/plugins/domain/auth/service/auth_service.go
T

279 lines
8.2 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package service implements domain business services and orchestration for the auth plugin.
package service
import (
"Wavelet/core/contracts"
"Wavelet/plugins/domain/auth/consts"
"Wavelet/plugins/domain/auth/dao"
"Wavelet/plugins/domain/auth/model/entity"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
)
// HashToken computes SHA-256 hex digest of access token.
func HashToken(token string) string {
h := sha256.New()
h.Write([]byte(token))
return hex.EncodeToString(h.Sum(nil))
}
// UserIDExtractor extracts user ID from a request context.
type UserIDExtractor func(ctx context.Context) (uint64, bool)
// AuthServiceImpl implements contracts.AuthService.
type AuthServiceImpl struct {
dao *dao.DAO
requireAuthMiddleware any
requireAdminMiddleware any
disallowTokenMiddleware any
userIDExtractor UserIDExtractor
}
// NewAuthService creates a new AuthServiceImpl.
func NewAuthService(
d *dao.DAO,
requireAuth any,
requireAdmin any,
disallowToken any,
extractor UserIDExtractor,
) *AuthServiceImpl {
return &AuthServiceImpl{
dao: d,
requireAuthMiddleware: requireAuth,
requireAdminMiddleware: requireAdmin,
disallowTokenMiddleware: disallowToken,
userIDExtractor: extractor,
}
}
// SetMiddlewareHandlers wires middleware handlers into AuthService after controller initialization.
func (s *AuthServiceImpl) SetMiddlewareHandlers(requireAuth, requireAdmin, disallowToken any, extractor UserIDExtractor) {
s.requireAuthMiddleware = requireAuth
s.requireAdminMiddleware = requireAdmin
s.disallowTokenMiddleware = disallowToken
s.userIDExtractor = extractor
}
// RequireAuthMiddleware returns the authentication check middleware.
func (s *AuthServiceImpl) RequireAuthMiddleware() any {
return s.requireAuthMiddleware
}
// RequireAdminMiddleware returns the admin authorization middleware.
func (s *AuthServiceImpl) RequireAdminMiddleware() any {
return s.requireAdminMiddleware
}
// DisallowTokenAuthMiddleware returns the token rejection middleware.
func (s *AuthServiceImpl) DisallowTokenAuthMiddleware() any {
return s.disallowTokenMiddleware
}
// GetCurrentUser 从 context 中读取登录用户。
func (s *AuthServiceImpl) GetCurrentUser(ctx context.Context) (*contracts.UserDTO, error) {
if v := ctx.Value(contracts.AuthUserObjKey); v != nil {
if u, ok := v.(*contracts.UserDTO); ok && u != nil {
return u, nil
}
}
return nil, errors.New(consts.ErrUserNotInContext)
}
// GetCurrentUserID 从请求登录态中读取用户 ID。
func (s *AuthServiceImpl) GetCurrentUserID(ctx context.Context) (uint64, error) {
if s.userIDExtractor != nil {
if userID, ok := s.userIDExtractor(ctx); ok {
return userID, nil
}
}
return 0, errors.New(consts.ErrUserNotInContext)
}
// VerifyToken 验证访问令牌并返回对应的用户。
func (s *AuthServiceImpl) VerifyToken(ctx context.Context, token string) (*contracts.UserDTO, error) {
if token == "" {
return nil, errors.New(consts.ErrEmptyToken)
}
tokenHash := HashToken(token)
tokenRecord, err := s.dao.GetCachedToken(ctx, tokenHash)
if err != nil {
tokenRecord, err = s.dao.GetAccessTokenByHash(ctx, tokenHash)
if err != nil {
return nil, err
}
s.dao.SetCachedToken(ctx, tokenHash, tokenRecord)
}
user, err := s.dao.GetCachedUser(ctx, tokenRecord.UserID)
if err != nil || user == nil || !user.IsActive {
user, err = s.dao.GetActiveUserByID(ctx, tokenRecord.UserID)
if err != nil {
return nil, err
}
s.dao.SetCachedUser(ctx, tokenRecord.UserID, user)
}
if user.Username == consts.SystemUsername {
return nil, errors.New(consts.ErrSystemUserTokenNotAllowed)
}
return user, nil
}
// CreateSession establishes an authenticated session.
func (s *AuthServiceImpl) CreateSession(_ context.Context, _ uint64, _ map[string]any) (string, error) {
return "", nil
}
// RevokeUserSessions revokes active sessions and cached tokens for a user.
func (s *AuthServiceImpl) RevokeUserSessions(ctx context.Context, userID uint64) error {
s.dao.InvalidateCachedUser(ctx, userID)
return nil
}
// RevokeToken invalidates a cached token by its hash.
func (s *AuthServiceImpl) RevokeToken(ctx context.Context, tokenHash string) error {
s.dao.InvalidateCachedToken(ctx, tokenHash)
return nil
}
// InvalidateCachedUser invalidates cached user profile.
func (s *AuthServiceImpl) InvalidateCachedUser(ctx context.Context, userID uint64) {
s.dao.InvalidateCachedUser(ctx, userID)
}
// InvalidateCachedToken invalidates cached access token.
func (s *AuthServiceImpl) InvalidateCachedToken(ctx context.Context, tokenHash string) {
s.dao.InvalidateCachedToken(ctx, tokenHash)
}
// ListAuthSources lists all configured authentication sources.
func (s *AuthServiceImpl) ListAuthSources(ctx context.Context) ([]contracts.AuthSourceViewDTO, error) {
sources, err := s.dao.ListAllAuthSources(ctx)
if err != nil {
return nil, err
}
views := make([]contracts.AuthSourceViewDTO, len(sources))
for i := range sources {
views[i] = contracts.AuthSourceViewDTO{
ID: sources[i].ID,
Name: sources[i].Name,
Type: sources[i].Type,
DisplayName: sources[i].DisplayName,
IsActive: sources[i].IsActive,
IconURL: sources[i].IconURL,
ClientSecretConfigured: sources[i].ClientSecret != "",
}
}
return views, nil
}
// CreateAuthSource creates a new authentication source.
func (s *AuthServiceImpl) CreateAuthSource(ctx context.Context, source contracts.AuthSourceDTO) (*contracts.AuthSourceDTO, error) {
model := entity.AuthSource{
ID: source.ID,
Name: source.Name,
Type: source.Type,
DisplayName: source.DisplayName,
ClientID: source.ClientID,
ClientSecret: source.ClientSecret,
OpenIDDiscoveryURL: source.OpenIDDiscoveryURL,
Scopes: source.Scopes,
IconURL: source.IconURL,
IsActive: source.IsActive,
}
if err := model.Validate(); err != nil {
return nil, err
}
if err := s.dao.CreateAuthSource(ctx, &model); err != nil {
return nil, err
}
model.Sanitize()
return toAuthSourceDTO(&model), nil
}
// UpdateAuthSource updates an existing authentication source.
func (s *AuthServiceImpl) UpdateAuthSource(ctx context.Context, id uint64, source contracts.AuthSourceDTO) (*contracts.AuthSourceDTO, error) {
existing, err := s.dao.GetAuthSourceByID(ctx, id)
if err != nil {
return nil, err
}
existing.DisplayName = source.DisplayName
existing.ClientID = source.ClientID
if source.ClientSecret != "" {
existing.ClientSecret = source.ClientSecret
}
existing.OpenIDDiscoveryURL = source.OpenIDDiscoveryURL
existing.Scopes = source.Scopes
existing.IconURL = source.IconURL
if err := existing.Validate(); err != nil {
return nil, err
}
if err := s.dao.SaveAuthSource(ctx, existing); err != nil {
return nil, err
}
existing.Sanitize()
return toAuthSourceDTO(existing), nil
}
// DeleteAuthSource deletes an authentication source.
func (s *AuthServiceImpl) DeleteAuthSource(ctx context.Context, id uint64) error {
existing, err := s.dao.GetAuthSourceByID(ctx, id)
if err != nil {
return err
}
return s.dao.DeleteAuthSource(ctx, existing)
}
// ToggleAuthSource toggles active status of an authentication source.
func (s *AuthServiceImpl) ToggleAuthSource(ctx context.Context, id uint64) (*contracts.AuthSourceDTO, error) {
existing, err := s.dao.GetAuthSourceByID(ctx, id)
if err != nil {
return nil, err
}
existing.IsActive = !existing.IsActive
if err := s.dao.SaveAuthSource(ctx, existing); err != nil {
return nil, err
}
existing.Sanitize()
return toAuthSourceDTO(existing), nil
}
func toAuthSourceDTO(s *entity.AuthSource) *contracts.AuthSourceDTO {
if s == nil {
return nil
}
return &contracts.AuthSourceDTO{
ID: s.ID,
Name: s.Name,
Type: s.Type,
DisplayName: s.DisplayName,
ClientID: s.ClientID,
ClientSecret: s.ClientSecret,
OpenIDDiscoveryURL: s.OpenIDDiscoveryURL,
Scopes: s.Scopes,
IconURL: s.IconURL,
IsActive: s.IsActive,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}