mirror of
https://github.com/cloudreve/cloudreve.git
synced 2026-06-28 06:46:08 +00:00
feat(oauth): user can manage existing OAuth grant
This commit is contained in:
@@ -26,6 +26,9 @@ type (
|
||||
UpsertGrant(ctx context.Context, userID, clientID int, scopes []string) error
|
||||
// UpdateGrantLastUsedAt updates the last used at for an OAuth grant for a user and client.
|
||||
UpdateGrantLastUsedAt(ctx context.Context, userID, clientID int) error
|
||||
// DeleteGrantByUserAndClientGUID deletes an OAuth grant for a user by the client GUID.
|
||||
// Returns true if the grant was deleted, false if it was not found.
|
||||
DeleteGrantByUserAndClientGUID(ctx context.Context, userID int, clientGUID string) (bool, error)
|
||||
// List returns a paginated list of OAuth clients.
|
||||
List(ctx context.Context, args *ListOAuthClientArgs) (*ListOAuthClientResult, error)
|
||||
// GetByID returns the OAuth client by its ID.
|
||||
@@ -38,6 +41,8 @@ type (
|
||||
Delete(ctx context.Context, id int) error
|
||||
// CountGrants returns the number of grants for an OAuth client.
|
||||
CountGrants(ctx context.Context, id int) (int, error)
|
||||
// GetGrantsByUserID returns the OAuth grants for a user.
|
||||
GetGrantsByUserID(ctx context.Context, userID int) ([]*ent.OAuthGrant, error)
|
||||
}
|
||||
|
||||
ListOAuthClientArgs struct {
|
||||
@@ -50,6 +55,8 @@ type (
|
||||
*PaginationResults
|
||||
Clients []*ent.OAuthClient
|
||||
}
|
||||
|
||||
LoadOAuthGrantClient struct{}
|
||||
)
|
||||
|
||||
func NewOAuthClientClient(client *ent.Client, dbType conf.DBType) OAuthClientClient {
|
||||
@@ -111,6 +118,35 @@ func (c *oauthClientClient) UpdateGrantLastUsedAt(ctx context.Context, userID, c
|
||||
Exec(ctx)
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) GetGrantsByUserID(ctx context.Context, userID int) ([]*ent.OAuthGrant, error) {
|
||||
return withOAuthGrantEagerLoadings(ctx, c.client.OAuthGrant.Query()).
|
||||
Where(oauthgrant.UserID(userID)).
|
||||
All(ctx)
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) DeleteGrantByUserAndClientGUID(ctx context.Context, userID int, clientGUID string) (bool, error) {
|
||||
// First, get the client by GUID to get its ID
|
||||
client, err := c.client.OAuthClient.Query().
|
||||
Where(oauthclient.GUID(clientGUID)).
|
||||
First(ctx)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("failed to get OAuth client: %w", err)
|
||||
}
|
||||
|
||||
// Delete the grant for this user and client
|
||||
deleted, err := c.client.OAuthGrant.Delete().
|
||||
Where(oauthgrant.UserID(userID), oauthgrant.ClientID(client.ID)).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to delete OAuth grant: %w", err)
|
||||
}
|
||||
|
||||
return deleted > 0, nil
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) List(ctx context.Context, args *ListOAuthClientArgs) (*ListOAuthClientResult, error) {
|
||||
query := c.client.OAuthClient.Query()
|
||||
|
||||
@@ -233,3 +269,12 @@ func getOAuthClientOrderOption(args *ListOAuthClientArgs) []oauthclient.OrderOpt
|
||||
return []oauthclient.OrderOption{oauthclient.ByID(orderTerm)}
|
||||
}
|
||||
}
|
||||
|
||||
func withOAuthGrantEagerLoadings(ctx context.Context, q *ent.OAuthGrantQuery) *ent.OAuthGrantQuery {
|
||||
if v, ok := ctx.Value(LoadOAuthGrantClient{}).(bool); ok && v {
|
||||
q.WithClient(func(ocq *ent.OAuthClientQuery) {
|
||||
})
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
@@ -66,3 +66,15 @@ func OpenIDUserInfo(c *gin.Context) {
|
||||
|
||||
c.JSON(200, res)
|
||||
}
|
||||
|
||||
func DeleteOAuthGrant(c *gin.Context) {
|
||||
service := ParametersFromContext[*oauth.DeleteOAuthGrantService](c, oauth.DeleteOAuthGrantParamCtx{})
|
||||
err := service.Delete(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.Response{})
|
||||
}
|
||||
|
||||
@@ -338,6 +338,12 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
|
||||
controllers.FromQuery[oauth.UserInfoService](oauth.UserInfoParamCtx{}),
|
||||
controllers.OpenIDUserInfo,
|
||||
)
|
||||
oauthRouter.DELETE("grant/:app_id",
|
||||
middleware.LoginRequired(),
|
||||
middleware.RequiredScopes(types.ScopeUserSecurityInfoWrite),
|
||||
controllers.FromUri[oauth.DeleteOAuthGrantService](oauth.DeleteOAuthGrantParamCtx{}),
|
||||
controllers.DeleteOAuthGrant,
|
||||
)
|
||||
}
|
||||
|
||||
authn := session.Group("authn")
|
||||
|
||||
@@ -224,6 +224,38 @@ func (s *ExchangeTokenService) Exchange(c *gin.Context) (*TokenResponse, error)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type (
|
||||
DeleteOAuthGrantParamCtx struct{}
|
||||
DeleteOAuthGrantService struct {
|
||||
AppID string `uri:"app_id" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *DeleteOAuthGrantService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
oAuthClient := dep.OAuthClientClient()
|
||||
|
||||
// Delete the grant - the method validates that the grant belongs to the current user
|
||||
deleted, err := oAuthClient.DeleteGrantByUserAndClientGUID(c, user.ID, s.AppID)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete OAuth grant", err)
|
||||
}
|
||||
|
||||
if !deleted {
|
||||
return serializer.NewError(serializer.CodeNotFound, "OAuth grant not found", nil)
|
||||
}
|
||||
|
||||
dep.AuditRecorder().Record(c, &types.LogEntry{
|
||||
Category: types.AuditLogTypeOAuthGrantRevoke,
|
||||
Exts: map[string]string{
|
||||
"client_id": s.AppID,
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
UserInfoParamCtx struct{}
|
||||
UserInfoService struct{}
|
||||
|
||||
@@ -22,17 +22,18 @@ type PreparePasskeyLoginResponse struct {
|
||||
}
|
||||
|
||||
type UserSettings struct {
|
||||
VersionRetentionEnabled bool `json:"version_retention_enabled"`
|
||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
||||
Paswordless bool `json:"passwordless"`
|
||||
TwoFAEnabled bool `json:"two_fa_enabled"`
|
||||
Passkeys []Passkey `json:"passkeys,omitempty"`
|
||||
DisableViewSync bool `json:"disable_view_sync"`
|
||||
ShareLinksInProfile string `json:"share_links_in_profile"`
|
||||
VersionRetentionEnabled bool `json:"version_retention_enabled"`
|
||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
||||
Paswordless bool `json:"passwordless"`
|
||||
TwoFAEnabled bool `json:"two_fa_enabled"`
|
||||
Passkeys []Passkey `json:"passkeys,omitempty"`
|
||||
DisableViewSync bool `json:"disable_view_sync"`
|
||||
ShareLinksInProfile string `json:"share_links_in_profile"`
|
||||
OAuthGrants []OauthGrant `json:"oauth_grants,omitempty"`
|
||||
}
|
||||
|
||||
func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings {
|
||||
func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser, grants []*ent.OAuthGrant) *UserSettings {
|
||||
return &UserSettings{
|
||||
VersionRetentionEnabled: u.Settings.VersionRetention,
|
||||
VersionRetentionExt: u.Settings.VersionRetentionExt,
|
||||
@@ -44,6 +45,9 @@ func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Pa
|
||||
}),
|
||||
DisableViewSync: u.Settings.DisableViewSync,
|
||||
ShareLinksInProfile: string(u.Settings.ShareLinksInProfile),
|
||||
OAuthGrants: lo.Map(grants, func(item *ent.OAuthGrant, index int) OauthGrant {
|
||||
return BuildOauthGrant(item)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +176,31 @@ func BuildUser(user *ent.User, idEncoder hashid.Encoder) User {
|
||||
}
|
||||
}
|
||||
|
||||
type OauthGrant struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientName string `json:"client_name"`
|
||||
ClientLogo string `json:"client_logo"`
|
||||
Scopes []string `json:"scopes"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
}
|
||||
|
||||
func BuildOauthGrant(grant *ent.OAuthGrant) OauthGrant {
|
||||
res := OauthGrant{
|
||||
Scopes: grant.Scopes,
|
||||
LastUsedAt: grant.LastUsedAt,
|
||||
}
|
||||
|
||||
if grant.Edges.Client != nil {
|
||||
res.ClientID = grant.Edges.Client.GUID
|
||||
res.ClientName = grant.Edges.Client.Name
|
||||
if grant.Edges.Client.Props != nil {
|
||||
res.ClientLogo = grant.Edges.Client.Props.Icon
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func BuildGroup(group *ent.Group, idEncoder hashid.Encoder) *Group {
|
||||
if group == nil {
|
||||
return nil
|
||||
|
||||
@@ -131,7 +131,13 @@ func GetUserSettings(c *gin.Context) (*UserSettings, error) {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user passkey", err)
|
||||
}
|
||||
|
||||
return BuildUserSettings(u, passkeys, dep.UAParser()), nil
|
||||
ctx := context.WithValue(c, inventory.LoadOAuthGrantClient{}, true)
|
||||
grants, err := dep.OAuthClientClient().GetGrantsByUserID(ctx, u.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user OAuth grants", err)
|
||||
}
|
||||
|
||||
return BuildUserSettings(u, passkeys, dep.UAParser(), grants), nil
|
||||
|
||||
// 用户组有效期
|
||||
|
||||
|
||||
Reference in New Issue
Block a user