Compare commits

...

11 Commits

Author SHA1 Message Date
ljw
855beb7fa9 up oauth 2024-10-31 14:03:48 +08:00
f57816b1b0 Merge pull request #36 from IamTaoChen/oidc-for-web
OIDC for web
2024-10-31 11:10:46 +08:00
Tao Chen
ff08fefc30 rename build stage 2024-10-31 09:21:43 +08:00
Tao Chen
f792ab9055 add some /admin/ to surport web OIDC 2024-10-31 09:21:30 +08:00
ljw
63af103a4e fix buidconfirm 2024-10-30 20:59:51 +08:00
ljw
0a36d44cec up del user 2024-10-30 19:34:56 +08:00
a1f4e1de84 Merge pull request #32 from IamTaoChen/bug/odic-user
delete user from user_thirds and update README
2024-10-30 19:08:50 +08:00
Tao Chen
05b20d47db modify delete user 2024-10-30 16:33:01 +08:00
Tao Chen
6b746f13d1 update README 2024-10-30 16:31:47 +08:00
Tao Chen
e838d5bcd2 update README for OIDC 2024-10-30 16:29:49 +08:00
Tao Chen
0dcc21260e delete user from user_thirds, too 2024-10-30 15:59:33 +08:00
14 changed files with 540 additions and 209 deletions

View File

@@ -4,7 +4,7 @@ ARG BUILDARCH=amd64
# Stage 1: Builder Stage # Stage 1: Builder Stage
# FROM golang:${GO_VERSION}-alpine AS builder # FROM golang:${GO_VERSION}-alpine AS builder
FROM crazymax/xgo:${GO_VERSION} AS builder FROM crazymax/xgo:${GO_VERSION} AS builder-backend
# Set up working directory # Set up working directory
WORKDIR /app WORKDIR /app
@@ -27,7 +27,7 @@ RUN CGO_ENABLED=1 GOOS=linux go build -a \
-installsuffix cgo -o release/apimain cmd/apimain.go -installsuffix cgo -o release/apimain cmd/apimain.go
# Stage 2: Frontend Build Stage (builder2) # Stage 2: Frontend Build Stage (builder2)
FROM node:18-alpine AS builder2 FROM node:18-alpine AS builder-admin-frontend
# Set working directory # Set working directory
WORKDIR /frontend WORKDIR /frontend
@@ -50,12 +50,12 @@ WORKDIR /app
RUN apk add --no-cache tzdata file RUN apk add --no-cache tzdata file
# Copy the built application and resources from the builder stage # Copy the built application and resources from the builder stage
COPY --from=builder /app/release /app/ COPY --from=builder-backend /app/release /app/
COPY --from=builder /app/conf /app/conf/ COPY --from=builder-backend /app/conf /app/conf/
COPY --from=builder /app/resources /app/resources/ COPY --from=builder-backend /app/resources /app/resources/
COPY --from=builder /app/docs /app/docs/ COPY --from=builder-backend /app/docs /app/docs/
# Copy frontend build from builder2 stage # Copy frontend build from builder2 stage
COPY --from=builder2 /frontend/dist/ /app/resources/admin/ COPY --from=builder-admin-frontend /frontend/dist/ /app/resources/admin/
# Ensure the binary is correctly built and linked # Ensure the binary is correctly built and linked
RUN file /app/apimain && \ RUN file /app/apimain && \

View File

