package client import ( "context" "net/http" "net/url" "time" ) // Group is a node of the server's group tree as the client sees it. type Group struct { ID string `json:"id"` Name string `json:"name"` ParentID string `json:"parent_id"` Admin bool `json:"admin"` Members int `json:"members"` } // Invite is a registration link. type Invite struct { Token string `json:"token"` URL string `json:"url"` GroupID string `json:"group_id"` Role string `json:"role"` Uses int `json:"uses"` MaxUses int `json:"max_uses"` ExpiresAt time.Time `json:"expires_at"` } // User is an account as an administrator sees it. type User struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` ServerAdmin bool `json:"server_admin"` TOTPEnabled bool `json:"totp_enabled"` } // Identity is the answer to "who am I and where do I belong". type Identity struct { User Groups []struct { ID string `json:"id"` Name string `json:"name"` Role string `json:"role"` } `json:"groups"` } func (a *API) Whoami(ctx context.Context) (*Identity, error) { var out Identity return &out, a.do(ctx, http.MethodGet, "/api/v1/auth/whoami", nil, &out) } func (a *API) Groups(ctx context.Context) ([]Group, error) { var out struct { Groups []Group `json:"groups"` } err := a.do(ctx, http.MethodGet, "/api/v1/groups", nil, &out) return out.Groups, err } func (a *API) CreateGroup(ctx context.Context, name, parentID string) (*Group, error) { var out Group return &out, a.do(ctx, http.MethodPost, "/api/v1/groups", map[string]string{"name": name, "parent_id": parentID}, &out) } func (a *API) DeleteGroup(ctx context.Context, id string) error { return a.do(ctx, http.MethodDelete, "/api/v1/groups/"+url.PathEscape(id), nil, nil) } func (a *API) SetMember(ctx context.Context, groupID, username, role string) error { return a.do(ctx, http.MethodPost, "/api/v1/groups/"+url.PathEscape(groupID)+"/members", map[string]string{"username": username, "role": role}, nil) } func (a *API) RemoveMember(ctx context.Context, groupID, user string) error { return a.do(ctx, http.MethodDelete, "/api/v1/groups/"+url.PathEscape(groupID)+"/members/"+url.PathEscape(user), nil, nil) } func (a *API) CreateDatabase(ctx context.Context, name, groupID, strategy string) (*Database, error) { body := map[string]string{"name": name, "owner_group_id": groupID} if strategy != "" { body["merge_strategy"] = strategy } var out Database return &out, a.do(ctx, http.MethodPost, "/api/v1/databases", body, &out) } // SetStrategy changes the merge strategy of a database. Only an // administrator of the owning group may do this. func (a *API) SetStrategy(ctx context.Context, dbID, strategy string) (*Database, error) { var out Database return &out, a.do(ctx, http.MethodPatch, "/api/v1/databases/"+url.PathEscape(dbID), map[string]string{"merge_strategy": strategy}, &out) } func (a *API) RenameDatabase(ctx context.Context, dbID, name string) (*Database, error) { var out Database return &out, a.do(ctx, http.MethodPatch, "/api/v1/databases/"+url.PathEscape(dbID), map[string]string{"name": name}, &out) } // DeleteDatabase removes a database and every version of it stored on the // server. The caller's own account password is required, and the one-time // code too when that account has one enrolled; the server answers // totp_required when it is missing. func (a *API) DeleteDatabase(ctx context.Context, dbID, password, totp string) error { return a.do(ctx, http.MethodDelete, "/api/v1/databases/"+url.PathEscape(dbID), map[string]string{"password": password, "totp": totp}, nil) } func (a *API) SetGrant(ctx context.Context, groupID, dbID, perm string) error { return a.do(ctx, http.MethodPost, "/api/v1/grants", map[string]string{"group_id": groupID, "database_id": dbID, "perm": perm}, nil) } func (a *API) DeleteGrant(ctx context.Context, groupID, dbID string) error { q := url.Values{"group_id": {groupID}, "database_id": {dbID}} return a.do(ctx, http.MethodDelete, "/api/v1/grants?"+q.Encode(), nil, nil) } func (a *API) CreateInvite(ctx context.Context, groupID, role string, ttlHours, maxUses int) (*Invite, error) { var out Invite return &out, a.do(ctx, http.MethodPost, "/api/v1/invites", map[string]any{ "group_id": groupID, "role": role, "ttl_hours": ttlHours, "max_uses": maxUses}, &out) } func (a *API) Invites(ctx context.Context) ([]Invite, error) { var out struct { Invites []Invite `json:"invites"` } err := a.do(ctx, http.MethodGet, "/api/v1/invites", nil, &out) return out.Invites, err } func (a *API) Users(ctx context.Context) ([]User, error) { var out struct { Users []User `json:"users"` } err := a.do(ctx, http.MethodGet, "/api/v1/users", nil, &out) return out.Users, err } // ChangePassword replaces the account password. The server requires the // current one, and the second factor when one is enrolled, then signs out // this account's other sessions. func (a *API) ChangePassword(ctx context.Context, current, next, totp string) error { return a.do(ctx, http.MethodPost, "/api/v1/auth/password", map[string]string{ "current_password": current, "new_password": next, "totp": totp}, nil) } // EnrollTOTP starts second-factor enrolment and returns the shared secret // and its otpauth URI. It needs the account password, and the current code // when a second factor is already on and is being replaced. func (a *API) EnrollTOTP(ctx context.Context, password, code string) (secret, uri string, err error) { var out struct { Secret string `json:"secret"` URI string `json:"uri"` } err = a.do(ctx, http.MethodPost, "/api/v1/auth/totp/enroll", map[string]string{"password": password, "totp": code}, &out) return out.Secret, out.URI, err } // ConfirmTOTP completes enrolment by proving the secret was stored. func (a *API) ConfirmTOTP(ctx context.Context, code string) error { return a.do(ctx, http.MethodPost, "/api/v1/auth/totp/confirm", map[string]string{"code": code}, nil) } // CreateVault asks the server to create a new, empty KDBX vault encrypted // with this password. For the merge strategy the server keeps the password, // wrapped under its master key, because merging means opening the file. func (a *API) CreateVault(ctx context.Context, name, groupID, strategy, password string) (*Database, error) { var out Database return &out, a.do(ctx, http.MethodPost, "/api/v1/vaults", map[string]string{ "name": name, "owner_group_id": groupID, "merge_strategy": strategy, "password": password, }, &out) } // StoreVaultKey gives the server the password of an existing database, so it // can merge it. The server verifies the password against the stored file // before accepting it. func (a *API) StoreVaultKey(ctx context.Context, dbID, password string) error { return a.do(ctx, http.MethodPost, "/api/v1/databases/"+url.PathEscape(dbID)+"/key", map[string]any{"password": password}, nil) } // ForgetVaultKey removes the stored password, which also ends any ability to // merge that database here. func (a *API) ForgetVaultKey(ctx context.Context, dbID string) error { return a.do(ctx, http.MethodPost, "/api/v1/databases/"+url.PathEscape(dbID)+"/key", map[string]any{"forget": true}, nil) }