* feat: add the ui for configuring the third-party standard OAuth2.0/OIDC. - update SystemSetting.js - add setup ui - add configuration * feat: add the ui for "allow the OAuth 2.0 to login" - update SystemSetting.js * feat: add OAuth 2.0 web ui and its process functions - update common.js - update AuthLogin.js - update config.js * fix: missing "Userinfo" endpoint configuration entry, used by OAuth clients to request user information from the IdP. - update config.js - update SystemSetting.js * feat: updated the icons for Lark and OIDC to match the style of the icons for WeChat, EMail, GitHub. - update lark.svg - new oidc.svg * refactor: Changing OAuth 2.0 to OIDC * feat: add OIDC login method * feat: Add support for OIDC login to the backend * fix: Change the AppId and AppSecret on the Web UI to the standard usage: ClientId, ClientSecret. * feat: Support quick configuration of OIDC through Well-Known Discovery Endpoint * feat: Standardize terminology, add well-known configuration - Change the AppId and AppSecret on the Server End to the standard usage: ClientId, ClientSecret. - add Well-Known configuration to store in database, no actual use in server end but store and display in web ui only
214 lines
6.1 KiB
Go
214 lines
6.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/songquanpeng/one-api/common"
|
|
"github.com/songquanpeng/one-api/common/config"
|
|
"github.com/songquanpeng/one-api/common/message"
|
|
"github.com/songquanpeng/one-api/model"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func GetStatus(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": gin.H{
|
|
"version": common.Version,
|
|
"start_time": common.StartTime,
|
|
"email_verification": config.EmailVerificationEnabled,
|
|
"github_oauth": config.GitHubOAuthEnabled,
|
|
"github_client_id": config.GitHubClientId,
|
|
"lark_client_id": config.LarkClientId,
|
|
"system_name": config.SystemName,
|
|
"logo": config.Logo,
|
|
"footer_html": config.Footer,
|
|
"wechat_qrcode": config.WeChatAccountQRCodeImageURL,
|
|
"wechat_login": config.WeChatAuthEnabled,
|
|
"server_address": config.ServerAddress,
|
|
"turnstile_check": config.TurnstileCheckEnabled,
|
|
"turnstile_site_key": config.TurnstileSiteKey,
|
|
"top_up_link": config.TopUpLink,
|
|
"chat_link": config.ChatLink,
|
|
"quota_per_unit": config.QuotaPerUnit,
|
|
"display_in_currency": config.DisplayInCurrencyEnabled,
|
|
"oidc": config.OidcEnabled,
|
|
"oidc_client_id": config.OidcClientId,
|
|
"oidc_well_known": config.OidcWellKnown,
|
|
"oidc_authorization_endpoint": config.OidcAuthorizationEndpoint,
|
|
"oidc_token_endpoint": config.OidcTokenEndpoint,
|
|
"oidc_userinfo_endpoint": config.OidcUserinfoEndpoint,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
func GetNotice(c *gin.Context) {
|
|
config.OptionMapRWMutex.RLock()
|
|
defer config.OptionMapRWMutex.RUnlock()
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": config.OptionMap["Notice"],
|
|
})
|
|
return
|
|
}
|
|
|
|
func GetAbout(c *gin.Context) {
|
|
config.OptionMapRWMutex.RLock()
|
|
defer config.OptionMapRWMutex.RUnlock()
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": config.OptionMap["About"],
|
|
})
|
|
return
|
|
}
|
|
|
|
func GetHomePageContent(c *gin.Context) {
|
|
config.OptionMapRWMutex.RLock()
|
|
defer config.OptionMapRWMutex.RUnlock()
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": config.OptionMap["HomePageContent"],
|
|
})
|
|
return
|
|
}
|
|
|
|
func SendEmailVerification(c *gin.Context) {
|
|
email := c.Query("email")
|
|
if err := common.Validate.Var(email, "required,email"); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "无效的参数",
|
|
})
|
|
return
|
|
}
|
|
if config.EmailDomainRestrictionEnabled {
|
|
allowed := false
|
|
for _, domain := range config.EmailDomainWhitelist {
|
|
if strings.HasSuffix(email, "@"+domain) {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
if !allowed {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "管理员启用了邮箱域名白名单,您的邮箱地址的域名不在白名单中",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
if model.IsEmailAlreadyTaken(email) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "邮箱地址已被占用",
|
|
})
|
|
return
|
|
}
|
|
code := common.GenerateVerificationCode(6)
|
|
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
|
|
subject := fmt.Sprintf("%s邮箱验证邮件", config.SystemName)
|
|
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
|
"<p>您的验证码为: <strong>%s</strong></p>"+
|
|
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", config.SystemName, code, common.VerificationValidMinutes)
|
|
err := message.SendEmail(subject, email, content)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
func SendPasswordResetEmail(c *gin.Context) {
|
|
email := c.Query("email")
|
|
if err := common.Validate.Var(email, "required,email"); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "无效的参数",
|
|
})
|
|
return
|
|
}
|
|
if !model.IsEmailAlreadyTaken(email) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "该邮箱地址未注册",
|
|
})
|
|
return
|
|
}
|
|
code := common.GenerateVerificationCode(0)
|
|
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
|
|
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", config.ServerAddress, email, code)
|
|
subject := fmt.Sprintf("%s密码重置", config.SystemName)
|
|
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
|
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
|
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
|
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", config.SystemName, link, link, common.VerificationValidMinutes)
|
|
err := message.SendEmail(subject, email, content)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
type PasswordResetRequest struct {
|
|
Email string `json:"email"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
func ResetPassword(c *gin.Context) {
|
|
var req PasswordResetRequest
|
|
err := json.NewDecoder(c.Request.Body).Decode(&req)
|
|
if req.Email == "" || req.Token == "" {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "无效的参数",
|
|
})
|
|
return
|
|
}
|
|
if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "重置链接非法或已过期",
|
|
})
|
|
return
|
|
}
|
|
password := common.GenerateVerificationCode(12)
|
|
err = model.ResetUserPasswordByEmail(req.Email, password)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
common.DeleteKey(req.Email, common.PasswordResetPurpose)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": password,
|
|
})
|
|
return
|
|
}
|