mirror of
https://github.com/cloudreve/cloudreve.git
synced 2026-06-27 22:35:59 +00:00
feat(oauth): OAuth for 3rd party apps
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/group"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/node"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/oauthclient"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
@@ -45,6 +46,10 @@ func migrate(l logging.Logger, client *ent.Client, ctx context.Context, kv cache
|
||||
return fmt.Errorf("failed migrating default storage policy: %w", err)
|
||||
}
|
||||
|
||||
if err := migrateOAuthClient(l, client, ctx); err != nil {
|
||||
return fmt.Errorf("failed migrating OAuth client: %w", err)
|
||||
}
|
||||
|
||||
if err := applyPatches(l, client, ctx, requiredDbVersion); err != nil {
|
||||
return fmt.Errorf("failed applying schema patches: %w", err)
|
||||
}
|
||||
@@ -269,6 +274,70 @@ func migrateMasterNode(l logging.Logger, client *ent.Client, ctx context.Context
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
OAuthClientDesktopGUID = "393a1839-f52e-498e-9972-e77cc2241eee"
|
||||
OAuthClientDesktopSecret = "8GaQIu3lOSdqYoDHi9cR8IZ4pvuMH8ya"
|
||||
OAuthClientDesktopName = "application:oauth.desktop"
|
||||
OAuthClientDesktopRedirectURI = "/callback/desktop"
|
||||
OAuthClientiOSGUID = "220db97a-44a3-44f7-99b6-d767262b4daa"
|
||||
OAuthClientiOSSecret = "1kxOW4IyVOkPlsKCnTwzfHyP8XrbpfaF"
|
||||
OAuthClientiOSName = "application:setting.iOSApp"
|
||||
OAuthClientiOSRedirectURI = "/callback/ios"
|
||||
)
|
||||
|
||||
func migrateOAuthClient(l logging.Logger, client *ent.Client, ctx context.Context) error {
|
||||
if err := migrateOAuthClientDesktop(l, client, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := migrateOAuthClientiOS(l, client, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateOAuthClientiOS(l logging.Logger, client *ent.Client, ctx context.Context) error {
|
||||
if _, err := client.OAuthClient.Query().Where(oauthclient.GUID(OAuthClientiOSGUID)).First(ctx); err == nil {
|
||||
l.Info("Default OAuth client (GUID=%s) already exists, skip migrating.", OAuthClientiOSGUID)
|
||||
return nil
|
||||
}
|
||||
if _, err := client.OAuthClient.Create().
|
||||
SetGUID(OAuthClientiOSGUID).
|
||||
SetSecret(OAuthClientiOSSecret).
|
||||
SetName(OAuthClientiOSName).
|
||||
SetRedirectUris([]string{OAuthClientiOSRedirectURI}).
|
||||
SetScopes([]string{"profile", "email", "openid", "offline_access", "UserInfo.Write", "UserSecurityInfo.Write", "Workflow.Write", "Files.Write", "Shares.Write", "Finance.Write", "DavAccount.Write"}).
|
||||
SetProps(&types.OAuthClientProps{Icon: "/static/img/cloudreve_ios.svg", RefreshTokenTTL: 7776000}).
|
||||
SetIsEnabled(true).
|
||||
Save(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create default OAuth client: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateOAuthClientDesktop(l logging.Logger, client *ent.Client, ctx context.Context) error {
|
||||
if _, err := client.OAuthClient.Query().Where(oauthclient.GUID(OAuthClientDesktopGUID)).First(ctx); err == nil {
|
||||
l.Info("Default OAuth client (GUID=%s) already exists, skip migrating.", OAuthClientDesktopGUID)
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := client.OAuthClient.Create().
|
||||
SetGUID(OAuthClientDesktopGUID).
|
||||
SetSecret(OAuthClientDesktopSecret).
|
||||
SetName(OAuthClientDesktopName).
|
||||
SetRedirectUris([]string{OAuthClientDesktopRedirectURI}).
|
||||
SetScopes([]string{"profile", "email", "openid", "offline_access", "UserInfo.Write", "Workflow.Write", "Files.Write", "Shares.Write"}).
|
||||
SetProps(&types.OAuthClientProps{Icon: "/static/img/cloudreve.svg", RefreshTokenTTL: 7776000}).
|
||||
SetIsEnabled(true).
|
||||
Save(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create default OAuth client: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
PatchFunc func(l logging.Logger, client *ent.Client, ctx context.Context) error
|
||||
Patch struct {
|
||||
@@ -467,7 +536,7 @@ var patches = []Patch{
|
||||
for i, t := range mailResetTemplate {
|
||||
mailResetTemplate[i].Title = fmt.Sprintf("[{{ .CommonContext.SiteBasic.Name }}] %s", t.Title)
|
||||
}
|
||||
|
||||
|
||||
newMailResetTemplate, err := json.Marshal(mailResetTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal mail_reset_template setting: %w", err)
|
||||
|
||||
82
inventory/oauth_client.go
Normal file
82
inventory/oauth_client.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/oauthclient"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/oauthgrant"
|
||||
)
|
||||
|
||||
type (
|
||||
OAuthClientClient interface {
|
||||
TxOperator
|
||||
// GetByGUID returns the OAuth client by its GUID (client_id).
|
||||
GetByGUID(ctx context.Context, guid string) (*ent.OAuthClient, error)
|
||||
// GetByGUIDWithGrants returns the OAuth client by its GUID (client_id) with the grants for the user.
|
||||
GetByGUIDWithGrants(ctx context.Context, guid string, uid int) (*ent.OAuthClient, error)
|
||||
// UpsertGrant creates or updates an OAuth grant for a user and client.
|
||||
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
|
||||
}
|
||||
)
|
||||
|
||||
func NewOAuthClientClient(client *ent.Client) OAuthClientClient {
|
||||
return &oauthClientClient{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
type oauthClientClient struct {
|
||||
client *ent.Client
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) SetClient(newClient *ent.Client) TxOperator {
|
||||
return &oauthClientClient{client: newClient}
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) GetClient() *ent.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) GetByGUID(ctx context.Context, guid string) (*ent.OAuthClient, error) {
|
||||
return c.client.OAuthClient.Query().
|
||||
Where(oauthclient.GUID(guid), oauthclient.IsEnabled(true)).
|
||||
First(ctx)
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) GetByGUIDWithGrants(ctx context.Context, guid string, uid int) (*ent.OAuthClient, error) {
|
||||
stm := c.client.OAuthClient.Query().
|
||||
Where(oauthclient.GUID(guid), oauthclient.IsEnabled(true))
|
||||
if uid > 0 {
|
||||
stm.WithGrants(func(ogq *ent.OAuthGrantQuery) {
|
||||
ogq.Where(oauthgrant.UserID(uid))
|
||||
})
|
||||
}
|
||||
|
||||
return stm.First(ctx)
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) UpsertGrant(ctx context.Context, userID, clientID int, scopes []string) error {
|
||||
return c.client.OAuthGrant.Create().
|
||||
SetUserID(userID).
|
||||
SetClientID(clientID).
|
||||
SetScopes(scopes).
|
||||
SetLastUsedAt(time.Now()).
|
||||
OnConflict(
|
||||
sql.ConflictColumns(oauthgrant.FieldUserID, oauthgrant.FieldClientID),
|
||||
).
|
||||
UpdateScopes().
|
||||
UpdateLastUsedAt().
|
||||
Exec(ctx)
|
||||
}
|
||||
|
||||
func (c *oauthClientClient) UpdateGrantLastUsedAt(ctx context.Context, userID, clientID int) error {
|
||||
return c.client.OAuthGrant.Update().
|
||||
Where(oauthgrant.UserID(userID), oauthgrant.ClientID(clientID)).
|
||||
SetLastUsedAt(time.Now()).
|
||||
Exec(ctx)
|
||||
}
|
||||
@@ -207,6 +207,12 @@ type (
|
||||
ShowReadMe bool `json:"show_read_me,omitempty"`
|
||||
}
|
||||
|
||||
OAuthClientProps struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
RefreshTokenTTL int64 `json:"refresh_token_ttl,omitempty"` // in seconds, 0 means default
|
||||
}
|
||||
|
||||
FileTypeIconSetting struct {
|
||||
Exts []string `json:"exts"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
@@ -363,3 +369,26 @@ const (
|
||||
const (
|
||||
CipherAES256CTR Cipher = "aes-256-ctr"
|
||||
)
|
||||
|
||||
const (
|
||||
ScopeProfile = "profile"
|
||||
ScopeEmail = "email"
|
||||
ScopeOpenID = "openid"
|
||||
ScopeOfflineAccess = "offline_access"
|
||||
ScopeUserInfoRead = "UserInfo.Read"
|
||||
ScopeUserInfoWrite = "UserInfo.Write"
|
||||
ScopeUserSecurityInfoRead = "UserSecurityInfo.Read"
|
||||
ScopeUserSecurityInfoWrite = "UserSecurityInfo.Write"
|
||||
ScopeWorkflowRead = "Workflow.Read"
|
||||
ScopeWorkflowWrite = "Workflow.Write"
|
||||
ScopeAdminRead = "Admin.Read"
|
||||
ScopeAdminWrite = "Admin.Write"
|
||||
ScopeFilesRead = "Files.Read"
|
||||
ScopeFilesWrite = "Files.Write"
|
||||
ScopeSharesRead = "Shares.Read"
|
||||
ScopeSharesWrite = "Shares.Write"
|
||||
ScopeFinanceRead = "Finance.Read"
|
||||
ScopeFinanceWrite = "Finance.Write"
|
||||
ScopeDavAccountRead = "DavAccount.Read"
|
||||
ScopeDavAccountWrite = "DavAccount.Write"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user