@@ -19,7 +19,7 @@
- 登录 - 登录
- 地址簿 - 地址簿
- 群组 - 群组
- 授权登录,支持`github``google`登录,支持`web后台`授权登录 - 授权登录,支持`github`, `google``OIDC` 登录,支持`web后台`授权登录
- i18n - i18n
- Web Admin - Web Admin
- 用户管理 - 用户管理
@@ -92,7 +92,7 @@
#### 登录 #### 登录
- 添加了`github``google`授权登录需要在后台配置好就可以用了具体可看后台OAuth配置 - 添加了`github`, `google` 以及`OIDC`授权登录需要在后台配置好就可以用了具体可看后台OAuth配置
- 添加了web后台授权登录,点击后直接登录后台就自动登录客户端了 - 添加了web后台授权登录,点击后直接登录后台就自动登录客户端了
![pc_login](docs/pc_login.png) ![pc_login](docs/pc_login.png)
@@ -124,8 +124,10 @@
4. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备 4. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备
![web_webclient](docs/admin_webclient.png) ![web_webclient](docs/admin_webclient.png)
5. Oauth,暂时只支持了`Github``Google`, 需要创建一个`OAuth App`,然后配置到后台 5. Oauth,支持了`Github`, `Google` 以及 `OIDC`, 需要创建一个`OAuth App`,然后配置到后台
![web_admin_oauth](docs/web_admin_oauth.png) ![web_admin_oauth](docs/web_admin_oauth.png)
- 对于`Google` 和 `Github`, `Issuer` 和 `Scopes`不需要填写.
- 对于`OIDC`, `Issuer`是必须的。`Scopes`是可选的,默认为 `openid,profile,email`. 确保可以获取 `sub`,`email` 和`preferred_username`
- `github oauth app`在`Settings`->`Developer settings`->`OAuth Apps`->`New OAuth App` - `github oauth app`在`Settings`->`Developer settings`->`OAuth Apps`->`New OAuth App`
中创建,地址 [https://github.com/settings/developers](https://github.com/settings/developers) 中创建,地址 [https://github.com/settings/developers](https://github.com/settings/developers)
- `Authorization callback URL`填写`http://<your server[:port]>/api/oauth/callback` - `Authorization callback URL`填写`http://<your server[:port]>/api/oauth/callback`

View File

@@ -18,7 +18,7 @@ desktop software that provides self-hosted solutions.
- Login - Login
- Address Book - Address Book
- Groups - Groups
- Authorized login, supports `GitHub` and `Google` login, supports `web admin` authorized login - Authorized login, supports `GitHub`, `Google` and `OIDC` login, supports `web admin` authorized login
- i18n - i18n
- Web Admin - Web Admin
- User Management - User Management
@@ -93,7 +93,7 @@ Basic implementation of the PC client's primary interfaces.Supports the Personal
#### Login #### Login
- Added `GitHub` and `Google` login, which can be used after configuration in the admin panel. See the OAuth - Added `GitHub`, `Google` and `OIDC` login, which can be used after configuration in the admin panel. See the OAuth
configuration section for details. configuration section for details.
- Added authorization login for the web admin panel. - Added authorization login for the web admin panel.
@@ -128,9 +128,11 @@ installation are `admin` `admin`, please change the password immediately.
4. You can directly launch the client or open the web client for convenience; you can also share it with guests, who can remotely access the device via the web client. 4. You can directly launch the client or open the web client for convenience; you can also share it with guests, who can remotely access the device via the web client.
![web_webclient](docs/en_img/admin_webclient.png) ![web_webclient](docs/en_img/admin_webclient.png)
5. OAuth support: Currently, `GitHub` and `Google` is supported. You need to create an `OAuth App` and configure it in 5. OAuth support: Currently, `GitHub`, `Google` and `OIDC` are supported. You need to create an `OAuth App` and configure it in
the admin panel. the admin panel.
![web_admin_oauth](docs/en_img/web_admin_oauth.png) ![web_admin_oauth](docs/en_img/web_admin_oauth.png)
- For `Google` and `Github`, you don't need to fill the `Issuer` and `Scpoes`
- For `OIDC`, you must set the `Issuer`. And `Scopes` is optional which default is `openid,email,profile`, please make sure this `Oauth App` can access `sub`, `email` and `preferred_username`
- Create a `GitHub OAuth App` - Create a `GitHub OAuth App`
at `Settings` -> `Developer settings` -> `OAuth Apps` -> `New OAuth App` [here](https://github.com/settings/developers). at `Settings` -> `Developer settings` -> `OAuth Apps` -> `New OAuth App` [here](https://github.com/settings/developers).
- Set the `Authorization callback URL` to `http://<your server[:port]>/api/oauth/callback`, - Set the `Authorization callback URL` to `http://<your server[:port]>/api/oauth/callback`,

View File

@@ -1453,6 +1453,38 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/login-options": {
"post": {
"description": "登录选项",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录"
],
"summary": "登录选项",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/admin/loginLog/delete": { "/admin/loginLog/delete": {
"post": { "post": {
"security": [ "security": [
@@ -1922,6 +1954,63 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/oidc/auth": {
"post": {
"description": "OidcAuth",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Oauth"
],
"summary": "OidcAuth",
"responses": {}
}
},
"/admin/oidc/auth-query": {
"get": {
"description": "OidcAuthQuery",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Oauth"
],
"summary": "OidcAuthQuery",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/admin.LoginPayload"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/peer/create": { "/admin/peer/create": {
"post": { "post": {
"security": [ "security": [
@@ -3979,6 +4068,14 @@ const docTemplateadmin = `{
} }
} }
}, },
"response.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"response.Response": { "response.Response": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -1446,6 +1446,38 @@
} }
} }
}, },
"/admin/login-options": {
"post": {
"description": "登录选项",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录"
],
"summary": "登录选项",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/admin/loginLog/delete": { "/admin/loginLog/delete": {
"post": { "post": {
"security": [ "security": [
@@ -1915,6 +1947,63 @@
} }
} }
}, },
"/admin/oidc/auth": {
"post": {
"description": "OidcAuth",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Oauth"
],
"summary": "OidcAuth",
"responses": {}
}
},
"/admin/oidc/auth-query": {
"get": {
"description": "OidcAuthQuery",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Oauth"
],
"summary": "OidcAuthQuery",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/admin.LoginPayload"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/peer/create": { "/admin/peer/create": {
"post": { "post": {
"security": [ "security": [
@@ -3972,6 +4061,14 @@
} }
} }
}, },
"response.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"response.Response": { "response.Response": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -653,6 +653,11 @@ definitions:
total: total:
type: integer type: integer
type: object type: object
response.ErrorResponse:
properties:
error:
type: string
type: object
response.Response: response.Response:
properties: properties:
code: code:
@@ -1520,6 +1525,27 @@ paths:
summary: 登录 summary: 登录
tags: tags:
- 登录 - 登录
/admin/login-options:
post:
consumes:
- application/json
description: 登录选项
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
type: string
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: 登录选项
tags:
- 登录
/admin/loginLog/delete: /admin/loginLog/delete:
post: post:
consumes: consumes:
@@ -1799,6 +1825,41 @@ paths:
summary: Oauth编辑 summary: Oauth编辑
tags: tags:
- Oauth - Oauth
/admin/oidc/auth:
post:
consumes:
- application/json
description: OidcAuth
produces:
- application/json
responses: {}
summary: OidcAuth
tags:
- Oauth
/admin/oidc/auth-query:
get:
consumes:
- application/json
description: OidcAuthQuery
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.Response'
- properties:
data:
$ref: '#/definitions/admin.LoginPayload'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
summary: OidcAuthQuery
tags:
- Oauth
/admin/peer/create: /admin/peer/create:
post: post:
consumes: consumes:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -2,13 +2,16 @@ package admin
import ( import (
"Gwen/global" "Gwen/global"
"Gwen/http/controller/api"
"Gwen/http/request/admin" "Gwen/http/request/admin"
apiReq "Gwen/http/request/api"
"Gwen/http/response" "Gwen/http/response"
adResp "Gwen/http/response/admin" adResp "Gwen/http/response/admin"
"Gwen/model" "Gwen/model"
"Gwen/service" "Gwen/service"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
) )
type Login struct { type Login struct {
@@ -51,7 +54,7 @@ func (ct *Login) Login(c *gin.Context) {
ut := service.AllService.UserService.Login(u, &model.LoginLog{ ut := service.AllService.UserService.Login(u, &model.LoginLog{
UserId: u.Id, UserId: u.Id,
Client: "webadmin", Client: "webadmin",
Uuid: "", Uuid: "", //must be empty
Ip: c.ClientIP(), Ip: c.ClientIP(),
Type: "account", Type: "account",
Platform: f.Platform, Platform: f.Platform,
@@ -82,3 +85,87 @@ func (ct *Login) Logout(c *gin.Context) {
} }
response.Success(c, nil) response.Success(c, nil)
} }
// LoginOptions
// @Tags 登录
// @Summary 登录选项
// @Description 登录选项
// @Accept json
// @Produce json
// @Success 200 {object} []string
// @Failure 500 {object} response.ErrorResponse
// @Router /admin/login-options [post]
func (ct *Login) LoginOptions(c *gin.Context) {
res := service.AllService.OauthService.List(1, 100, func(tx *gorm.DB) {
tx.Select("op").Order("id")
})
var ops []string
for _, v := range res.Oauths {
ops = append(ops, v.Op)
}
response.Success(c, ops)
}
// OidcAuth
// @Tags Oauth
// @Summary OidcAuth
// @Description OidcAuth
// @Accept json
// @Produce json
// @Router /admin/oidc/auth [post]
func (ct *Login) OidcAuth(c *gin.Context) {
// o := &api.Oauth{}
// o.OidcAuth(c)
f := &apiReq.OidcAuthRequest{}
err := c.ShouldBindJSON(f)
if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
err, code, url := service.AllService.OauthService.BeginAuth(f.Op)
if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error()))
return
}
service.AllService.OauthService.SetOauthCache(code, &service.OauthCacheItem{
Action: service.OauthActionTypeLogin,
Op: f.Op,
Id: f.Id,
DeviceType: "webadmin",
// DeviceOs: ct.Platform(c),
DeviceOs: f.DeviceInfo.Os,
Uuid: f.Uuid,
}, 5*60)
response.Success(c, gin.H{
"code": code,
"url": url,
})
}
// OidcAuthQuery
// @Tags Oauth
// @Summary OidcAuthQuery
// @Description OidcAuthQuery
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=adResp.LoginPayload}
// @Failure 500 {object} response.Response
// @Router /admin/oidc/auth-query [get]
func (ct *Login) OidcAuthQuery(c *gin.Context) {
o := &api.Oauth{}
u, ut := o.OidcAuthQueryPre(c)
if ut == nil {
return
}
//fmt.Println("u:", u)
//fmt.Println("ut:", ut)
response.Success(c, &adResp.LoginPayload{
Token: ut.Token,
Username: u.Username,
RouteNames: service.AllService.UserService.RouteNames(u),
Nickname: u.Nickname,
})
}

View File

@@ -102,7 +102,7 @@ func (o *Oauth) BindConfirm(c *gin.Context) {
return return
} }
u := service.AllService.UserService.CurUser(c) u := service.AllService.UserService.CurUser(c)
err = service.AllService.OauthService.BindGithubUser(v.ThirdOpenId, v.ThirdOpenId, u.Id) err = service.AllService.OauthService.BindOauthUser(v.Op, v.ThirdOpenId, v.ThirdName, u.Id)
if err != nil { if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "BindFail")) response.Fail(c, 101, response.TranslateMsg(c, "BindFail"))
return return

View File

@@ -32,6 +32,7 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error()) response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
//fmt.Println(f)
if f.Op != model.OauthTypeWebauth && f.Op != model.OauthTypeGoogle && f.Op != model.OauthTypeGithub && f.Op != model.OauthTypeOidc { if f.Op != model.OauthTypeWebauth && f.Op != model.OauthTypeGoogle && f.Op != model.OauthTypeGithub && f.Op != model.OauthTypeOidc {
response.Error(c, response.TranslateMsg(c, "ParamsError")) response.Error(c, response.TranslateMsg(c, "ParamsError"))
return return
@@ -59,6 +60,59 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
}) })
} }
func (o *Oauth) OidcAuthQueryPre(c *gin.Context) (*model.User, *model.UserToken) {
var u *model.User
var ut *model.UserToken
q := &api.OidcAuthQuery{}
// 解析查询参数并处理错误
if err := c.ShouldBindQuery(q); err != nil {
response.Error(c, response.TranslateMsg(c, "ParamsError")+": "+err.Error())
return nil, nil
}
// 获取 OAuth 缓存
v := service.AllService.OauthService.GetOauthCache(q.Code)
if v == nil {
response.Error(c, response.TranslateMsg(c, "OauthExpired"))
return nil, nil
}
// 如果 UserId 为 0说明还在授权中
if v.UserId == 0 {
c.JSON(http.StatusOK, gin.H{"message": "Authorization in progress, please login and bind"})
return nil, nil
}
// 获取用户信息
u = service.AllService.UserService.InfoById(v.UserId)
if u == nil {
response.Error(c, response.TranslateMsg(c, "UserNotFound"))
return nil, nil
}
// 删除 OAuth 缓存
service.AllService.OauthService.DeleteOauthCache(q.Code)
// 创建登录日志并生成用户令牌
ut = service.AllService.UserService.Login(u, &model.LoginLog{
UserId: u.Id,
Client: v.DeviceType,
Uuid: v.Uuid,
Ip: c.ClientIP(),
Type: model.LoginLogTypeOauth,
Platform: v.DeviceOs,
})
if ut == nil {
response.Error(c, response.TranslateMsg(c, "LoginFailed"))
return nil, nil
}
// 返回用户令牌
return u, ut
}
// OidcAuthQuery // OidcAuthQuery
// @Tags Oauth // @Tags Oauth
// @Summary OidcAuthQuery // @Summary OidcAuthQuery
@@ -69,33 +123,10 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /oidc/auth-query [get] // @Router /oidc/auth-query [get]
func (o *Oauth) OidcAuthQuery(c *gin.Context) { func (o *Oauth) OidcAuthQuery(c *gin.Context) {
q := &api.OidcAuthQuery{} u, ut := o.OidcAuthQueryPre(c)
err := c.ShouldBindQuery(q) if u == nil || ut == nil {
if err != nil {
response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
v := service.AllService.OauthService.GetOauthCache(q.Code)
if v == nil {
response.Error(c, response.TranslateMsg(c, "OauthExpired"))
return
}
if v.UserId == 0 {
//正在授权
c.JSON(http.StatusOK, gin.H{})
return
}
u := service.AllService.UserService.InfoById(v.UserId)
//fmt.Println("auth success u", u)
service.AllService.OauthService.DeleteOauthCache(q.Code)
ut := service.AllService.UserService.Login(u, &model.LoginLog{
UserId: u.Id,
Client: v.DeviceType,
Uuid: v.Uuid,
Ip: c.ClientIP(),
Type: model.LoginLogTypeOauth,
Platform: v.DeviceOs,
})
c.JSON(http.StatusOK, apiResp.LoginRes{ c.JSON(http.StatusOK, apiResp.LoginRes{
AccessToken: ut.Token, AccessToken: ut.Token,
Type: "access_token", Type: "access_token",
@@ -129,7 +160,11 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
ty := v.Op ty := v.Op
ac := v.Action ac := v.Action
var u *model.User
openid := ""
thirdName := ""
//fmt.Println("ty ac ", ty, ac) //fmt.Println("ty ac ", ty, ac)
if ty == model.OauthTypeGithub { if ty == model.OauthTypeGithub {
code := c.Query("code") code := c.Query("code")
err, userData := service.AllService.OauthService.GithubCallback(code) err, userData := service.AllService.OauthService.GithubCallback(code)
@@ -137,187 +172,100 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error())) c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error()))
return return
} }
if ac == service.OauthActionTypeBind { openid = strconv.Itoa(userData.Id)
//fmt.Println("bind", ty, userData) thirdName = userData.Login
utr := service.AllService.OauthService.UserThirdInfo(ty, strconv.Itoa(userData.Id)) } else if ty == model.OauthTypeGoogle {
if utr.UserId > 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBindOtherUser"))
return
}
//绑定
u := service.AllService.UserService.InfoById(v.UserId)
if u == nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ItemNotFound"))
return
}
//绑定github
err = service.AllService.OauthService.BindGithubUser(strconv.Itoa(userData.Id), userData.Login, v.UserId)
if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "BindFail"))
return
}
c.String(http.StatusOK, response.TranslateMsg(c, "BindSuccess"))
return
} else if ac == service.OauthActionTypeLogin {
//登录
if v.UserId != 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBeenSuccess"))
return
}
u := service.AllService.UserService.InfoByGithubId(strconv.Itoa(userData.Id))
if u == nil {
oa := service.AllService.OauthService.InfoByOp(ty)
if !*oa.AutoRegister {
//c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定")
v.ThirdName = userData.Login
v.ThirdOpenId = strconv.Itoa(userData.Id)
url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey
c.Redirect(http.StatusFound, url)
return
}
//自动注册
u = service.AllService.UserService.RegisterByGithub(userData.Login, strconv.Itoa(userData.Id))
if u.Id == 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthRegisterFailed"))
return
}
}
v.UserId = u.Id
service.AllService.OauthService.SetOauthCache(cacheKey, v, 0)
c.String(http.StatusOK, response.TranslateMsg(c, "OauthSuccess"))
return
}
}
if ty == model.OauthTypeGoogle {
code := c.Query("code") code := c.Query("code")
err, userData := service.AllService.OauthService.GoogleCallback(code) err, userData := service.AllService.OauthService.GoogleCallback(code)
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error())) c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error()))
return return
} }
openid = userData.Email
//将空格替换成_ //将空格替换成_
googleName := strings.Replace(userData.Name, " ", "_", -1) thirdName = strings.Replace(userData.Name, " ", "_", -1)
if ac == service.OauthActionTypeBind { } else if ty == model.OauthTypeOidc {
//fmt.Println("bind", ty, userData)
utr := service.AllService.OauthService.UserThirdInfo(ty, userData.Email)
if utr.UserId > 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBindOtherUser"))
return
}
//绑定
u := service.AllService.UserService.InfoById(v.UserId)
if u == nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ItemNotFound"))
return
}
//绑定
err = service.AllService.OauthService.BindGoogleUser(userData.Email, googleName, v.UserId)
if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "BindFail"))
return
}
c.String(http.StatusOK, response.TranslateMsg(c, "BindSuccess"))
return
} else if ac == service.OauthActionTypeLogin {
if v.UserId != 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBeenSuccess"))
return
}
u := service.AllService.UserService.InfoByGoogleEmail(userData.Email)
if u == nil {
oa := service.AllService.OauthService.InfoByOp(ty)
if !*oa.AutoRegister {
//c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定")
v.ThirdName = googleName
v.ThirdOpenId = userData.Email
url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey
c.Redirect(http.StatusFound, url)
return
}
//自动注册
u = service.AllService.UserService.RegisterByGoogle(googleName, userData.Email)
if u.Id == 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthRegisterFailed"))
return
}
}
v.UserId = u.Id
service.AllService.OauthService.SetOauthCache(cacheKey, v, 0)
c.String(http.StatusOK, response.TranslateMsg(c, "OauthSuccess"))
return
}
}
if ty == model.OauthTypeOidc {
code := c.Query("code") code := c.Query("code")
err, userData := service.AllService.OauthService.OidcCallback(code) err, userData := service.AllService.OauthService.OidcCallback(code)
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error())) c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error()))
return return
} }
//将空格替换成_ openid = userData.Sub
// OidcName := strings.Replace(userData.Name, " ", "_", -1) thirdName = userData.PreferredUsername
if ac == service.OauthActionTypeBind { } else {
//fmt.Println("bind", ty, userData) c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ParamsError"))
utr := service.AllService.OauthService.UserThirdInfo(ty, userData.Sub) return
if utr.UserId > 0 { }
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBindOtherUser")) if ac == service.OauthActionTypeBind {
return
}
//绑定
u := service.AllService.UserService.InfoById(v.UserId)
if u == nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ItemNotFound"))
return
}
//绑定, user preffered_username as username
err = service.AllService.OauthService.BindOidcUser(userData.Sub, userData.PreferredUsername, v.UserId)
if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "BindFail"))
return
}
c.String(http.StatusOK, response.TranslateMsg(c, "BindSuccess"))
return
} else if ac == service.OauthActionTypeLogin {
if v.UserId != 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBeenSuccess"))
return
}
u := service.AllService.UserService.InfoByOidcSub(userData.Sub)
if u == nil {
oa := service.AllService.OauthService.InfoByOp(ty)
if !*oa.AutoRegister {
//c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定")
v.ThirdName = userData.PreferredUsername //fmt.Println("bind", ty, userData)
v.ThirdOpenId = userData.Sub utr := service.AllService.OauthService.UserThirdInfo(ty, openid)
v.ThirdEmail = userData.Email if utr.UserId > 0 {
url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBindOtherUser"))
c.Redirect(http.StatusFound, url)
return
}
//自动注册
u = service.AllService.UserService.RegisterByOidc(userData.PreferredUsername, userData.Sub)
if u.Id == 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthRegisterFailed"))
return
}
}
v.UserId = u.Id
service.AllService.OauthService.SetOauthCache(cacheKey, v, 0)
c.String(http.StatusOK, response.TranslateMsg(c, "OauthSuccess"))
return return
} }
//绑定
u = service.AllService.UserService.InfoById(v.UserId)
if u == nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ItemNotFound"))
return
}
//绑定
err := service.AllService.OauthService.BindOauthUser(ty, openid, thirdName, v.UserId)
if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "BindFail"))
return
}
c.String(http.StatusOK, response.TranslateMsg(c, "BindSuccess"))
return
} else if ac == service.OauthActionTypeLogin {
//登录
if v.UserId != 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBeenSuccess"))
return
}
u = service.AllService.UserService.InfoByGithubId(openid)
if u == nil {
oa := service.AllService.OauthService.InfoByOp(ty)
if !*oa.AutoRegister {
//c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定")
v.ThirdName = thirdName
v.ThirdOpenId = openid
url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey
c.Redirect(http.StatusFound, url)
return
}
//自动注册
u = service.AllService.UserService.RegisterByOauth(ty, thirdName, openid)
if u.Id == 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthRegisterFailed"))
return
}
}
v.UserId = u.Id
service.AllService.OauthService.SetOauthCache(cacheKey, v, 0)
// 如果是webadmin登录成功后跳转到webadmin
if v.DeviceType == "webadmin" {
/*service.AllService.UserService.Login(u, &model.LoginLog{
UserId: u.Id,
Client: "webadmin",
Uuid: "", //must be empty
Ip: c.ClientIP(),
Type: model.LoginLogTypeOauth,
Platform: v.DeviceOs,
})*/
url := global.Config.Rustdesk.ApiServer + "/_admin/#/"
c.Redirect(http.StatusFound, url)
return
}
c.String(http.StatusOK, response.TranslateMsg(c, "OauthSuccess"))
return
} else {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ParamsError"))
return
} }
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "SystemError"))
} }

View File

@@ -33,7 +33,6 @@ func Init(g *gin.Engine) {
rs := &admin.Rustdesk{} rs := &admin.Rustdesk{}
adg.GET("/server-config", rs.ServerConfig) adg.GET("/server-config", rs.ServerConfig)
adg.GET("/app-config", rs.AppConfig) adg.GET("/app-config", rs.AppConfig)
//访问静态文件 //访问静态文件
//g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload")) //g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload"))
} }
@@ -41,6 +40,9 @@ func LoginBind(rg *gin.RouterGroup) {
cont := &admin.Login{} cont := &admin.Login{}
rg.POST("/login", cont.Login) rg.POST("/login", cont.Login)
rg.POST("/logout", cont.Logout) rg.POST("/logout", cont.Logout)
rg.GET("/login-options", cont.LoginOptions)
rg.POST("/oidc/auth", cont.OidcAuth)
rg.GET("/oidc/auth-query", cont.OidcAuthQuery)
} }
func UserBind(rg *gin.RouterGroup) { func UserBind(rg *gin.RouterGroup) {

View File

@@ -253,6 +253,7 @@ func (os *OauthService) getOidcConfig() (error, *oauth2.Config) {
} }
func getHTTPClientWithProxy() *http.Client { func getHTTPClientWithProxy() *http.Client {
//todo add timeout
if global.Config.Proxy.Enable { if global.Config.Proxy.Enable {
if global.Config.Proxy.Host == "" { if global.Config.Proxy.Host == "" {
global.Logger.Warn("Proxy is enabled but proxy host is empty.") global.Logger.Warn("Proxy is enabled but proxy host is empty.")
@@ -441,6 +442,11 @@ func (os *OauthService) UnBindThird(thirdType string, userid uint) error {
return global.DB.Where("user_id = ? and third_type = ?", userid, thirdType).Delete(&model.UserThird{}).Error return global.DB.Where("user_id = ? and third_type = ?", userid, thirdType).Delete(&model.UserThird{}).Error
} }
// DeleteUserByUserId: When user is deleted, delete all third party bindings
func (os *OauthService) DeleteUserByUserId(userid uint) error {
return global.DB.Where("user_id = ?", userid).Delete(&model.UserThird{}).Error
}
// InfoById 根据id取用户信息 // InfoById 根据id取用户信息
func (os *OauthService) InfoById(id uint) *model.Oauth { func (os *OauthService) InfoById(id uint) *model.Oauth {
u := &model.Oauth{} u := &model.Oauth{}

View File

@@ -148,8 +148,37 @@ func (us *UserService) Create(u *model.User) error {
func (us *UserService) Logout(u *model.User, token string) error { func (us *UserService) Logout(u *model.User, token string) error {
return global.DB.Where("user_id = ? and token = ?", u.Id, token).Delete(&model.UserToken{}).Error return global.DB.Where("user_id = ? and token = ?", u.Id, token).Delete(&model.UserToken{}).Error
} }
// Delete 删除用户和oauth信息
func (us *UserService) Delete(u *model.User) error { func (us *UserService) Delete(u *model.User) error {
return global.DB.Delete(u).Error tx := global.DB.Begin()
// 删除用户
if err := tx.Delete(u).Error; err != nil {
tx.Rollback()
return err
}
// 删除关联的 OAuth 信息
if err := tx.Where("user_id = ?", u.Id).Delete(&model.UserThird{}).Error; err != nil {
tx.Rollback()
return err
}
// 删除关联的ab
if err := tx.Where("user_id = ?", u.Id).Delete(&model.AddressBook{}).Error; err != nil {
tx.Rollback()
return err
}
// 删除关联的abc
if err := tx.Where("user_id = ?", u.Id).Delete(&model.AddressBookCollection{}).Error; err != nil {
tx.Rollback()
return err
}
// 删除关联的abcr
if err := tx.Where("user_id = ?", u.Id).Delete(&model.AddressBookCollectionRule{}).Error; err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
} }
// Update 更新 // Update 更新
@@ -252,14 +281,14 @@ func (us *UserService) RegisterByOauth(thirdType, thirdName, uid string) *model.
Username: username, Username: username,
GroupId: 1, GroupId: 1,
} }
global.DB.Create(u) tx.Create(u)
if u.Id == 0 { if u.Id == 0 {
tx.Rollback() tx.Rollback()
return u return u
} }
ut.UserId = u.Id ut.UserId = u.Id
global.DB.Create(ut) tx.Create(ut)
tx.Commit() tx.Commit()
return u return u