package drive import ( "context" "encoding/json" "errors" "fmt" "strings" "github.com/ultisuite/ulti-backend/internal/drivestore" "github.com/ultisuite/ulti-backend/internal/nextcloud" "github.com/ultisuite/ulti-backend/internal/orgpolicy" ) var ErrOAuthNotConfigured = errors.New("oauth provider not configured") type MountView struct { ID string `json:"id"` Scope string `json:"scope"` OrgSlug *string `json:"org_slug,omitempty"` DisplayName string `json:"display_name"` BackendType string `json:"backend_type"` MountPoint string `json:"mount_point"` Status string `json:"status"` LastError string `json:"last_error,omitempty"` NCMountID *int `json:"nc_mount_id,omitempty"` NeedsOAuth bool `json:"needs_oauth,omitempty"` } type CreateMountParams struct { Scope string OrgSlug string DisplayName string BackendType string WebDAV *nextcloud.WebDAVMountConfig OAuthBackend string OAuthAuth string } func oauthProviderForBackend(backendType string) (providerKey, ncBackend, authBackend string, isOAuth bool) { switch strings.TrimSpace(strings.ToLower(backendType)) { case "googledrive", "google": return orgpolicy.MountOAuthProviderGoogle, "googledrive", "oauth2::google", true case "dropbox": return orgpolicy.MountOAuthProviderDropbox, "dropbox", "oauth2::dropbox", true case "onedrive", "microsoft": return orgpolicy.MountOAuthProviderMicrosoft, "onedrive", "oauth2::microsoft", true default: return "", "", "", false } } func (s *Service) orgPolicyLoader() *orgpolicy.Loader { return orgpolicy.NewLoader(s.db, nil) } func (s *Service) ListMountsForUser(ctx context.Context, platformUserID, ncUserID string, orgSlugs []string) ([]MountView, error) { store := s.ensureStore() if store == nil { return nil, fmt.Errorf("store not configured") } mounts, err := store.ListMountsForUser(ctx, platformUserID, orgSlugs) if err != nil { return nil, err } out := make([]MountView, 0, len(mounts)) for _, m := range mounts { out = append(out, mapMountView(m)) } return out, nil } func mapMountView(m drivestore.Mount) MountView { view := MountView{ ID: m.ID, Scope: m.Scope, OrgSlug: m.OrgSlug, DisplayName: m.DisplayName, BackendType: m.BackendType, MountPoint: m.MountPoint, Status: m.Status, LastError: m.LastError, NCMountID: m.NCMountID, } if m.Status == "pending_oauth" { view.NeedsOAuth = true } return view } func (s *Service) CreateMount(ctx context.Context, platformUserID, ncUserID string, p CreateMountParams) (MountView, error) { store := s.ensureStore() if store == nil { return MountView{}, fmt.Errorf("store not configured") } displayName := strings.TrimSpace(p.DisplayName) backendType := strings.TrimSpace(strings.ToLower(p.BackendType)) if displayName == "" || backendType == "" { return MountView{}, ErrInvalid } mountPoint := "/" + strings.Trim(displayName, "/") scope := strings.TrimSpace(strings.ToLower(p.Scope)) if scope != "user" && scope != "org" { return MountView{}, ErrInvalid } var ncMountID int var err error var configEnc []byte status := "active" providerKey, ncBackend, authBackend, isOAuth := oauthProviderForBackend(backendType) if isOAuth { creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey) if err != nil { return MountView{}, err } if !creds.Enabled { return MountView{}, ErrOAuthNotConfigured } oauthConfig := map[string]string{ "client_id": creds.ClientID, "client_secret": creds.ClientSecret, "configured": "false", "token": "", } configEnc, _ = json.Marshal(oauthConfig) if scope == "org" { return MountView{}, ErrInvalid } ncMountID, err = s.nc.CreateOAuthExternalMount(ctx, ncUserID, mountPoint, ncBackend, authBackend, oauthConfig) status = "pending_oauth" } else { switch backendType { case "webdav", "dav": if p.WebDAV == nil { return MountView{}, ErrInvalid } configEnc, _ = json.Marshal(p.WebDAV) if scope == "org" { ncMountID, err = s.nc.CreateGlobalWebDAVMount(ctx, mountPoint, *p.WebDAV) } else { ncMountID, err = s.nc.CreateUserWebDAVMount(ctx, ncUserID, mountPoint, *p.WebDAV) } case "googledrive", "google", "dropbox", "onedrive", "microsoft": return MountView{}, ErrOAuthNotConfigured default: if p.OAuthBackend != "" { auth := p.OAuthAuth if auth == "" { auth = "oauth2::" + backendType } ncMountID, err = s.nc.CreateOAuthExternalMount(ctx, ncUserID, mountPoint, p.OAuthBackend, auth, nil) status = "pending_oauth" } else { return MountView{}, ErrInvalid } } } lastError := "" if err != nil { status = "error" lastError = err.Error() } var ownerID *string var orgSlug *string if scope == "user" { ownerID = &platformUserID } else { slug := strings.TrimSpace(strings.ToLower(p.OrgSlug)) if slug == "" { return MountView{}, ErrInvalid } orgSlug = &slug } var ncIDPtr *int if ncMountID > 0 { ncIDPtr = &ncMountID } row, err := store.CreateMount(ctx, drivestore.CreateMountParams{ Scope: scope, OwnerUserID: ownerID, OrgSlug: orgSlug, NCMountID: ncIDPtr, DisplayName: displayName, BackendType: backendType, MountPoint: mountPoint, Status: status, ConfigEnc: configEnc, }) if err != nil { if ncMountID > 0 { _ = s.nc.DeleteExternalMount(ctx, ncMountID) } return MountView{}, err } if status == "error" { _ = store.UpdateMountStatus(ctx, row.ID, status, lastError, ncIDPtr) } return mapMountView(row), nil } func (s *Service) DeleteMount(ctx context.Context, mountID string) error { store := s.ensureStore() if store == nil { return fmt.Errorf("store not configured") } mount, err := store.GetMount(ctx, mountID) if err != nil { return err } if mount.NCMountID != nil && *mount.NCMountID > 0 { if err := s.nc.DeleteExternalMount(ctx, *mount.NCMountID); err != nil { return mapDriveError(err) } } return store.DeleteMount(ctx, mountID) } func (s *Service) GetMountOAuthURL(ctx context.Context, mountID, platformUserID, ncUserID, redirectURI string) (string, error) { if err := validateMountOAuthRedirectURI(redirectURI); err != nil { return "", err } store := s.ensureStore() if store == nil { return "", fmt.Errorf("store not configured") } mount, err := store.GetMount(ctx, mountID) if err != nil { return "", err } if mount.OwnerUserID == nil || *mount.OwnerUserID != platformUserID { return "", ErrForbidden } if mount.NCMountID == nil { return "", ErrInvalid } providerKey, _, _, isOAuth := oauthProviderForBackend(mount.BackendType) if !isOAuth { return "", ErrInvalid } creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey) if err != nil { return "", err } if !creds.Enabled { return "", ErrOAuthNotConfigured } return s.nc.StartExternalStorageOAuth2(ctx, ncUserID, creds.ClientID, creds.ClientSecret, redirectURI) } func (s *Service) CompleteMountOAuth(ctx context.Context, mountID, platformUserID, ncUserID, redirectURI, code string) error { if err := validateMountOAuthRedirectURI(redirectURI); err != nil { return err } store := s.ensureStore() if store == nil { return fmt.Errorf("store not configured") } code = strings.TrimSpace(code) if code == "" { return ErrInvalid } mount, err := store.GetMount(ctx, mountID) if err != nil { return err } if mount.OwnerUserID == nil || *mount.OwnerUserID != platformUserID { return ErrForbidden } if mount.NCMountID == nil { return ErrInvalid } providerKey, _, _, isOAuth := oauthProviderForBackend(mount.BackendType) if !isOAuth { return ErrInvalid } creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey) if err != nil { return err } if !creds.Enabled { return ErrOAuthNotConfigured } token, err := s.nc.CompleteExternalStorageOAuth2(ctx, ncUserID, creds.ClientID, creds.ClientSecret, redirectURI, code) if err != nil { _ = store.UpdateMountStatus(ctx, mount.ID, "error", err.Error(), mount.NCMountID) return err } if err := s.nc.UpdateUserExternalMountOAuth(ctx, ncUserID, *mount.NCMountID, creds.ClientID, creds.ClientSecret, token); err != nil { _ = store.UpdateMountStatus(ctx, mount.ID, "error", err.Error(), mount.NCMountID) return err } configEnc, _ := store.GetMountConfig(ctx, mount.ID) cfg := map[string]any{} if len(configEnc) > 0 { _ = json.Unmarshal(configEnc, &cfg) } cfg["token"] = token cfg["configured"] = true cfg["client_id"] = creds.ClientID cfg["client_secret"] = creds.ClientSecret if merged, err := json.Marshal(cfg); err == nil { _ = store.UpdateMountConfig(ctx, mount.ID, merged) } return store.UpdateMountStatus(ctx, mount.ID, "active", "", mount.NCMountID) }