Files
OpenFlare/Wavelet/internal/apps/openflare/auth/logics.go
T
2026-06-18 16:08:48 +08:00

802 lines
24 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package auth
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"math/big"
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
"github.com/Rain-kl/Wavelet/internal/buildinfo"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/db/idgen"
"github.com/Rain-kl/Wavelet/internal/listener"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/task"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
const (
legacyTokenName = "openflare-legacy"
minPasswordLength = 8
legacyItemsPerPage = 10
passwordResetKeyFmt = "of_password_reset:%s"
passwordResetExpiry = 15 * time.Minute
verificationCodeRange = 900000
verificationOffset = 100000
)
// LoginInput holds legacy login credentials.
type LoginInput struct {
Username string
Password string
Code string
}
// RegisterInput holds legacy registration fields.
type RegisterInput struct {
Username string
Password string
Nickname string
DisplayName string
Email string
Code string
}
// ManageUserInput holds legacy user management actions.
type ManageUserInput struct {
Username string
Action string
}
// UpdateUserInput holds legacy admin user update fields.
type UpdateUserInput struct {
ID int
Username string
Password string
DisplayName string
Role int
Email string
}
// UpdateSelfInput holds legacy self-update fields.
type UpdateSelfInput struct {
Username string
Password string
DisplayName string
Email string
}
// CreateUserInput holds legacy admin create-user fields.
type CreateUserInput struct {
Username string
Password string
DisplayName string
Role int
Email string
}
// PasswordResetInput holds password reset confirmation.
type PasswordResetInput struct {
Email string
Token string
}
// LinkExistingInput binds a pending OAuth account to an existing user.
type LinkExistingInput struct {
Username string
Password string
}
// Login authenticates a user and returns the legacy user shape with an access token.
func Login(ctx context.Context, c *gin.Context, input LoginInput) (LegacyUser, error) {
if !isPasswordLoginEnabled(ctx) {
return LegacyUser{}, errors.New(errPasswordLoginDisabled)
}
input.Username = strings.TrimSpace(input.Username)
if input.Username == "" || input.Password == "" {
return LegacyUser{}, errors.New(errInvalidParams)
}
var user model.User
if err := db.DB(ctx).Where("username = ? OR email = ?", input.Username, input.Username).First(&user).Error; err != nil {
logger.WarnF(ctx, "[LoginAudit] failed login attempt (username not found) for input: %s, IP: %s", input.Username, c.ClientIP())
return LegacyUser{}, errors.New(errUsernameOrPasswordWrong)
}
if !user.IsActive {
return LegacyUser{}, errors.New(errBannedAccount)
}
if !user.CheckPassword(input.Password) {
logger.WarnF(ctx, "[LoginAudit] failed login attempt (incorrect password) for username: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
return LegacyUser{}, errors.New(errUsernameOrPasswordWrong)
}
if isEmailLoginVerificationEnabled(ctx) {
if err := verifyLoginEmailCode(ctx, user.Email, input.Code); err != nil {
return LegacyUser{}, err
}
}
user.LastLoginAt = time.Now()
if err := db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error; err != nil {
return LegacyUser{}, err
}
if err := setLoginSession(ctx, c, &user); err != nil {
return LegacyUser{}, errors.New(errSaveSessionFailed)
}
token, err := issueLegacyAccessToken(ctx, &user)
if err != nil {
return LegacyUser{}, err
}
logger.InfoF(ctx, "[LoginAudit] successful legacy login for user: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
listener.EmitAdminLoggedIn(ctx, &user, c.ClientIP())
return ToLegacyUser(&user, token), nil
}
// Register creates a new user and logs them in.
func Register(ctx context.Context, c *gin.Context, input RegisterInput) (LegacyUser, error) {
if !isRegistrationEnabled(ctx) || !isPasswordRegisterEnabled(ctx) {
return LegacyUser{}, errors.New(errRegistrationDisabled)
}
input.Username = strings.TrimSpace(input.Username)
input.Password = strings.TrimSpace(input.Password)
input.Nickname = strings.TrimSpace(input.Nickname)
input.DisplayName = strings.TrimSpace(input.DisplayName)
input.Email = strings.TrimSpace(input.Email)
input.Code = strings.TrimSpace(input.Code)
if input.Username == "" || input.Password == "" {
return LegacyUser{}, errors.New(errInvalidParams)
}
if len(input.Password) < minPasswordLength {
return LegacyUser{}, errors.New(errPasswordTooShort)
}
if input.Email == "" {
return LegacyUser{}, errors.New(errEmailRequired)
}
if isEmailRegisterVerificationEnabled(ctx) {
if input.Code == "" || !verifyEmailCode(ctx, input.Email, "register", input.Code) {
return LegacyUser{}, errors.New(errEmailCodeInvalid)
}
}
user := model.User{
ID: idgen.NextUint64ID(),
Username: input.Username,
Nickname: input.Nickname,
Email: input.Email,
IsActive: true,
IsAdmin: false,
LastLoginAt: time.Now(),
}
if user.Nickname == "" {
user.Nickname = input.DisplayName
}
if user.Nickname == "" {
user.Nickname = input.Username
}
if err := user.SetEncryptedPassword(input.Password); err != nil {
return LegacyUser{}, err
}
if err := user.RegisterUser(ctx, db.DB(ctx)); err != nil {
return LegacyUser{}, err
}
if err := setLoginSession(ctx, c, &user); err != nil {
return LegacyUser{}, errors.New(errSaveSessionFailed)
}
token, err := issueLegacyAccessToken(ctx, &user)
if err != nil {
return LegacyUser{}, err
}
return ToLegacyUser(&user, token), nil
}
// Logout clears session and revokes the legacy access token when provided.
func Logout(ctx context.Context, c *gin.Context) error {
token := strings.TrimSpace(c.GetHeader(compat.OpenFlareTokenHeader()))
if token == "" {
token = strings.TrimSpace(c.GetHeader("X-Access-Token"))
}
if token != "" {
tokenHash := model.HashToken(token)
_ = db.DB(ctx).Where("token_hash = ? AND name = ?", tokenHash, legacyTokenName).Delete(&model.AccessToken{}).Error
}
session := sessions.Default(c)
session.Options(oauth.GetSessionOptions(-1))
session.Clear()
return session.Save()
}
// GetSelf returns the current user's legacy profile.
func GetSelf(ctx context.Context, userID uint64) (LegacyUser, error) {
user, err := repository.GetUserByID(ctx, userID)
if err != nil {
return LegacyUser{}, errors.New(errUserNotFound)
}
return ToLegacyUser(&user, ""), nil
}
// GenerateUserToken issues a fresh legacy access token for the user.
func GenerateUserToken(ctx context.Context, userID uint64) (string, error) {
user, err := repository.GetUserByID(ctx, userID)
if err != nil {
return "", errors.New(errUserNotFound)
}
return issueLegacyAccessToken(ctx, &user)
}
// UpdateSelf updates the logged-in user's profile.
func UpdateSelf(ctx context.Context, userID uint64, input UpdateSelfInput) error {
user, err := repository.GetUserByID(ctx, userID)
if err != nil {
return errors.New(errUserNotFound)
}
if input.DisplayName != "" {
user.Nickname = strings.TrimSpace(input.DisplayName)
}
if input.Username != "" {
user.Username = strings.TrimSpace(input.Username)
}
if input.Email != "" {
user.Email = strings.TrimSpace(input.Email)
}
if input.Password != "" {
if len(input.Password) < minPasswordLength {
return errors.New(errPasswordTooShort)
}
if err := user.SetEncryptedPassword(input.Password); err != nil {
return err
}
}
return db.DB(ctx).Save(&user).Error
}
// DeleteSelf removes the logged-in user.
func DeleteSelf(ctx context.Context, userID uint64) error {
return repository.DeleteUserWithRelations(ctx, userID)
}
// ListUsers returns a paginated legacy user list.
func ListUsers(ctx context.Context, page int) ([]LegacyUser, error) {
if page < 0 {
page = 0
}
_, users, err := repository.ListAdminUsers(ctx, repository.AdminUserListFilter{
Page: page + 1,
PageSize: legacyItemsPerPage,
})
if err != nil {
return nil, err
}
return ToLegacyUsers(users), nil
}
// SearchUsers searches users by keyword.
func SearchUsers(ctx context.Context, keyword string) ([]LegacyUser, error) {
keyword = strings.TrimSpace(keyword)
var users []model.User
query := db.DB(ctx).Model(&model.User{}).
Select("id, username, nickname, email, is_active, is_admin")
if keyword != "" {
like := keyword + "%"
query = query.Where(
"CAST(id AS TEXT) = ? OR username LIKE ? OR email LIKE ? OR nickname LIKE ?",
keyword, like, like, like,
)
}
if err := query.Order("id DESC").Find(&users).Error; err != nil {
return nil, err
}
return ToLegacyUsers(users), nil
}
// GetUserByID returns a legacy user if the caller has sufficient role.
func GetUserByID(ctx context.Context, callerRole int, id uint64) (LegacyUser, error) {
user, err := repository.GetAdminUserDetail(ctx, id)
if err != nil {
return LegacyUser{}, errors.New(errUserNotFound)
}
targetRole := RoleFromUser(&user)
if callerRole <= targetRole {
return LegacyUser{}, errors.New(errInsufficientPermission)
}
return ToLegacyUser(&user, ""), nil
}
// CreateUser creates a user from legacy admin input.
func CreateUser(ctx context.Context, callerRole int, input CreateUserInput) error {
input.Username = strings.TrimSpace(input.Username)
input.Password = strings.TrimSpace(input.Password)
input.DisplayName = strings.TrimSpace(input.DisplayName)
input.Email = strings.TrimSpace(input.Email)
if input.Username == "" || input.Password == "" {
return errors.New(errInvalidParams)
}
if input.Role >= callerRole {
return errors.New(errInsufficientPermission)
}
if input.Email == "" {
input.Email = input.Username + "@openflare.local"
}
newUser := model.User{
ID: idgen.NextUint64ID(),
Username: input.Username,
Nickname: input.DisplayName,
Email: input.Email,
IsActive: true,
IsAdmin: input.Role >= compat.RoleAdminUser,
}
if newUser.Nickname == "" {
newUser.Nickname = input.Username
}
if err := newUser.SetEncryptedPassword(input.Password); err != nil {
return err
}
return repository.CreateUser(ctx, &newUser)
}
// UpdateUser updates another user with legacy role checks.
func UpdateUser(ctx context.Context, callerRole int, input UpdateUserInput) error {
if input.ID == 0 {
return errors.New(errInvalidParams)
}
origin, err := repository.GetAdminUserDetail(ctx, uint64(input.ID))
if err != nil {
return errors.New(errUserNotFound)
}
originRole := RoleFromUser(&origin)
if callerRole <= originRole {
return errors.New(errInsufficientPermission)
}
if input.Role > 0 && callerRole <= input.Role {
return errors.New(errInsufficientPermission)
}
if trimmed := strings.TrimSpace(input.Username); trimmed != "" {
origin.Username = trimmed
}
if input.DisplayName != "" {
origin.Nickname = strings.TrimSpace(input.DisplayName)
}
if input.Email != "" {
origin.Email = strings.TrimSpace(input.Email)
}
if input.Role > 0 {
origin.IsAdmin = input.Role >= compat.RoleAdminUser
}
if input.Password != "" {
if len(input.Password) < minPasswordLength {
return errors.New(errPasswordTooShort)
}
if err := origin.SetEncryptedPassword(input.Password); err != nil {
return err
}
}
return db.DB(ctx).Save(&origin).Error
}
// DeleteUserByID deletes a user when the caller has sufficient role.
func DeleteUserByID(ctx context.Context, callerRole int, id uint64) error {
origin, err := repository.GetAdminUserDetail(ctx, id)
if err != nil {
return errors.New(errUserNotFound)
}
if callerRole <= RoleFromUser(&origin) {
return errors.New(errInsufficientPermission)
}
if RoleFromUser(&origin) >= compat.RoleRootUser {
return errors.New(errCannotDeleteRoot)
}
return repository.DeleteUserWithRelations(ctx, id)
}
// ManageUser performs enable/disable/delete/promote/demote actions.
func ManageUser(ctx context.Context, callerRole int, input ManageUserInput) (LegacyUser, error) {
input.Username = strings.TrimSpace(input.Username)
input.Action = strings.TrimSpace(input.Action)
if input.Username == "" || input.Action == "" {
return LegacyUser{}, errors.New(errInvalidParams)
}
user, err := repository.GetUserByUsername(ctx, input.Username)
if err != nil {
return LegacyUser{}, errors.New(errUserNotFound)
}
targetRole := RoleFromUser(&user)
if callerRole <= targetRole && callerRole != compat.RoleRootUser {
return LegacyUser{}, errors.New(errInsufficientPermission)
}
switch input.Action {
case "disable":
if targetRole >= compat.RoleRootUser {
return LegacyUser{}, errors.New(errCannotDisableRoot)
}
user.IsActive = false
case "enable":
user.IsActive = true
case "delete":
if targetRole >= compat.RoleRootUser {
return LegacyUser{}, errors.New(errCannotDeleteRoot)
}
if err := repository.DeleteUserWithRelations(ctx, user.ID); err != nil {
return LegacyUser{}, err
}
return LegacyUser{Role: compat.RoleCommonUser, Status: legacyUserStatusDisabled}, nil
case "promote":
if callerRole != compat.RoleRootUser {
return LegacyUser{}, errors.New(errCannotPromoteAdmin)
}
if user.IsAdmin {
return LegacyUser{}, errors.New(errAlreadyAdmin)
}
user.IsAdmin = true
case "demote":
if targetRole >= compat.RoleRootUser {
return LegacyUser{}, errors.New(errCannotDisableRoot)
}
if !user.IsAdmin {
return LegacyUser{}, errors.New(errAlreadyCommonUser)
}
user.IsAdmin = false
default:
return LegacyUser{}, errors.New(errInvalidParams)
}
if err := db.DB(ctx).Save(&user).Error; err != nil {
return LegacyUser{}, err
}
return LegacyUser{
Role: RoleFromUser(&user),
Status: StatusFromUser(&user),
}, nil
}
// SendRegisterVerificationEmail sends a registration verification code.
func SendRegisterVerificationEmail(ctx context.Context, email string) error {
email = strings.TrimSpace(email)
if email == "" || !strings.Contains(email, "@") {
return errors.New(errInvalidParams)
}
var count int64
if err := db.DB(ctx).Model(&model.User{}).Where("email = ?", email).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New(errEmailAlreadyRegistered)
}
return sendEmailVerificationCode(ctx, email, "register", "register_email")
}
// SendPasswordResetEmail stores a reset token and emails the user.
func SendPasswordResetEmail(ctx context.Context, email string) error {
email = strings.TrimSpace(email)
if email == "" || !strings.Contains(email, "@") {
return errors.New(errInvalidParams)
}
var user model.User
if err := db.DB(ctx).Where("email = ?", email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(errEmailNotRegistered)
}
return err
}
token, err := generateResetToken()
if err != nil {
return err
}
key := fmt.Sprintf(passwordResetKeyFmt, email)
if err := db.SetJSON(ctx, key, token, passwordResetExpiry); err != nil {
return err
}
serverAddr, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress)
base := strings.TrimRight(serverAddr.Value, "/")
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", base, email, token)
body := fmt.Sprintf("<p>您好,你正在进行密码重置。</p><p>点击<a href='%s'>此处</a>进行密码重置。</p>", link)
return dispatchEmail(ctx, email, "密码重置", body)
}
// ResetPassword validates a reset token and returns a new random password.
func ResetPassword(ctx context.Context, input PasswordResetInput) (string, error) {
input.Email = strings.TrimSpace(input.Email)
input.Token = strings.TrimSpace(input.Token)
if input.Email == "" || input.Token == "" {
return "", errors.New(errInvalidParams)
}
key := fmt.Sprintf(passwordResetKeyFmt, input.Email)
var stored string
if err := db.GetJSON(ctx, key, &stored); err != nil || stored != input.Token {
return "", errors.New(errResetLinkInvalid)
}
password, err := generateResetToken()
if err != nil {
return "", err
}
if len(password) < minPasswordLength {
password = password + "Aa1!"
}
var user model.User
if err := db.DB(ctx).Where("email = ?", input.Email).First(&user).Error; err != nil {
return "", errors.New(errEmailNotRegistered)
}
if err := user.SetEncryptedPassword(password); err != nil {
return "", err
}
if err := db.DB(ctx).Model(&user).Update("password", user.Password).Error; err != nil {
return "", err
}
_ = db.Redis.Del(ctx, db.PrefixedKey(key)).Err()
return password, nil
}
// BuildPublicStatus assembles the legacy /api/status payload.
func BuildPublicStatus(ctx context.Context) (map[string]any, error) {
authSources, err := publicAuthSources(ctx, "/api")
if err != nil {
authSources = []map[string]any{}
}
siteName, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeySiteName)
serverAddr, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress)
return map[string]any{
"version": buildVersion(),
"start_time": appStartUnix(),
"email_verification": isEmailRegisterVerificationEnabled(ctx),
"github_oauth": false,
"github_client_id": "",
"system_name": siteName.Value,
"home_page_link": "",
"footer_html": "",
"wechat_qrcode": "",
"wechat_login": false,
"server_address": serverAddr.Value,
"password_register_enabled": isPasswordRegisterEnabled(ctx),
"cap_login_enabled": capLoginEnabled(ctx),
"auth_sources": authSources,
}, nil
}
// GetNotice returns the legacy notice option value.
func GetNotice(ctx context.Context) string {
return getOptionValue(ctx, "notice")
}
// GetAbout returns the legacy about option value.
func GetAbout(ctx context.Context) string {
return getOptionValue(ctx, "about")
}
func issueLegacyAccessToken(ctx context.Context, user *model.User) (string, error) {
tokenStr, err := model.GenerateTokenString()
if err != nil {
return "", errors.New(errGenerateTokenFailed)
}
record := model.AccessToken{
UserID: user.ID,
Name: legacyTokenName,
TokenHash: model.HashToken(tokenStr),
MaskedToken: model.MaskTokenString(tokenStr),
IsAdmin: user.IsAdmin,
}
if err := db.DB(ctx).Create(&record).Error; err != nil {
return "", err
}
return tokenStr, nil
}
func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) error {
session := sessions.Default(c)
session.Set(oauth.UserIDKey, user.ID)
session.Set(oauth.UserNameKey, user.Username)
session.Set(oauth.PasswordHashKey, user.Password)
maxAge := config.Config.App.SessionAge
isSessionCookie := false
ttlHours, err := repository.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours)
if err == nil {
switch {
case ttlHours == -1:
maxAge = 10 * 365 * 24 * 3600
case ttlHours > 0:
maxAge = ttlHours * 3600
case ttlHours == 0:
isSessionCookie = true
}
}
session.Options(oauth.GetSessionOptions(maxAge))
if err := session.Save(); err != nil {
return err
}
if isSessionCookie {
oauth.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
}
return nil
}
func isPasswordLoginEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordLoginEnabled)
return err != nil || enabled
}
func isPasswordRegisterEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordRegisterEnabled)
return err != nil || enabled
}
func isRegistrationEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyRegistrationEnabled)
return err != nil || enabled
}
func isEmailLoginVerificationEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled)
return err == nil && enabled
}
func isEmailRegisterVerificationEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled)
return err == nil && enabled
}
func capLoginEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyCapLoginEnabled)
return err == nil && enabled
}
func verifyLoginEmailCode(ctx context.Context, email, code string) error {
if code == "" {
return errors.New("need_email_code:" + email)
}
if !verifyEmailCode(ctx, email, "login", code) {
return errors.New(errEmailCodeInvalid)
}
return nil
}
func verifyEmailCode(ctx context.Context, email, scene, code string) bool {
key := fmt.Sprintf("email_code:%s:%s", scene, email)
var stored string
if err := db.GetJSON(ctx, key, &stored); err != nil {
return false
}
if stored != code {
return false
}
_ = db.Redis.Del(ctx, db.PrefixedKey(key)).Err()
return true
}
func sendEmailVerificationCode(ctx context.Context, email, scene, templateName string) error {
scHost, errHost := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPHost)
scPort, errPort := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPort)
scUser, errUser := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPUsername)
scPass, errPass := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPassword)
if errHost != nil || errPort != nil || errUser != nil || errPass != nil ||
scHost.Value == "" || scPort.Value == "" || scUser.Value == "" || scPass.Value == "" {
return errors.New("系统 SMTP 邮件服务配置不完整")
}
code, err := generateVerificationCode()
if err != nil {
return err
}
codeKey := fmt.Sprintf("email_code:%s:%s", scene, email)
if err := db.SetJSON(ctx, codeKey, code, 5*time.Minute); err != nil {
return err
}
tmpl, err := repository.GetTemplateByKey(ctx, templateName)
if err != nil {
body := fmt.Sprintf("<p>您的验证码为: <strong>%s</strong></p>", code)
return dispatchEmail(ctx, email, "邮箱验证", body)
}
subject, body, err := tmpl.Render(map[string]any{"Code": code})
if err != nil {
return err
}
return dispatchEmail(ctx, email, subject, body)
}
type sendEmailPayload struct {
To string `json:"to"`
Subject string `json:"subject"`
Body string `json:"body"`
}
func dispatchEmail(ctx context.Context, to, subject, body string) error {
payload := sendEmailPayload{To: to, Subject: subject, Body: body}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return err
}
_, err = task.DispatchTask(ctx, "mail:send", payloadBytes, "system")
return err
}
func generateVerificationCode() (string, error) {
n, err := rand.Int(rand.Reader, big.NewInt(verificationCodeRange))
if err != nil {
return "", err
}
return fmt.Sprintf("%06d", n.Int64()+verificationOffset), nil
}
func generateResetToken() (string, error) {
n, err := rand.Int(rand.Reader, big.NewInt(1<<62))
if err != nil {
return "", err
}
return fmt.Sprintf("%x", n.Int64()), nil
}
func publicAuthSources(ctx context.Context, baseAPIPath string) ([]map[string]any, error) {
sources, err := model.GetActiveAuthSources(ctx)
if err != nil {
return nil, err
}
result := make([]map[string]any, 0, len(sources))
base := strings.TrimRight(baseAPIPath, "/")
for _, source := range sources {
result = append(result, map[string]any{
"id": source.ID,
"name": source.Name,
"type": source.Type,
"display_name": source.DisplayName,
"authorize_url": fmt.Sprintf("%s/oauth/%s/authorize", base, source.Name),
"icon_url": source.IconURL,
})
}
return result, nil
}
func getOptionValue(ctx context.Context, key string) string {
sc, err := repository.GetSystemConfigByKey(ctx, key)
if err != nil {
return ""
}
return sc.Value
}
var appStart = time.Now()
func appStartUnix() int64 {
return appStart.Unix()
}
func buildVersion() string {
if buildinfo.Version != "" {
return buildinfo.Version
}
return "dev"
}