Add backend foundation and config-driven workbench
This commit is contained in:
595
backend/internal/service/auth_service.go
Normal file
595
backend/internal/service/auth_service.go
Normal file
@@ -0,0 +1,595 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/platform/jwtx"
|
||||
"cmr-backend/internal/platform/security"
|
||||
"cmr-backend/internal/platform/wechatmini"
|
||||
"cmr-backend/internal/store/postgres"
|
||||
)
|
||||
|
||||
type AuthSettings struct {
|
||||
AppEnv string
|
||||
RefreshTTL time.Duration
|
||||
SMSCodeTTL time.Duration
|
||||
SMSCodeCooldown time.Duration
|
||||
SMSProvider string
|
||||
DevSMSCode string
|
||||
WechatMini *wechatmini.Client
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
cfg AuthSettings
|
||||
store *postgres.Store
|
||||
jwtManager *jwtx.Manager
|
||||
}
|
||||
|
||||
type SendSMSCodeInput struct {
|
||||
CountryCode string `json:"countryCode"`
|
||||
Mobile string `json:"mobile"`
|
||||
ClientType string `json:"clientType"`
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
Scene string `json:"scene"`
|
||||
}
|
||||
|
||||
type SendSMSCodeResult struct {
|
||||
TTLSeconds int64 `json:"ttlSeconds"`
|
||||
CooldownSeconds int64 `json:"cooldownSeconds"`
|
||||
DevCode *string `json:"devCode,omitempty"`
|
||||
}
|
||||
|
||||
type LoginSMSInput struct {
|
||||
CountryCode string `json:"countryCode"`
|
||||
Mobile string `json:"mobile"`
|
||||
Code string `json:"code"`
|
||||
ClientType string `json:"clientType"`
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
}
|
||||
|
||||
type LoginWechatMiniInput struct {
|
||||
Code string `json:"code"`
|
||||
ClientType string `json:"clientType"`
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
}
|
||||
|
||||
type BindMobileInput struct {
|
||||
UserID string `json:"-"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
Mobile string `json:"mobile"`
|
||||
Code string `json:"code"`
|
||||
ClientType string `json:"clientType"`
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
}
|
||||
|
||||
type RefreshTokenInput struct {
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ClientType string `json:"clientType"`
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
}
|
||||
|
||||
type LogoutInput struct {
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
type AuthUser struct {
|
||||
ID string `json:"id"`
|
||||
PublicID string `json:"publicId"`
|
||||
Status string `json:"status"`
|
||||
Nickname *string `json:"nickname,omitempty"`
|
||||
AvatarURL *string `json:"avatarUrl,omitempty"`
|
||||
}
|
||||
|
||||
type AuthTokens struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
AccessTokenExpiresAt string `json:"accessTokenExpiresAt"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
RefreshTokenExpiresAt string `json:"refreshTokenExpiresAt"`
|
||||
}
|
||||
|
||||
type AuthResult struct {
|
||||
User AuthUser `json:"user"`
|
||||
Tokens AuthTokens `json:"tokens"`
|
||||
NewUser bool `json:"newUser"`
|
||||
}
|
||||
|
||||
func NewAuthService(cfg AuthSettings, store *postgres.Store, jwtManager *jwtx.Manager) *AuthService {
|
||||
return &AuthService{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
jwtManager: jwtManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthService) SendSMSCode(ctx context.Context, input SendSMSCodeInput) (*SendSMSCodeResult, error) {
|
||||
input.CountryCode = normalizeCountryCode(input.CountryCode)
|
||||
input.Mobile = normalizeMobile(input.Mobile)
|
||||
input.Scene = normalizeScene(input.Scene)
|
||||
|
||||
if err := validateClientType(input.ClientType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.Mobile == "" || input.DeviceKey == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "mobile and deviceKey are required")
|
||||
}
|
||||
|
||||
latest, err := s.store.GetLatestSMSCodeMeta(ctx, input.CountryCode, input.Mobile, input.ClientType, input.Scene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if latest != nil && latest.CooldownUntil.After(now) {
|
||||
return nil, apperr.New(http.StatusTooManyRequests, "sms_cooldown", "sms code sent too frequently")
|
||||
}
|
||||
|
||||
code := s.cfg.DevSMSCode
|
||||
if code == "" {
|
||||
code, err = security.GenerateNumericCode(6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
expiresAt := now.Add(s.cfg.SMSCodeTTL)
|
||||
cooldownUntil := now.Add(s.cfg.SMSCodeCooldown)
|
||||
if err := s.store.CreateSMSCode(ctx, postgres.CreateSMSCodeParams{
|
||||
Scene: input.Scene,
|
||||
CountryCode: input.CountryCode,
|
||||
Mobile: input.Mobile,
|
||||
ClientType: input.ClientType,
|
||||
DeviceKey: input.DeviceKey,
|
||||
CodeHash: security.HashText(code),
|
||||
ProviderName: s.cfg.SMSProvider,
|
||||
ProviderDebug: map[string]any{"mode": s.cfg.SMSProvider},
|
||||
ExpiresAt: expiresAt,
|
||||
CooldownUntil: cooldownUntil,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &SendSMSCodeResult{
|
||||
TTLSeconds: int64(s.cfg.SMSCodeTTL.Seconds()),
|
||||
CooldownSeconds: int64(s.cfg.SMSCodeCooldown.Seconds()),
|
||||
}
|
||||
if strings.EqualFold(s.cfg.SMSProvider, "console") || strings.EqualFold(s.cfg.AppEnv, "development") {
|
||||
result.DevCode = &code
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) LoginSMS(ctx context.Context, input LoginSMSInput) (*AuthResult, error) {
|
||||
input.CountryCode = normalizeCountryCode(input.CountryCode)
|
||||
input.Mobile = normalizeMobile(input.Mobile)
|
||||
input.Code = strings.TrimSpace(input.Code)
|
||||
|
||||
if err := validateClientType(input.ClientType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.Mobile == "" || input.DeviceKey == "" || input.Code == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "mobile, code and deviceKey are required")
|
||||
}
|
||||
|
||||
codeRecord, err := s.store.GetLatestValidSMSCode(ctx, input.CountryCode, input.Mobile, input.ClientType, "login")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if codeRecord == nil || codeRecord.CodeHash != security.HashText(input.Code) {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "invalid_sms_code", "invalid sms code")
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
consumed, err := s.store.ConsumeSMSCode(ctx, tx, codeRecord.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !consumed {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "invalid_sms_code", "sms code already used")
|
||||
}
|
||||
|
||||
user, err := s.store.FindUserByMobile(ctx, tx, input.CountryCode, input.Mobile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newUser := false
|
||||
if user == nil {
|
||||
userPublicID, err := security.GeneratePublicID("usr")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err = s.store.CreateUser(ctx, tx, postgres.CreateUserParams{
|
||||
PublicID: userPublicID,
|
||||
Status: "active",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.store.CreateMobileIdentity(ctx, tx, postgres.CreateMobileIdentityParams{
|
||||
UserID: user.ID,
|
||||
CountryCode: input.CountryCode,
|
||||
Mobile: input.Mobile,
|
||||
Provider: "mobile",
|
||||
ProviderSubj: input.CountryCode + ":" + input.Mobile,
|
||||
IdentityType: "mobile",
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newUser = true
|
||||
}
|
||||
|
||||
if err := s.store.TouchUserLogin(ctx, tx, user.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := s.issueAuthResult(ctx, tx, *user, input.ClientType, input.DeviceKey, newUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) Refresh(ctx context.Context, input RefreshTokenInput) (*AuthResult, error) {
|
||||
input.RefreshToken = strings.TrimSpace(input.RefreshToken)
|
||||
if err := validateClientType(input.ClientType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.RefreshToken == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "refreshToken is required")
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
record, err := s.store.GetRefreshTokenForUpdate(ctx, tx, security.HashText(input.RefreshToken))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record == nil || record.IsRevoked || record.ExpiresAt.Before(time.Now().UTC()) {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "invalid_refresh_token", "refresh token is invalid or expired")
|
||||
}
|
||||
|
||||
if input.ClientType != "" && input.ClientType != record.ClientType {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "invalid_refresh_token", "refresh token client mismatch")
|
||||
}
|
||||
if input.DeviceKey != "" && record.DeviceKey != nil && input.DeviceKey != *record.DeviceKey {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "invalid_refresh_token", "refresh token device mismatch")
|
||||
}
|
||||
|
||||
user, err := s.store.GetUserByID(ctx, tx, record.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "invalid_refresh_token", "refresh token user not found")
|
||||
}
|
||||
|
||||
result, refreshTokenID, err := s.issueAuthResultWithRefreshID(ctx, tx, *user, record.ClientType, nullableStringValue(record.DeviceKey), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.store.RotateRefreshToken(ctx, tx, record.ID, refreshTokenID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) LoginWechatMini(ctx context.Context, input LoginWechatMiniInput) (*AuthResult, error) {
|
||||
input.Code = strings.TrimSpace(input.Code)
|
||||
if err := validateClientType(input.ClientType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.ClientType != "wechat" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_client_type", "wechat mini login requires clientType=wechat")
|
||||
}
|
||||
if input.Code == "" || strings.TrimSpace(input.DeviceKey) == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and deviceKey are required")
|
||||
}
|
||||
if s.cfg.WechatMini == nil {
|
||||
return nil, apperr.New(http.StatusNotImplemented, "wechat_not_configured", "wechat mini provider is not configured")
|
||||
}
|
||||
|
||||
session, err := s.cfg.WechatMini.ExchangeCode(ctx, input.Code)
|
||||
if err != nil {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "wechat_login_failed", err.Error())
|
||||
}
|
||||
|
||||
openIDSubject := session.AppID + ":" + session.OpenID
|
||||
unionIDSubject := strings.TrimSpace(session.UnionID)
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
user, err := s.store.FindUserByProviderSubject(ctx, tx, "wechat_mini", openIDSubject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil && unionIDSubject != "" {
|
||||
user, err = s.store.FindUserByProviderSubject(ctx, tx, "wechat_unionid", unionIDSubject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
newUser := false
|
||||
if user == nil {
|
||||
userPublicID, err := security.GeneratePublicID("usr")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err = s.store.CreateUser(ctx, tx, postgres.CreateUserParams{
|
||||
PublicID: userPublicID,
|
||||
Status: "active",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newUser = true
|
||||
}
|
||||
|
||||
profileJSON, err := json.Marshal(map[string]any{
|
||||
"appId": session.AppID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.store.CreateIdentity(ctx, tx, postgres.CreateIdentityParams{
|
||||
UserID: user.ID,
|
||||
IdentityType: "wechat_mini_openid",
|
||||
Provider: "wechat_mini",
|
||||
ProviderSubj: openIDSubject,
|
||||
ProfileJSON: string(profileJSON),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if unionIDSubject != "" {
|
||||
if err := s.store.CreateIdentity(ctx, tx, postgres.CreateIdentityParams{
|
||||
UserID: user.ID,
|
||||
IdentityType: "wechat_unionid",
|
||||
Provider: "wechat_unionid",
|
||||
ProviderSubj: unionIDSubject,
|
||||
ProfileJSON: "{}",
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.store.TouchUserLogin(ctx, tx, user.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := s.issueAuthResult(ctx, tx, *user, input.ClientType, input.DeviceKey, newUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) BindMobile(ctx context.Context, input BindMobileInput) (*AuthResult, error) {
|
||||
input.CountryCode = normalizeCountryCode(input.CountryCode)
|
||||
input.Mobile = normalizeMobile(input.Mobile)
|
||||
input.Code = strings.TrimSpace(input.Code)
|
||||
|
||||
if err := validateClientType(input.ClientType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.UserID == "" || input.Mobile == "" || input.Code == "" || strings.TrimSpace(input.DeviceKey) == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "user, mobile, code and deviceKey are required")
|
||||
}
|
||||
|
||||
codeRecord, err := s.store.GetLatestValidSMSCode(ctx, input.CountryCode, input.Mobile, input.ClientType, "bind_mobile")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if codeRecord == nil || codeRecord.CodeHash != security.HashText(input.Code) {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "invalid_sms_code", "invalid sms code")
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
consumed, err := s.store.ConsumeSMSCode(ctx, tx, codeRecord.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !consumed {
|
||||
return nil, apperr.New(http.StatusUnauthorized, "invalid_sms_code", "sms code already used")
|
||||
}
|
||||
|
||||
currentUser, err := s.store.GetUserByID(ctx, tx, input.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if currentUser == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "user_not_found", "current user not found")
|
||||
}
|
||||
|
||||
mobileUser, err := s.store.FindUserByMobile(ctx, tx, input.CountryCode, input.Mobile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
finalUser := currentUser
|
||||
newlyBound := false
|
||||
|
||||
if mobileUser == nil {
|
||||
if err := s.store.CreateMobileIdentity(ctx, tx, postgres.CreateMobileIdentityParams{
|
||||
UserID: currentUser.ID,
|
||||
CountryCode: input.CountryCode,
|
||||
Mobile: input.Mobile,
|
||||
Provider: "mobile",
|
||||
ProviderSubj: input.CountryCode + ":" + input.Mobile,
|
||||
IdentityType: "mobile",
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newlyBound = true
|
||||
} else if mobileUser.ID != currentUser.ID {
|
||||
if err := s.store.TransferNonMobileIdentities(ctx, tx, currentUser.ID, mobileUser.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.store.RevokeRefreshTokensByUserID(ctx, tx, currentUser.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.store.DeactivateUser(ctx, tx, currentUser.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
finalUser = mobileUser
|
||||
}
|
||||
|
||||
if err := s.store.TouchUserLogin(ctx, tx, finalUser.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := s.issueAuthResult(ctx, tx, *finalUser, input.ClientType, input.DeviceKey, newlyBound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) Logout(ctx context.Context, input LogoutInput) error {
|
||||
if strings.TrimSpace(input.RefreshToken) == "" {
|
||||
return nil
|
||||
}
|
||||
return s.store.RevokeRefreshToken(ctx, security.HashText(strings.TrimSpace(input.RefreshToken)))
|
||||
}
|
||||
|
||||
func (s *AuthService) issueAuthResult(
|
||||
ctx context.Context,
|
||||
tx postgres.Tx,
|
||||
user postgres.User,
|
||||
clientType string,
|
||||
deviceKey string,
|
||||
newUser bool,
|
||||
) (*AuthResult, error) {
|
||||
result, _, err := s.issueAuthResultWithRefreshID(ctx, tx, user, clientType, deviceKey, newUser)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *AuthService) issueAuthResultWithRefreshID(
|
||||
ctx context.Context,
|
||||
tx postgres.Tx,
|
||||
user postgres.User,
|
||||
clientType string,
|
||||
deviceKey string,
|
||||
newUser bool,
|
||||
) (*AuthResult, string, error) {
|
||||
accessToken, accessExpiresAt, err := s.jwtManager.IssueAccessToken(user.ID, user.PublicID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
refreshToken, err := security.GenerateToken(32)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
refreshTokenHash := security.HashText(refreshToken)
|
||||
refreshExpiresAt := time.Now().UTC().Add(s.cfg.RefreshTTL)
|
||||
|
||||
refreshID, err := s.store.CreateRefreshToken(ctx, tx, postgres.CreateRefreshTokenParams{
|
||||
UserID: user.ID,
|
||||
ClientType: clientType,
|
||||
DeviceKey: deviceKey,
|
||||
TokenHash: refreshTokenHash,
|
||||
ExpiresAt: refreshExpiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return &AuthResult{
|
||||
User: AuthUser{
|
||||
ID: user.ID,
|
||||
PublicID: user.PublicID,
|
||||
Status: user.Status,
|
||||
Nickname: user.Nickname,
|
||||
AvatarURL: user.AvatarURL,
|
||||
},
|
||||
Tokens: AuthTokens{
|
||||
AccessToken: accessToken,
|
||||
AccessTokenExpiresAt: accessExpiresAt.Format(time.RFC3339),
|
||||
RefreshToken: refreshToken,
|
||||
RefreshTokenExpiresAt: refreshExpiresAt.Format(time.RFC3339),
|
||||
},
|
||||
NewUser: newUser,
|
||||
}, refreshID, nil
|
||||
}
|
||||
|
||||
func validateClientType(clientType string) error {
|
||||
switch clientType {
|
||||
case "app", "wechat":
|
||||
return nil
|
||||
default:
|
||||
return apperr.New(http.StatusBadRequest, "invalid_client_type", "clientType must be app or wechat")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCountryCode(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "86"
|
||||
}
|
||||
return strings.TrimPrefix(value, "+")
|
||||
}
|
||||
|
||||
func normalizeMobile(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.ReplaceAll(value, " ", "")
|
||||
value = strings.ReplaceAll(value, "-", "")
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeScene(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "login"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func nullableStringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
Reference in New Issue
Block a user