feat(oo): project team CRUD and user lifecycle #73
+2
-2
@@ -3,9 +3,9 @@
|
|||||||
// Command tree is subject-based (mirrors the library split and the `tea` CLI):
|
// Command tree is subject-based (mirrors the library split and the `tea` CLI):
|
||||||
//
|
//
|
||||||
// oo calendar list | events | add | delete
|
// oo calendar list | events | add | delete
|
||||||
// oo projects list | get | milestones | milestone-create | create | update | delete | contacts (add|remove) | link-authors | link-git | files (list|upload|download|rename|delete|dedupe|as-md|put-md|put-txt|put-xlsx)
|
// oo projects list | get | milestones | milestone-create | create | update | delete | contacts (add|remove) | team (list|add|remove|set) | link-authors | link-git | files (list|upload|download|rename|delete|dedupe|as-md|put-md|put-txt|put-xlsx)
|
||||||
// oo tasks list | get | create | update | delete | subtask add | files (list|upload|detach)
|
// oo tasks list | get | create | update | delete | subtask add | files (list|upload|detach)
|
||||||
// oo users list | self (alias: oo whoami)
|
// oo users list | self | get | create | update | delete | block | unblock | password (alias: oo whoami)
|
||||||
// oo contacts list | get | delete | info-add | merge | dedupe-info | tags | tag-add | tag-create | tag-remove
|
// oo contacts list | get | delete | info-add | merge | dedupe-info | tags | tag-add | tag-create | tag-remove
|
||||||
// oo persons list | create | delete | dedupe
|
// oo persons list | create | delete | dedupe
|
||||||
// oo companies list | create | delete | dedupe | dedupe-persons
|
// oo companies list | create | delete | dedupe | dedupe-persons
|
||||||
|
|||||||
@@ -28,6 +28,137 @@ func init() {
|
|||||||
projectsCmd.AddCommand(prjUpdateCmd())
|
projectsCmd.AddCommand(prjUpdateCmd())
|
||||||
projectsCmd.AddCommand(prjDeleteCmd())
|
projectsCmd.AddCommand(prjDeleteCmd())
|
||||||
projectsCmd.AddCommand(prjContactsCmd())
|
projectsCmd.AddCommand(prjContactsCmd())
|
||||||
|
projectsCmd.AddCommand(prjTeamCmd())
|
||||||
|
}
|
||||||
|
|
||||||
|
func prjTeamCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "team",
|
||||||
|
Short: "Project team (portal users) — CRUD",
|
||||||
|
Long: `Project team members are portal users (People), not CRM contacts.
|
||||||
|
CRM companies/persons linked to a project live under 'oo projects contacts'.`,
|
||||||
|
}
|
||||||
|
cmd.AddCommand(prjTeamListCmd())
|
||||||
|
cmd.AddCommand(prjTeamAddCmd())
|
||||||
|
cmd.AddCommand(prjTeamRemoveCmd())
|
||||||
|
cmd.AddCommand(prjTeamSetCmd())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func prjTeamListCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list PROJECT_ID",
|
||||||
|
Short: "List portal users on the project team",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
pid, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("project id: %w", err)
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
list, err := c.ListProjectTeam(cmd.Context(), pid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printTable([]string{"id", "displayName", "userName", "email", "isAdmin"}, teamRows(list))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prjTeamAddCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "add PROJECT_ID USER_ID [USER_ID...]",
|
||||||
|
Short: "Add portal user(s) to the project team",
|
||||||
|
Args: cobra.MinimumNArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
pid, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("project id: %w", err)
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, uid := range args[1:] {
|
||||||
|
if _, err := c.AddProjectTeamUser(cmd.Context(), pid, uid); err != nil {
|
||||||
|
return fmt.Errorf("add %s: %w", uid, err)
|
||||||
|
}
|
||||||
|
printObject(map[string]any{"project_id": pid, "user_id": uid, "added": true})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prjTeamRemoveCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "remove PROJECT_ID USER_ID [USER_ID...]",
|
||||||
|
Short: "Remove portal user(s) from the project team",
|
||||||
|
Args: cobra.MinimumNArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
pid, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("project id: %w", err)
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, uid := range args[1:] {
|
||||||
|
if _, err := c.RemoveProjectTeamUser(cmd.Context(), pid, uid); err != nil {
|
||||||
|
return fmt.Errorf("remove %s: %w", uid, err)
|
||||||
|
}
|
||||||
|
printObject(map[string]any{"project_id": pid, "user_id": uid, "removed": true})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prjTeamSetCmd() *cobra.Command {
|
||||||
|
var notify bool
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "set PROJECT_ID USER_ID [USER_ID...]",
|
||||||
|
Short: "Replace the project team with the given users (register several at once)",
|
||||||
|
Args: cobra.MinimumNArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
pid, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("project id: %w", err)
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
team, err := c.SetProjectTeam(cmd.Context(), pid, args[1:], notify)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printTable([]string{"id", "displayName", "userName", "email", "isAdmin"}, teamRows(team))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().BoolVar(¬ify, "notify", false, "notify added members")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// teamRows maps raw team member maps into table rows.
|
||||||
|
func teamRows(list []map[string]any) []map[string]any {
|
||||||
|
rows := make([]map[string]any, 0, len(list))
|
||||||
|
for _, m := range list {
|
||||||
|
rows = append(rows, map[string]any{
|
||||||
|
"id": idString(m, "id"),
|
||||||
|
"displayName": m["displayName"],
|
||||||
|
"userName": m["userName"],
|
||||||
|
"email": m["email"],
|
||||||
|
"isAdmin": m["isAdmin"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
func prjListCmd() *cobra.Command {
|
func prjListCmd() *cobra.Command {
|
||||||
|
|||||||
+259
@@ -1,6 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +19,13 @@ func init() {
|
|||||||
rootCmd.AddCommand(usersCmd)
|
rootCmd.AddCommand(usersCmd)
|
||||||
usersCmd.AddCommand(usersListCmd())
|
usersCmd.AddCommand(usersListCmd())
|
||||||
usersCmd.AddCommand(usersSelfCmd())
|
usersCmd.AddCommand(usersSelfCmd())
|
||||||
|
usersCmd.AddCommand(usersGetCmd())
|
||||||
|
usersCmd.AddCommand(usersCreateCmd())
|
||||||
|
usersCmd.AddCommand(usersUpdateCmd())
|
||||||
|
usersCmd.AddCommand(usersDeleteCmd())
|
||||||
|
usersCmd.AddCommand(usersBlockCmd())
|
||||||
|
usersCmd.AddCommand(usersUnblockCmd())
|
||||||
|
usersCmd.AddCommand(usersPasswordCmd())
|
||||||
rootCmd.AddCommand(whoamiCmd())
|
rootCmd.AddCommand(whoamiCmd())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +78,252 @@ func usersSelfCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func usersGetCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "get USER_ID",
|
||||||
|
Short: "Show one portal user profile",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u, err := c.GetUser(cmd.Context(), args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printObject(u)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersCreateCmd() *cobra.Command {
|
||||||
|
var first, last, email, password, title, location, sex, comment string
|
||||||
|
var visitor bool
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create a portal user",
|
||||||
|
Long: `Create a portal user (POST /api/2.0/people).
|
||||||
|
|
||||||
|
Without --password the portal generates one and the account stays NotActivated
|
||||||
|
until the user follows the activation link. With --password the account is
|
||||||
|
Active immediately. Use --visitor for a guest account.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if email == "" || first == "" || last == "" {
|
||||||
|
return fmt.Errorf("--email, --first and --last are required")
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req := onlyoffice.NewUserRequest{
|
||||||
|
FirstName: first,
|
||||||
|
LastName: last,
|
||||||
|
Email: email,
|
||||||
|
Password: password,
|
||||||
|
Title: title,
|
||||||
|
Location: location,
|
||||||
|
Sex: sex,
|
||||||
|
Comment: comment,
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("visitor") {
|
||||||
|
req.IsVisitor = &visitor
|
||||||
|
}
|
||||||
|
u, err := c.CreateUser(cmd.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printObject(map[string]any{
|
||||||
|
"id": idString(u, "id"),
|
||||||
|
"displayName": idString(u, "displayName"),
|
||||||
|
"email": idString(u, "email"),
|
||||||
|
"status": u["status"],
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVar(&first, "first", "", "first name (required)")
|
||||||
|
cmd.Flags().StringVar(&last, "last", "", "last name (required)")
|
||||||
|
cmd.Flags().StringVar(&email, "email", "", "email (required)")
|
||||||
|
cmd.Flags().StringVar(&password, "password", "", "initial password (default: portal-generated)")
|
||||||
|
cmd.Flags().StringVar(&title, "title", "", "job title")
|
||||||
|
cmd.Flags().StringVar(&location, "location", "", "location")
|
||||||
|
cmd.Flags().StringVar(&sex, "sex", "", "sex: male|female")
|
||||||
|
cmd.Flags().StringVar(&comment, "comment", "", "comment")
|
||||||
|
cmd.Flags().BoolVar(&visitor, "visitor", false, "create as guest (isVisitor=true)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersUpdateCmd() *cobra.Command {
|
||||||
|
var first, last, email, title, location, sex, comment string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update USER_ID",
|
||||||
|
Short: "Update portal user profile fields (only flags passed)",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
body := map[string]any{}
|
||||||
|
if cmd.Flags().Changed("first") {
|
||||||
|
body["firstname"] = first
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("last") {
|
||||||
|
body["lastname"] = last
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("email") {
|
||||||
|
body["email"] = email
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("title") {
|
||||||
|
body["title"] = title
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("location") {
|
||||||
|
body["location"] = location
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("sex") {
|
||||||
|
body["sex"] = sex
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("comment") {
|
||||||
|
body["comment"] = comment
|
||||||
|
}
|
||||||
|
if len(body) == 0 {
|
||||||
|
return fmt.Errorf("nothing to update: pass at least one of --first/--last/--email/--title/--location/--sex/--comment")
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u, err := c.UpdateUser(cmd.Context(), args[0], body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printObject(map[string]any{
|
||||||
|
"id": idString(u, "id"),
|
||||||
|
"displayName": idString(u, "displayName"),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVar(&first, "first", "", "first name")
|
||||||
|
cmd.Flags().StringVar(&last, "last", "", "last name")
|
||||||
|
cmd.Flags().StringVar(&email, "email", "", "email")
|
||||||
|
cmd.Flags().StringVar(&title, "title", "", "job title")
|
||||||
|
cmd.Flags().StringVar(&location, "location", "", "location")
|
||||||
|
cmd.Flags().StringVar(&sex, "sex", "", "sex: male|female")
|
||||||
|
cmd.Flags().StringVar(&comment, "comment", "", "comment")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersDeleteCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "delete USER_ID [USER_ID...]",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Short: "Delete portal user(s) permanently",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, id := range args {
|
||||||
|
u, err := c.DeleteUser(cmd.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete %s: %w", id, err)
|
||||||
|
}
|
||||||
|
printObject(map[string]any{"id": id, "deleted": true, "displayName": idString(u, "displayName")})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersBlockCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "block USER_ID [USER_ID...]",
|
||||||
|
Aliases: []string{"disable"},
|
||||||
|
Short: "Block (terminate) user(s): login denied, profile kept",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, id := range args {
|
||||||
|
if err := c.BlockUser(cmd.Context(), id); err != nil {
|
||||||
|
return fmt.Errorf("block %s: %w", id, err)
|
||||||
|
}
|
||||||
|
printObject(map[string]any{"id": id, "blocked": true})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersUnblockCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "unblock USER_ID [USER_ID...]",
|
||||||
|
Aliases: []string{"enable", "activate"},
|
||||||
|
Short: "Unblock (activate) user(s)",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, id := range args {
|
||||||
|
if err := c.UnblockUser(cmd.Context(), id); err != nil {
|
||||||
|
return fmt.Errorf("unblock %s: %w", id, err)
|
||||||
|
}
|
||||||
|
printObject(map[string]any{"id": id, "unblocked": true})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersPasswordCmd() *cobra.Command {
|
||||||
|
var password string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "password USER_ID",
|
||||||
|
Short: "Set a user password (reads stdin when --password is empty)",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
pwd := password
|
||||||
|
if pwd == "" {
|
||||||
|
b, err := readLine(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read password: %w", err)
|
||||||
|
}
|
||||||
|
pwd = b
|
||||||
|
}
|
||||||
|
if pwd == "" {
|
||||||
|
return fmt.Errorf("password is empty")
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.ChangeUserPassword(cmd.Context(), args[0], pwd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printObject(map[string]any{"id": args[0], "password_changed": true})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVar(&password, "password", "", "new password (omit to read one line from stdin)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLine reads a single trimmed line from r.
|
||||||
|
func readLine(r *os.File) (string, error) {
|
||||||
|
sc := bufio.NewScanner(r)
|
||||||
|
if !sc.Scan() {
|
||||||
|
if err := sc.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(sc.Text()), nil
|
||||||
|
}
|
||||||
|
|
||||||
// whoamiCmd is a convenience shortcut at the root level.
|
// whoamiCmd is a convenience shortcut at the root level.
|
||||||
func whoamiCmd() *cobra.Command {
|
func whoamiCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
|
|||||||
@@ -111,6 +111,25 @@ func (c *Client) deleteObject(ctx context.Context, path string) (map[string]any,
|
|||||||
return unmarshalResponseObject(raw)
|
return unmarshalResponseObject(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unmarshalResponseArray extracts the "response" field from a raw OnlyOffice
|
||||||
|
// envelope and decodes it into a list of maps. Returns (nil, nil) for a null,
|
||||||
|
// empty or scalar payload. Companion to unmarshalResponseObject for endpoints
|
||||||
|
// whose response is a list (project team, people/status, …).
|
||||||
|
func unmarshalResponseArray(raw json.RawMessage) ([]map[string]any, error) {
|
||||||
|
resp, err := responseField(raw, "response")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(resp) == 0 || string(resp) == "null" || resp[0] != '[' {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var list []map[string]any
|
||||||
|
if err := json.Unmarshal(resp, &list); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
// unmarshalResponseObject extracts the "response" field from a raw OnlyOffice
|
// unmarshalResponseObject extracts the "response" field from a raw OnlyOffice
|
||||||
// envelope and decodes it into map[string]any. Returns (nil, nil) for a null
|
// envelope and decodes it into map[string]any. Returns (nil, nil) for a null
|
||||||
// response, an empty array, or scalar payloads. When the API returns a list
|
// response, an empty array, or scalar payloads. When the API returns a list
|
||||||
@@ -258,27 +277,62 @@ func (c *Client) postJSONObject(ctx context.Context, path string, body any) (map
|
|||||||
return unmarshalResponseObject(raw)
|
return unmarshalResponseObject(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsonBodyReader turns a request body value into an io.Reader. nil becomes
|
||||||
|
// "{}", []byte/string pass through, anything else is JSON-marshalled.
|
||||||
|
func jsonBodyReader(body any) (io.Reader, error) {
|
||||||
|
switch b := body.(type) {
|
||||||
|
case nil:
|
||||||
|
return strings.NewReader("{}"), nil
|
||||||
|
case []byte:
|
||||||
|
return bytes.NewReader(b), nil
|
||||||
|
case string:
|
||||||
|
return strings.NewReader(b), nil
|
||||||
|
default:
|
||||||
|
buf, err := json.Marshal(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes.NewReader(buf), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// postJSONArray is postJSON + unmarshalResponseArray.
|
||||||
|
func (c *Client) postJSONArray(ctx context.Context, path string, body any) ([]map[string]any, error) {
|
||||||
|
raw, err := c.postJSON(ctx, path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return unmarshalResponseArray(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// putJSONArray is putJSON + unmarshalResponseArray.
|
||||||
|
func (c *Client) putJSONArray(ctx context.Context, path string, body any) ([]map[string]any, error) {
|
||||||
|
raw, err := c.putJSON(ctx, path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return unmarshalResponseArray(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteJSONArray is deleteJSON + unmarshalResponseArray.
|
||||||
|
func (c *Client) deleteJSONArray(ctx context.Context, path string, body any) ([]map[string]any, error) {
|
||||||
|
raw, err := c.deleteJSON(ctx, path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return unmarshalResponseArray(raw)
|
||||||
|
}
|
||||||
|
|
||||||
// postJSON issues an authenticated POST with application/json body.
|
// postJSON issues an authenticated POST with application/json body.
|
||||||
func (c *Client) postJSON(ctx context.Context, path string, body any) (json.RawMessage, error) {
|
func (c *Client) postJSON(ctx context.Context, path string, body any) (json.RawMessage, error) {
|
||||||
auth, err := c.authHeader()
|
auth, err := c.authHeader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var rdr io.Reader
|
rdr, err := jsonBodyReader(body)
|
||||||
switch b := body.(type) {
|
|
||||||
case nil:
|
|
||||||
rdr = strings.NewReader("{}")
|
|
||||||
case []byte:
|
|
||||||
rdr = bytes.NewReader(b)
|
|
||||||
case string:
|
|
||||||
rdr = strings.NewReader(b)
|
|
||||||
default:
|
|
||||||
buf, err := json.Marshal(b)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rdr = bytes.NewReader(buf)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL()+path, rdr)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL()+path, rdr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -307,21 +361,10 @@ func (c *Client) putJSON(ctx context.Context, path string, body any) (json.RawMe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var rdr io.Reader
|
rdr, err := jsonBodyReader(body)
|
||||||
switch b := body.(type) {
|
|
||||||
case nil:
|
|
||||||
rdr = strings.NewReader("{}")
|
|
||||||
case []byte:
|
|
||||||
rdr = bytes.NewReader(b)
|
|
||||||
case string:
|
|
||||||
rdr = strings.NewReader(b)
|
|
||||||
default:
|
|
||||||
buf, err := json.Marshal(b)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rdr = bytes.NewReader(buf)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseURL()+path, rdr)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseURL()+path, rdr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -48,3 +48,26 @@ func TestUnmarshalResponseObjectNull(t *testing.T) {
|
|||||||
t.Fatalf("expected nil, got %#v", out)
|
t.Fatalf("expected nil, got %#v", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalResponseArrayList(t *testing.T) {
|
||||||
|
raw := json.RawMessage(`{"response":[{"id":"a","displayName":"A"},{"id":"b","displayName":"B"}]}`)
|
||||||
|
out, err := unmarshalResponseArray(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(out) != 2 || out[0]["id"] != "a" || out[1]["displayName"] != "B" {
|
||||||
|
t.Fatalf("unexpected list: %#v", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalResponseArrayNullAndScalar(t *testing.T) {
|
||||||
|
for _, raw := range []string{`{"response":null}`, `{"response":{}}`, `{"response":"x"}`} {
|
||||||
|
out, err := unmarshalResponseArray(json.RawMessage(raw))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", raw, err)
|
||||||
|
}
|
||||||
|
if out != nil {
|
||||||
|
t.Fatalf("%s: expected nil, got %#v", raw, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package onlyoffice
|
||||||
|
|
||||||
|
// Project team (portal users) CRUD. Team is distinct from CRM contacts,
|
||||||
|
// which are linked through project_contacts.go.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListProjectTeam returns portal users on the project team.
|
||||||
|
// GET /api/2.0/project/{projectid}/team
|
||||||
|
func (c *Client) ListProjectTeam(ctx context.Context, projectID int) ([]map[string]any, error) {
|
||||||
|
return c.GetProjectTeam(ctx, projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddProjectTeamUser adds a portal user to the project team.
|
||||||
|
// POST /api/2.0/project/{projectid}/team with {"userId": "..."}.
|
||||||
|
// Returns the resulting team list.
|
||||||
|
func (c *Client) AddProjectTeamUser(ctx context.Context, projectID int, userID string) ([]map[string]any, error) {
|
||||||
|
return c.postJSONArray(ctx, fmt.Sprintf("/api/2.0/project/%d/team", projectID),
|
||||||
|
map[string]any{"userId": userID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveProjectTeamUser removes a portal user from the project team.
|
||||||
|
// DELETE /api/2.0/project/{projectid}/team with {"userId": "..."}.
|
||||||
|
// Returns the resulting team list.
|
||||||
|
func (c *Client) RemoveProjectTeamUser(ctx context.Context, projectID int, userID string) ([]map[string]any, error) {
|
||||||
|
return c.deleteJSONArray(ctx, fmt.Sprintf("/api/2.0/project/%d/team", projectID),
|
||||||
|
map[string]any{"userId": userID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProjectTeam replaces the project team with the given user IDs.
|
||||||
|
// PUT /api/2.0/project/{projectid}/team with participants + notify.
|
||||||
|
// This is the canonical way to register several members at once.
|
||||||
|
func (c *Client) SetProjectTeam(ctx context.Context, projectID int, participants []string, notify bool) ([]map[string]any, error) {
|
||||||
|
return c.putJSONArray(ctx, fmt.Sprintf("/api/2.0/project/%d/team", projectID),
|
||||||
|
map[string]any{"projectId": projectID, "participants": participants, "notify": notify})
|
||||||
|
}
|
||||||
@@ -106,6 +106,53 @@ func (c *Client) ChangeUserPassword(ctx context.Context, userID, password string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewUserRequest is the payload for CreateUser. Field names follow the
|
||||||
|
// OnlyOffice REST contract (lowercase firstname/lastname).
|
||||||
|
type NewUserRequest struct {
|
||||||
|
FirstName string `json:"firstname"`
|
||||||
|
LastName string `json:"lastname"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
|
Sex string `json:"sex,omitempty"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
IsVisitor *bool `json:"isVisitor,omitempty"`
|
||||||
|
Department []string `json:"department,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser adds a portal user (POST /api/2.0/people). Returns the created
|
||||||
|
// user profile. When Password is empty the portal generates one and the account
|
||||||
|
// stays NotActivated until the user follows the activation link.
|
||||||
|
func (c *Client) CreateUser(ctx context.Context, req NewUserRequest) (map[string]any, error) {
|
||||||
|
return c.postJSONObject(ctx, "/api/2.0/people", req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser removes a portal user permanently (DELETE /api/2.0/people/{id}).
|
||||||
|
// OnlyOffice refuses to delete an active user ("The user is not suspended"),
|
||||||
|
// so a blocked/terminated account is deactivated first and deletion retried.
|
||||||
|
func (c *Client) DeleteUser(ctx context.Context, userID string) (map[string]any, error) {
|
||||||
|
u, err := c.deleteObject(ctx, fmt.Sprintf("/api/2.0/people/%s", url.PathEscape(userID)))
|
||||||
|
if err == nil {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
if bErr := c.BlockUser(ctx, userID); bErr != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.deleteObject(ctx, fmt.Sprintf("/api/2.0/people/%s", url.PathEscape(userID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockUser terminates (blocks) the user: login is denied but the profile and
|
||||||
|
// data are kept. Reversible with UnblockUser.
|
||||||
|
func (c *Client) BlockUser(ctx context.Context, userID string) error {
|
||||||
|
return c.ChangeUserStatus(ctx, userID, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnblockUser reactivates a terminated/blocked user.
|
||||||
|
func (c *Client) UnblockUser(ctx context.Context, userID string) error {
|
||||||
|
return c.ChangeUserStatus(ctx, userID, true)
|
||||||
|
}
|
||||||
|
|
||||||
// SelfUserID returns the ID of the authenticated user (people/@self), cached.
|
// SelfUserID returns the ID of the authenticated user (people/@self), cached.
|
||||||
func (c *Client) SelfUserID(ctx context.Context) (string, error) {
|
func (c *Client) SelfUserID(ctx context.Context) (string, error) {
|
||||||
if c.selfID != "" {
|
if c.selfID != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user