mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Custom profile attributes field endpoints (#29662)
* Adds the main Property System Architecture components
This change adds the necessary migrations for the Property Groups,
Fields and Values tables to be created, the store layer and a Property
Service that can be used from the app layer.
* Adds Custom Profile Attributes endpoints and app layer
* implement get and patch cpa values
* run i18n-extract
* Update property field type to use user instead of person
* Update PropertyFields to allow for unique nondeleted fields and remove redundant indexes
* Update PropertyValues to allow for unique nondeleted fields and remove redundant indexes
* Use StringMap instead of the map[string]any on property fields
* Add i18n strings
* Revert "Use StringMap instead of the map[string]any on property fields"
This reverts commit e2735ab0f8.
* Cast JSON binary data to string and add todo note for StringMap use
* Add mocks to the retrylayer tests
* Cast JSON binary data to string in property value store
* Check for binary parameter instead of casting to string for JSON data
* Fix bad merge
* Check property field type is one of the allowed ones
* Avoid reusing err variable to be explicit about the returned value
* Merge Property System Migrations into one file
* Adds NOT NULL to timestamps at the DB level
* Update stores to use tableSelectQuery instead of a slice var
* Update PropertyField model translations to be more explicit and avoid repetition
* Update PropertyValue model translations to be more explicit and avoid repetition
* Use ExecBuilder instead of ToSql&Exec
* Update property field errors to add context
* Ensure PerPage is greater than zero
* Update store errors to give more context
* Use ExecBuilder in the property stores where possible
* Add an on conflict suffix to the group register to avoid race conditions
* Remove user profile API documentation changes
* Update patchCPAValues endpoint and docs to return the updated information
* Merge two similar error conditions
* Use a route function for ListCPAValues
* Remove badly used translation string
* Remove unused get in register group method
* Adds input sanitization and validation to the CPA API endpoints
* Takes login outside of one test case to make it clear it affects multiple t.Runs
* Fix wrap error and return code when property field has been deleted
* Fix receiver name
* Adds comment to move the CPA group ID to the db cache
* Set the PerPage of CPA fields to the fields limit
* Update server/channels/app/custom_profile_attributes_test.go
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Standardize group ID access
* Avoid polluting the state between tests
* Use specific errors for the retrieval of CPA group
---------
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
This commit is contained in:
committed by
GitHub
parent
c6984941f1
commit
ca34c6a03f
@@ -58,6 +58,7 @@ build-v4: node_modules playbooks
|
||||
@cat $(V4_SRC)/outgoing_oauth_connections.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/metrics.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/scheduled_post.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/custom_profile_attributes.yaml >> $(V4_YAML)
|
||||
@if [ -r $(PLAYBOOKS_SRC)/paths.yaml ]; then cat $(PLAYBOOKS_SRC)/paths.yaml >> $(V4_YAML); fi
|
||||
@if [ -r $(PLAYBOOKS_SRC)/merged-definitions.yaml ]; then cat $(PLAYBOOKS_SRC)/merged-definitions.yaml >> $(V4_YAML); else cat $(V4_SRC)/definitions.yaml >> $(V4_YAML); fi
|
||||
@echo Extracting code samples
|
||||
|
||||
257
api/v4/source/custom_profile_attributes.yaml
Normal file
257
api/v4/source/custom_profile_attributes.yaml
Normal file
@@ -0,0 +1,257 @@
|
||||
"/api/v4/custom_profile_attributes/fields":
|
||||
get:
|
||||
tags:
|
||||
- custom profile attributes
|
||||
summary: List all the Custom Profile Attributes fields
|
||||
description: |
|
||||
List all the Custom Profile Attributes fields.
|
||||
|
||||
_This endpoint is experimental._
|
||||
|
||||
__Minimum server version__: 10.5
|
||||
|
||||
##### Permissions
|
||||
Must be authenticated.
|
||||
operationId: ListAllCPAFields
|
||||
responses:
|
||||
"200":
|
||||
description: Custom Profile Attributes fetch successful. Result may be empty.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PropertyField"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
post:
|
||||
tags:
|
||||
- custom profile attributes
|
||||
summary: Create a Custom Profile Attribute field
|
||||
description: |
|
||||
Create a new Custom Profile Attribute field on the system.
|
||||
|
||||
_This endpoint is experimental._
|
||||
|
||||
__Minimum server version__: 10.5
|
||||
|
||||
##### Permissions
|
||||
Must have `manage_system` permission.
|
||||
operationId: CreateCPAField
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
attrs:
|
||||
type: string
|
||||
responses:
|
||||
"201":
|
||||
description: Custom Profile Attribute field creation successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PropertyField"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
"/api/v4/custom_profile_attributes/fields/{field_id}":
|
||||
patch:
|
||||
tags:
|
||||
- custom profile attributes
|
||||
summary: Patch a Custom Profile Attribute field
|
||||
description: |
|
||||
Partially update a Custom Profile Attribute field by providing
|
||||
only the fields you want to update. Omitted fields will not be
|
||||
updated. The fields that can be updated are defined in the
|
||||
request body, all other provided fields will be ignored.
|
||||
|
||||
_This endpoint is experimental._
|
||||
|
||||
__Minimum server version__: 10.5
|
||||
|
||||
##### Permissions
|
||||
Must have `manage_system` permission.
|
||||
operationId: PatchCPAField
|
||||
parameters:
|
||||
- name: field_id
|
||||
in: path
|
||||
description: Custom Profile Attribute field GUID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: Custom Profile Attribute field that is to be updated
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
attrs:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Custom Profile Attribute field patch successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PropertyField"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
delete:
|
||||
tags:
|
||||
- custom profile attributes
|
||||
summary: Delete a Custom Profile Attribute field
|
||||
description: |
|
||||
Marks a Custom Profile Attribute field and all its values as
|
||||
deleted.
|
||||
|
||||
_This endpoint is experimental._
|
||||
|
||||
__Minimum server version__: 10.5
|
||||
|
||||
##### Permissions
|
||||
Must have `manage_system` permission.
|
||||
operationId: DeleteCPAField
|
||||
parameters:
|
||||
- name: field_id
|
||||
in: path
|
||||
description: Custom Profile Attribute field GUID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Custom Profile Attribute field deletion successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StatusOK"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
"/api/v4/custom_profile_attributes/values":
|
||||
patch:
|
||||
tags:
|
||||
- custom profile attributes
|
||||
summary: Patch Custom Profile Attribute values
|
||||
description: |
|
||||
Partially update a set of values on the requester's Custom
|
||||
Profile Attribute fields by providing only the information you
|
||||
want to update. Omitted fields will not be updated. The fields
|
||||
that can be updated are defined in the request body, all other
|
||||
provided fields will be ignored.
|
||||
|
||||
_This endpoint is experimental._
|
||||
|
||||
__Minimum server version__: 10.5
|
||||
|
||||
##### Permissions
|
||||
Must be authenticated.
|
||||
operationId: PatchCPAValues
|
||||
requestBody:
|
||||
description: Custom Profile Attribute values that are to be updated
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Custom Profile Attribute values patch successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"/api/v4/users/{user_id}/custom_profile_attributes":
|
||||
get:
|
||||
tags:
|
||||
- custom profile attributes
|
||||
summary: List Custom Profile Attribute values
|
||||
description: |
|
||||
List all the Custom Profile Attributes values for specified user.
|
||||
|
||||
_This endpoint is experimental._
|
||||
|
||||
__Minimum server version__: 10.5
|
||||
|
||||
##### Permissions
|
||||
Must have `view members` permission.
|
||||
operationId: ListCPAValues
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
description: User GUID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Custom Profile Attribute values fetch successful. Result may be empty.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
field_id:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
|
||||
|
||||
@@ -404,6 +404,69 @@ components:
|
||||
type: string
|
||||
metadata:
|
||||
$ref: "#/components/schemas/PostMetadata"
|
||||
PropertyField:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
group_id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
attrs:
|
||||
type: object
|
||||
target_id:
|
||||
type: string
|
||||
target_type:
|
||||
type: string
|
||||
create_at:
|
||||
type: integer
|
||||
format: int64
|
||||
update_at:
|
||||
type: integer
|
||||
format: int64
|
||||
delete_at:
|
||||
type: integer
|
||||
format: int64
|
||||
PropertyFieldPatch:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
attrs:
|
||||
type: object
|
||||
target_id:
|
||||
type: string
|
||||
target_type:
|
||||
type: string
|
||||
PropertyValue:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
target_id:
|
||||
type: string
|
||||
target_type:
|
||||
type: string
|
||||
group_id:
|
||||
type: string
|
||||
field_id:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
create_at:
|
||||
type: integer
|
||||
format: int64
|
||||
update_at:
|
||||
type: integer
|
||||
format: int64
|
||||
delete_at:
|
||||
type: integer
|
||||
format: int64
|
||||
FileInfoList:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -614,6 +614,7 @@ x-tagGroups:
|
||||
- exports
|
||||
- usage
|
||||
- reports
|
||||
- custom profile attributes
|
||||
servers:
|
||||
- url: http://your-mattermost-url.com
|
||||
- url: https://your-mattermost-url.com
|
||||
|
||||
@@ -756,6 +756,12 @@
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: cpa
|
||||
in: query
|
||||
description: Includes the Custom Profile Attributes information if set to true.
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: User retrieval successful
|
||||
|
||||
@@ -151,6 +151,11 @@ type Routes struct {
|
||||
|
||||
OutgoingOAuthConnections *mux.Router // 'api/v4/oauth/outgoing_connections'
|
||||
OutgoingOAuthConnection *mux.Router // 'api/v4/oauth/outgoing_connections/{outgoing_oauth_connection_id:[A-Za-z0-9]+}'
|
||||
|
||||
CustomProfileAttributes *mux.Router // 'api/v4/custom_profile_attributes'
|
||||
CustomProfileAttributesFields *mux.Router // 'api/v4/custom_profile_attributes/fields'
|
||||
CustomProfileAttributesField *mux.Router // 'api/v4/custom_profile_attributes/fields/{field_id:[A-Za-z0-9]+}'
|
||||
CustomProfileAttributesValues *mux.Router // 'api/v4/custom_profile_attributes/values'
|
||||
}
|
||||
|
||||
type API struct {
|
||||
@@ -288,6 +293,11 @@ func Init(srv *app.Server) (*API, error) {
|
||||
api.BaseRoutes.OutgoingOAuthConnections = api.BaseRoutes.APIRoot.PathPrefix("/oauth/outgoing_connections").Subrouter()
|
||||
api.BaseRoutes.OutgoingOAuthConnection = api.BaseRoutes.OutgoingOAuthConnections.PathPrefix("/{outgoing_oauth_connection_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.CustomProfileAttributes = api.BaseRoutes.APIRoot.PathPrefix("/custom_profile_attributes").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesFields = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/fields").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
|
||||
|
||||
api.InitUser()
|
||||
api.InitBot()
|
||||
api.InitTeam()
|
||||
@@ -338,6 +348,7 @@ func Init(srv *app.Server) (*API, error) {
|
||||
api.InitOutgoingOAuthConnection()
|
||||
api.InitClientPerformanceMetrics()
|
||||
api.InitScheduledPost()
|
||||
api.InitCustomProfileAttributes()
|
||||
|
||||
// If we allow testing then listen for manual testing URL hits
|
||||
if *srv.Config().ServiceSettings.EnableTesting {
|
||||
@@ -420,6 +431,11 @@ func InitLocal(srv *app.Server) *API {
|
||||
|
||||
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
|
||||
|
||||
api.BaseRoutes.CustomProfileAttributes = api.BaseRoutes.APIRoot.PathPrefix("/custom_profile_attributes").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesFields = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/fields").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
|
||||
api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
|
||||
|
||||
api.InitUserLocal()
|
||||
api.InitTeamLocal()
|
||||
api.InitChannelLocal()
|
||||
@@ -440,6 +456,7 @@ func InitLocal(srv *app.Server) *API {
|
||||
api.InitExportLocal()
|
||||
api.InitJobLocal()
|
||||
api.InitSamlLocal()
|
||||
api.InitCustomProfileAttributesLocal()
|
||||
|
||||
srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
|
||||
|
||||
|
||||
218
server/channels/api4/custom_profile_attributes.go
Normal file
218
server/channels/api4/custom_profile_attributes.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
||||
)
|
||||
|
||||
func (api *API) InitCustomProfileAttributes() {
|
||||
if api.srv.Config().FeatureFlags.CustomProfileAttributes {
|
||||
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(listCPAFields)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(createCPAField)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(patchCPAField)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(deleteCPAField)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
|
||||
}
|
||||
}
|
||||
|
||||
func listCPAFields(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
fields, appErr := c.App.ListCPAFields()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(fields); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func createCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
var pf *model.PropertyField
|
||||
err := json.NewDecoder(r.Body).Decode(&pf)
|
||||
if err != nil || pf == nil {
|
||||
c.SetInvalidParamWithErr("property_field", err)
|
||||
return
|
||||
}
|
||||
|
||||
pf.SanitizeInput()
|
||||
|
||||
auditRec := c.MakeAuditRecord("createCPAField", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "property_field", pf)
|
||||
|
||||
createdField, appErr := c.App.CreateCPAField(pf)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(createdField)
|
||||
auditRec.AddEventObjectType("property_field")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(createdField); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func patchCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireFieldId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var patch *model.PropertyFieldPatch
|
||||
err := json.NewDecoder(r.Body).Decode(&patch)
|
||||
if err != nil || patch == nil {
|
||||
c.SetInvalidParamWithErr("property_field_patch", err)
|
||||
return
|
||||
}
|
||||
|
||||
patch.SanitizeInput()
|
||||
|
||||
auditRec := c.MakeAuditRecord("patchCPAField", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "property_field_patch", patch)
|
||||
|
||||
originalField, appErr := c.App.GetCPAField(c.Params.FieldId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(originalField)
|
||||
|
||||
patchedField, appErr := c.App.PatchCPAField(c.Params.FieldId, patch)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(patchedField)
|
||||
auditRec.AddEventObjectType("property_field")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(patchedField); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireFieldId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteCPAField", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "field_id", c.Params.FieldId)
|
||||
|
||||
field, appErr := c.App.GetCPAField(c.Params.FieldId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(field)
|
||||
|
||||
if appErr := c.App.DeleteCPAField(c.Params.FieldId); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(field)
|
||||
auditRec.AddEventObjectType("property_field")
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var attributeValues map[string]string
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&attributeValues); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("attrs", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
// This check is unnecessary for now
|
||||
// Will be required when/if admins can patch other's values
|
||||
userID := c.AppContext.Session().UserId
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userID) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("patchCPAValues", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "user_id", userID)
|
||||
|
||||
results := make(map[string]string)
|
||||
for fieldID, value := range attributeValues {
|
||||
patchedValue, appErr := c.App.PatchCPAValue(userID, fieldID, strings.TrimSpace(value))
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
results[fieldID] = patchedValue.Value
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventObjectType("patchCPAValues")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(results); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func listCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.Params.UserId
|
||||
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, userID)
|
||||
if err != nil || !canSee {
|
||||
c.SetPermissionError(model.PermissionViewMembers)
|
||||
return
|
||||
}
|
||||
|
||||
values, appErr := c.App.ListCPAValues(userID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
returnValue := make(map[string]string)
|
||||
for _, value := range values {
|
||||
returnValue[value.FieldID] = value.Value
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(returnValue); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
17
server/channels/api4/custom_profile_attributes_local.go
Normal file
17
server/channels/api4/custom_profile_attributes_local.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (api *API) InitCustomProfileAttributesLocal() {
|
||||
if api.srv.Config().FeatureFlags.CustomProfileAttributes {
|
||||
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(listCPAFields)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(createCPAField)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(patchCPAField)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(deleteCPAField)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
|
||||
}
|
||||
}
|
||||
269
server/channels/api4/custom_profile_attributes_test.go
Normal file
269
server/channels/api4/custom_profile_attributes_test.go
Normal file
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateCPAField(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("a user without admin permissions should not be able to create a field", func(t *testing.T) {
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
|
||||
_, resp, err := th.Client.CreateCPAField(context.Background(), field)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
field := &model.PropertyField{Name: model.NewId()}
|
||||
|
||||
createdField, resp, err := client.CreateCPAField(context.Background(), field)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, createdField)
|
||||
}, "an invalid field should be rejected")
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
name := model.NewId()
|
||||
field := &model.PropertyField{
|
||||
Name: fmt.Sprintf(" %s\t", name), // name should be sanitized
|
||||
Type: model.PropertyFieldTypeText,
|
||||
Attrs: map[string]any{"visibility": "default"},
|
||||
}
|
||||
|
||||
createdField, resp, err := client.CreateCPAField(context.Background(), field)
|
||||
CheckCreatedStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, createdField.ID)
|
||||
require.Equal(t, name, createdField.Name)
|
||||
require.Equal(t, "default", createdField.Attrs["visibility"])
|
||||
}, "a user with admin permissions should be able to create the field")
|
||||
}
|
||||
|
||||
func TestListCPAFields(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
Attrs: map[string]any{"visibility": "default"},
|
||||
}
|
||||
createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, createdField)
|
||||
|
||||
t.Run("any user should be able to list fields", func(t *testing.T) {
|
||||
fields, resp, err := th.Client.ListCPAFields(context.Background())
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, fields)
|
||||
require.Len(t, fields, 1)
|
||||
require.Equal(t, createdField.ID, fields[0].ID)
|
||||
})
|
||||
|
||||
t.Run("the endpoint should only list non deleted fields", func(t *testing.T) {
|
||||
require.Nil(t, th.App.DeleteCPAField(createdField.ID))
|
||||
fields, resp, err := th.Client.ListCPAFields(context.Background())
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, fields)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCPAField(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("a user without admin permissions should not be able to patch a field", func(t *testing.T) {
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, appErr := th.App.CreateCPAField(field)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, createdField)
|
||||
|
||||
patch := &model.PropertyFieldPatch{Name: model.NewPointer(model.NewId())}
|
||||
_, resp, err := th.Client.PatchCPAField(context.Background(), createdField.ID, patch)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, appErr := th.App.CreateCPAField(field)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, createdField)
|
||||
|
||||
newName := model.NewId()
|
||||
patch := &model.PropertyFieldPatch{Name: model.NewPointer(fmt.Sprintf(" %s \t ", newName))} // name should be sanitized
|
||||
patchedField, resp, err := client.PatchCPAField(context.Background(), createdField.ID, patch)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newName, patchedField.Name)
|
||||
}, "a user with admin permissions should be able to patch the field")
|
||||
}
|
||||
|
||||
func TestDeleteCPAField(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("a user without admin permissions should not be able to delete a field", func(t *testing.T) {
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, createdField)
|
||||
|
||||
resp, err := th.Client.DeleteCPAField(context.Background(), createdField.ID)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, createdField)
|
||||
require.Zero(t, createdField.DeleteAt)
|
||||
|
||||
resp, err := client.DeleteCPAField(context.Background(), createdField.ID)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
deletedField, appErr := th.App.GetCPAField(createdField.ID)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, deletedField.DeleteAt)
|
||||
}, "a user with admin permissions should be able to delete the field")
|
||||
}
|
||||
|
||||
func TestListCPAValues(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.RemovePermissionFromRole(model.PermissionViewMembers.Id, model.SystemUserRoleId)
|
||||
defer func() {
|
||||
th.AddPermissionToRole(model.PermissionViewMembers.Id, model.SystemUserRoleId)
|
||||
}()
|
||||
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, appErr := th.App.CreateCPAField(field)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, createdField)
|
||||
|
||||
values := map[string]string{}
|
||||
values[createdField.ID] = "Field Value"
|
||||
_, _, err := th.Client.PatchCPAValues(context.Background(), values)
|
||||
require.NoError(t, err)
|
||||
|
||||
// login with Client2 from this point on
|
||||
th.LoginBasic2()
|
||||
|
||||
t.Run("any team member should be able to list values", func(t *testing.T) {
|
||||
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, values)
|
||||
require.Len(t, values, 1)
|
||||
})
|
||||
|
||||
t.Run("non team member should NOT be able to list values", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.RemoveTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser2.Id)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCPAValues(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, appErr := th.App.CreateCPAField(field)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, createdField)
|
||||
|
||||
t.Run("any team member should be able to create their own values", func(t *testing.T) {
|
||||
values := map[string]string{}
|
||||
value := "Field Value"
|
||||
values[createdField.ID] = fmt.Sprintf(" %s ", value) // value should be sanitized
|
||||
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, patchedValues)
|
||||
require.Len(t, patchedValues, 1)
|
||||
require.Equal(t, value, patchedValues[createdField.ID])
|
||||
|
||||
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, values)
|
||||
require.Len(t, values, 1)
|
||||
require.Equal(t, "Field Value", values[createdField.ID])
|
||||
})
|
||||
|
||||
t.Run("any team member should be able to patch their own values", func(t *testing.T) {
|
||||
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, values)
|
||||
require.Len(t, values, 1)
|
||||
|
||||
value := "Updated Field Value"
|
||||
values[createdField.ID] = fmt.Sprintf(" %s \t", value) // value should be sanitized
|
||||
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, patchedValues[createdField.ID])
|
||||
|
||||
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, values[createdField.ID])
|
||||
})
|
||||
}
|
||||
@@ -513,6 +513,7 @@ type AppIface interface {
|
||||
CountNotification(notificationType model.NotificationType, platform string)
|
||||
CountNotificationAck(notificationType model.NotificationType, platform string)
|
||||
CountNotificationReason(notificationStatus model.NotificationStatus, notificationType model.NotificationType, notificationReason model.NotificationReason, platform string)
|
||||
CreateCPAField(field *model.PropertyField) (*model.PropertyField, *model.AppError)
|
||||
CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError)
|
||||
CreateChannelBookmark(c request.CTX, newBookmark *model.ChannelBookmark, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
|
||||
CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError)
|
||||
@@ -560,6 +561,7 @@ type AppIface interface {
|
||||
DeleteAllExpiredPluginKeys() *model.AppError
|
||||
DeleteAllKeysForPlugin(pluginID string) *model.AppError
|
||||
DeleteBrandImage(rctx request.CTX) *model.AppError
|
||||
DeleteCPAField(id string) *model.AppError
|
||||
DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError
|
||||
DeleteChannelBookmark(bookmarkId, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
|
||||
DeleteCommand(commandID string) *model.AppError
|
||||
@@ -643,6 +645,8 @@ type AppIface interface {
|
||||
GetBookmark(bookmarkId string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
|
||||
GetBrandImage(rctx request.CTX) ([]byte, *model.AppError)
|
||||
GetBulkReactionsForPosts(postIDs []string) (map[string][]*model.Reaction, *model.AppError)
|
||||
GetCPAField(fieldID string) (*model.PropertyField, *model.AppError)
|
||||
GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError)
|
||||
GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError)
|
||||
GetChannelBookmarks(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, *model.AppError)
|
||||
GetChannelByName(c request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError)
|
||||
@@ -948,6 +952,8 @@ type AppIface interface {
|
||||
License() *model.License
|
||||
LimitedClientConfig() map[string]string
|
||||
ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError)
|
||||
ListCPAFields() ([]*model.PropertyField, *model.AppError)
|
||||
ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppError)
|
||||
ListDirectory(path string) ([]string, *model.AppError)
|
||||
ListDirectoryRecursively(path string) ([]string, *model.AppError)
|
||||
ListExportDirectory(path string) ([]string, *model.AppError)
|
||||
@@ -973,6 +979,8 @@ type AppIface interface {
|
||||
OpenInteractiveDialog(c request.CTX, request model.OpenDialogRequest) *model.AppError
|
||||
OriginChecker() func(*http.Request) bool
|
||||
OutgoingOAuthConnections() einterfaces.OutgoingOAuthConnectionInterface
|
||||
PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.AppError)
|
||||
PatchCPAValue(userID string, fieldID string, value string) (*model.PropertyValue, *model.AppError)
|
||||
PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError)
|
||||
PatchChannelMembersNotifyProps(c request.CTX, members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, *model.AppError)
|
||||
PatchPost(c request.CTX, postID string, patch *model.PostPatch, patchPostOptions *model.UpdatePostOptions) (*model.Post, *model.AppError)
|
||||
|
||||
240
server/channels/app/custom_profile_attributes.go
Normal file
240
server/channels/app/custom_profile_attributes.go
Normal file
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const CustomProfileAttributesFieldLimit = 20
|
||||
|
||||
var cpaGroupID string
|
||||
|
||||
// ToDo: we should explore moving this to the database cache layer
|
||||
// instead of maintaining the ID cached at the application level
|
||||
func (a *App) cpaGroupID() (string, error) {
|
||||
if cpaGroupID != "" {
|
||||
return cpaGroupID, nil
|
||||
}
|
||||
|
||||
cpaGroup, err := a.Srv().propertyService.RegisterPropertyGroup(model.CustomProfileAttributesPropertyGroupName)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "cannot register Custom Profile Attributes property group")
|
||||
}
|
||||
cpaGroupID = cpaGroup.ID
|
||||
|
||||
return cpaGroupID, nil
|
||||
}
|
||||
|
||||
func (a *App) GetCPAField(fieldID string) (*model.PropertyField, *model.AppError) {
|
||||
groupID, err := a.cpaGroupID()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
field, err := a.Srv().propertyService.GetPropertyField(fieldID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.get_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if field.GroupID != groupID {
|
||||
return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
return field, nil
|
||||
}
|
||||
|
||||
func (a *App) ListCPAFields() ([]*model.PropertyField, *model.AppError) {
|
||||
groupID, err := a.cpaGroupID()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
opts := model.PropertyFieldSearchOpts{
|
||||
GroupID: groupID,
|
||||
Page: 0,
|
||||
PerPage: CustomProfileAttributesFieldLimit,
|
||||
}
|
||||
|
||||
fields, err := a.Srv().propertyService.SearchPropertyFields(opts)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.search_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func (a *App) CreateCPAField(field *model.PropertyField) (*model.PropertyField, *model.AppError) {
|
||||
groupID, err := a.cpaGroupID()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
existingFields, appErr := a.ListCPAFields()
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if len(existingFields) >= CustomProfileAttributesFieldLimit {
|
||||
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.limit_reached.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
|
||||
}
|
||||
|
||||
field.GroupID = groupID
|
||||
newField, err := a.Srv().propertyService.CreatePropertyField(field)
|
||||
if err != nil {
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(err, &appErr):
|
||||
return nil, appErr
|
||||
default:
|
||||
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.create_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return newField, nil
|
||||
}
|
||||
|
||||
func (a *App) PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.AppError) {
|
||||
existingField, appErr := a.GetCPAField(fieldID)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// custom profile attributes doesn't use targets
|
||||
patch.TargetID = nil
|
||||
patch.TargetType = nil
|
||||
existingField.Patch(patch)
|
||||
|
||||
patchedField, err := a.Srv().propertyService.UpdatePropertyField(existingField)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("UpdateCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("UpdateCPAField", "app.custom_profile_attributes.property_field_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return patchedField, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteCPAField(id string) *model.AppError {
|
||||
groupID, err := a.cpaGroupID()
|
||||
if err != nil {
|
||||
return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
existingField, err := a.Srv().propertyService.GetPropertyField(id)
|
||||
if err != nil {
|
||||
return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.get_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if existingField.GroupID != groupID {
|
||||
return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
if err := a.Srv().propertyService.DeletePropertyField(id); err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.property_field_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppError) {
|
||||
groupID, err := a.cpaGroupID()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
opts := model.PropertyValueSearchOpts{
|
||||
GroupID: groupID,
|
||||
TargetID: userID,
|
||||
Page: 0,
|
||||
PerPage: 999999,
|
||||
IncludeDeleted: false,
|
||||
}
|
||||
fields, err := a.Srv().propertyService.SearchPropertyValues(opts)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ListCPAValues", "app.custom_profile_attributes.list_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func (a *App) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError) {
|
||||
groupID, err := a.cpaGroupID()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetCPAValue", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
value, err := a.Srv().propertyService.GetPropertyValue(valueID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetCPAValue", "app.custom_profile_attributes.get_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if value.GroupID != groupID {
|
||||
return nil, model.NewAppError("GetCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (a *App) PatchCPAValue(userID string, fieldID string, value string) (*model.PropertyValue, *model.AppError) {
|
||||
groupID, err := a.cpaGroupID()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
// make sure field exists in this group
|
||||
existingField, appErr := a.GetCPAField(fieldID)
|
||||
if appErr != nil {
|
||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(appErr)
|
||||
} else if existingField.DeleteAt > 0 {
|
||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
existingValues, appErr := a.ListCPAValues(userID)
|
||||
if appErr != nil {
|
||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_list.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
}
|
||||
var existingValue *model.PropertyValue
|
||||
for key, value := range existingValues {
|
||||
if value.FieldID == fieldID {
|
||||
existingValue = existingValues[key]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existingValue != nil {
|
||||
existingValue.Value = value
|
||||
_, err = a.ch.srv.propertyService.UpdatePropertyValue(existingValue)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
} else {
|
||||
propertyValue := &model.PropertyValue{
|
||||
GroupID: groupID,
|
||||
TargetType: "user",
|
||||
TargetID: userID,
|
||||
FieldID: fieldID,
|
||||
Value: value,
|
||||
}
|
||||
existingValue, err = a.ch.srv.propertyService.CreatePropertyValue(propertyValue)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_creation.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
return existingValue, nil
|
||||
}
|
||||
462
server/channels/app/custom_profile_attributes_test.go
Normal file
462
server/channels/app/custom_profile_attributes_test.go
Normal file
@@ -0,0 +1,462 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetCPAField(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
cpaGroupID, cErr := th.App.cpaGroupID()
|
||||
require.NoError(t, cErr)
|
||||
|
||||
t.Run("should fail when getting a non-existent field", func(t *testing.T) {
|
||||
field, err := th.App.GetCPAField(model.NewId())
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "app.custom_profile_attributes.get_property_field.app_error", err.Id)
|
||||
require.Empty(t, field)
|
||||
})
|
||||
|
||||
t.Run("should fail when getting a field from a different group", func(t *testing.T) {
|
||||
field := &model.PropertyField{
|
||||
GroupID: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, err := th.App.Srv().propertyService.CreatePropertyField(field)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetchedField, appErr := th.App.GetCPAField(createdField.ID)
|
||||
require.NotNil(t, appErr)
|
||||
require.Equal(t, "app.custom_profile_attributes.property_field_not_found.app_error", appErr.Id)
|
||||
require.Empty(t, fetchedField)
|
||||
})
|
||||
|
||||
t.Run("should get an existing CPA field", func(t *testing.T) {
|
||||
field := &model.PropertyField{
|
||||
GroupID: cpaGroupID,
|
||||
Name: "Test Field",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
Attrs: map[string]any{"visibility": "hidden"},
|
||||
}
|
||||
|
||||
createdField, err := th.App.CreateCPAField(field)
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, createdField.ID)
|
||||
|
||||
fetchedField, err := th.App.GetCPAField(createdField.ID)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, createdField.ID, fetchedField.ID)
|
||||
require.Equal(t, "Test Field", fetchedField.Name)
|
||||
require.Equal(t, map[string]any{"visibility": "hidden"}, fetchedField.Attrs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListCPAFields(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
cpaGroupID, cErr := th.App.cpaGroupID()
|
||||
require.NoError(t, cErr)
|
||||
|
||||
t.Run("should list the CPA property fields", func(t *testing.T) {
|
||||
field1 := &model.PropertyField{
|
||||
GroupID: cpaGroupID,
|
||||
Name: "Field 1",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
|
||||
_, err := th.App.Srv().propertyService.CreatePropertyField(field1)
|
||||
require.NoError(t, err)
|
||||
|
||||
field2 := &model.PropertyField{
|
||||
GroupID: model.NewId(),
|
||||
Name: "Field 2",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
_, err = th.App.Srv().propertyService.CreatePropertyField(field2)
|
||||
require.NoError(t, err)
|
||||
|
||||
field3 := &model.PropertyField{
|
||||
GroupID: cpaGroupID,
|
||||
Name: "Field 3",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
_, err = th.App.Srv().propertyService.CreatePropertyField(field3)
|
||||
require.NoError(t, err)
|
||||
|
||||
fields, appErr := th.App.ListCPAFields()
|
||||
require.Nil(t, appErr)
|
||||
require.Len(t, fields, 2)
|
||||
|
||||
fieldNames := []string{}
|
||||
for _, field := range fields {
|
||||
fieldNames = append(fieldNames, field.Name)
|
||||
}
|
||||
require.ElementsMatch(t, []string{"Field 1", "Field 3"}, fieldNames)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateCPAField(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
|
||||
cpaGroupID, cErr := th.App.cpaGroupID()
|
||||
require.NoError(t, cErr)
|
||||
|
||||
t.Run("should fail if the field is not valid", func(t *testing.T) {
|
||||
field := &model.PropertyField{Name: model.NewId()}
|
||||
|
||||
createdField, err := th.App.CreateCPAField(field)
|
||||
require.NotNil(t, err)
|
||||
require.Empty(t, createdField)
|
||||
})
|
||||
|
||||
t.Run("should not be able to create a property field for a different feature", func(t *testing.T) {
|
||||
field := &model.PropertyField{
|
||||
GroupID: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
|
||||
createdField, appErr := th.App.CreateCPAField(field)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, cpaGroupID, createdField.GroupID)
|
||||
})
|
||||
|
||||
t.Run("should correctly create a CPA field", func(t *testing.T) {
|
||||
field := &model.PropertyField{
|
||||
GroupID: cpaGroupID,
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
Attrs: map[string]any{"visibility": "hidden"},
|
||||
}
|
||||
|
||||
createdField, err := th.App.CreateCPAField(field)
|
||||
require.Nil(t, err)
|
||||
require.NotZero(t, createdField.ID)
|
||||
require.Equal(t, cpaGroupID, createdField.GroupID)
|
||||
require.Equal(t, map[string]any{"visibility": "hidden"}, createdField.Attrs)
|
||||
|
||||
fetchedField, gErr := th.App.Srv().propertyService.GetPropertyField(createdField.ID)
|
||||
require.NoError(t, gErr)
|
||||
require.Equal(t, field.Name, fetchedField.Name)
|
||||
require.NotZero(t, fetchedField.CreateAt)
|
||||
require.Equal(t, fetchedField.CreateAt, fetchedField.UpdateAt)
|
||||
})
|
||||
|
||||
// reset the server at this point to avoid polluting the state
|
||||
th.TearDown()
|
||||
|
||||
t.Run("CPA should honor the field limit", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("should not be able to create CPA fields above the limit", func(t *testing.T) {
|
||||
// we create the rest of the fields required to reach the limit
|
||||
for i := 1; i <= CustomProfileAttributesFieldLimit; i++ {
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, err := th.App.CreateCPAField(field)
|
||||
require.Nil(t, err)
|
||||
require.NotZero(t, createdField.ID)
|
||||
}
|
||||
|
||||
// then, we create a last one that would exceed the limit
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, err := th.App.CreateCPAField(field)
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, http.StatusUnprocessableEntity, err.StatusCode)
|
||||
require.Zero(t, createdField)
|
||||
})
|
||||
|
||||
t.Run("deleted fields should not count for the limit", func(t *testing.T) {
|
||||
// we retrieve the list of fields and check we've reached the limit
|
||||
fields, err := th.App.ListCPAFields()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, fields, CustomProfileAttributesFieldLimit)
|
||||
|
||||
// then we delete one field
|
||||
require.Nil(t, th.App.DeleteCPAField(fields[0].ID))
|
||||
|
||||
// creating a new one should work now
|
||||
field := &model.PropertyField{
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, err := th.App.CreateCPAField(field)
|
||||
require.Nil(t, err)
|
||||
require.NotZero(t, createdField.ID)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCPAField(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
cpaGroupID, cErr := th.App.cpaGroupID()
|
||||
require.NoError(t, cErr)
|
||||
|
||||
newField := &model.PropertyField{
|
||||
GroupID: cpaGroupID,
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
Attrs: map[string]any{"visibility": "hidden"},
|
||||
}
|
||||
createdField, err := th.App.CreateCPAField(newField)
|
||||
require.Nil(t, err)
|
||||
|
||||
patch := &model.PropertyFieldPatch{
|
||||
Name: model.NewPointer("Patched name"),
|
||||
Attrs: model.NewPointer(map[string]any{"visibility": "default"}),
|
||||
TargetID: model.NewPointer(model.NewId()),
|
||||
TargetType: model.NewPointer(model.NewId()),
|
||||
}
|
||||
|
||||
t.Run("should fail if the field doesn't exist", func(t *testing.T) {
|
||||
updatedField, err := th.App.PatchCPAField(model.NewId(), patch)
|
||||
require.NotNil(t, err)
|
||||
require.Empty(t, updatedField)
|
||||
})
|
||||
|
||||
t.Run("should not allow to patch a field outside of CPA", func(t *testing.T) {
|
||||
newField := &model.PropertyField{
|
||||
GroupID: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
field, err := th.App.Srv().propertyService.CreatePropertyField(newField)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedField, uErr := th.App.PatchCPAField(field.ID, patch)
|
||||
require.NotNil(t, uErr)
|
||||
require.Equal(t, "app.custom_profile_attributes.property_field_not_found.app_error", uErr.Id)
|
||||
require.Empty(t, updatedField)
|
||||
})
|
||||
|
||||
t.Run("should correctly patch the CPA property field", func(t *testing.T) {
|
||||
time.Sleep(10 * time.Millisecond) // ensure the UpdateAt is different than CreateAt
|
||||
|
||||
updatedField, err := th.App.PatchCPAField(createdField.ID, patch)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, createdField.ID, updatedField.ID)
|
||||
require.Equal(t, "Patched name", updatedField.Name)
|
||||
require.Equal(t, "default", updatedField.Attrs["visibility"])
|
||||
require.Empty(t, updatedField.TargetID, "CPA should not allow to patch the field's target ID")
|
||||
require.Empty(t, updatedField.TargetType, "CPA should not allow to patch the field's target type")
|
||||
require.Greater(t, updatedField.UpdateAt, createdField.UpdateAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteCPAField(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
cpaGroupID, cErr := th.App.cpaGroupID()
|
||||
require.NoError(t, cErr)
|
||||
|
||||
newField := &model.PropertyField{
|
||||
GroupID: cpaGroupID,
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, err := th.App.CreateCPAField(newField)
|
||||
require.Nil(t, err)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
newValue := &model.PropertyValue{
|
||||
TargetID: model.NewId(),
|
||||
TargetType: "user",
|
||||
GroupID: cpaGroupID,
|
||||
FieldID: createdField.ID,
|
||||
Value: fmt.Sprintf("Value %d", i),
|
||||
}
|
||||
value, err := th.App.Srv().propertyService.CreatePropertyValue(newValue)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, value.ID)
|
||||
}
|
||||
|
||||
t.Run("should fail if the field doesn't exist", func(t *testing.T) {
|
||||
err := th.App.DeleteCPAField(model.NewId())
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "app.custom_profile_attributes.get_property_field.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("should not allow to delete a field outside of CPA", func(t *testing.T) {
|
||||
newField := &model.PropertyField{
|
||||
GroupID: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
field, err := th.App.Srv().propertyService.CreatePropertyField(newField)
|
||||
require.NoError(t, err)
|
||||
|
||||
dErr := th.App.DeleteCPAField(field.ID)
|
||||
require.NotNil(t, dErr)
|
||||
require.Equal(t, "app.custom_profile_attributes.property_field_not_found.app_error", dErr.Id)
|
||||
})
|
||||
|
||||
t.Run("should correctly delete the field", func(t *testing.T) {
|
||||
// check that we have the associated values to the field prior deletion
|
||||
opts := model.PropertyValueSearchOpts{PerPage: 10, FieldID: createdField.ID}
|
||||
values, err := th.App.Srv().propertyService.SearchPropertyValues(opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, values, 3)
|
||||
|
||||
// delete the field
|
||||
require.Nil(t, th.App.DeleteCPAField(createdField.ID))
|
||||
|
||||
// check that it is marked as deleted
|
||||
fetchedField, err := th.App.Srv().propertyService.GetPropertyField(createdField.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, fetchedField.DeleteAt)
|
||||
|
||||
// ensure that the associated fields have been marked as deleted too
|
||||
values, err = th.App.Srv().propertyService.SearchPropertyValues(opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, values, 0)
|
||||
|
||||
opts.IncludeDeleted = true
|
||||
values, err = th.App.Srv().propertyService.SearchPropertyValues(opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, values, 3)
|
||||
for _, value := range values {
|
||||
require.NotZero(t, value.DeleteAt)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCPAValue(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
cpaGroupID, cErr := th.App.cpaGroupID()
|
||||
require.NoError(t, cErr)
|
||||
|
||||
fieldID := model.NewId()
|
||||
|
||||
t.Run("should fail if the value doesn't exist", func(t *testing.T) {
|
||||
pv, appErr := th.App.GetCPAValue(model.NewId())
|
||||
require.NotNil(t, appErr)
|
||||
require.Nil(t, pv)
|
||||
})
|
||||
|
||||
t.Run("should fail if the group id is invalid", func(t *testing.T) {
|
||||
propertyValue := &model.PropertyValue{
|
||||
TargetID: model.NewId(),
|
||||
TargetType: "user",
|
||||
GroupID: model.NewId(),
|
||||
FieldID: fieldID,
|
||||
Value: "Value",
|
||||
}
|
||||
propertyValue, err := th.App.Srv().propertyService.CreatePropertyValue(propertyValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
pv, appErr := th.App.GetCPAValue(propertyValue.ID)
|
||||
require.NotNil(t, appErr)
|
||||
require.Nil(t, pv)
|
||||
})
|
||||
|
||||
t.Run("should succeed if id exists", func(t *testing.T) {
|
||||
propertyValue := &model.PropertyValue{
|
||||
TargetID: model.NewId(),
|
||||
TargetType: "user",
|
||||
GroupID: cpaGroupID,
|
||||
FieldID: fieldID,
|
||||
Value: "Value",
|
||||
}
|
||||
propertyValue, err := th.App.Srv().propertyService.CreatePropertyValue(propertyValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
pv, appErr := th.App.GetCPAValue(propertyValue.ID)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, pv)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCPAValue(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
cpaGroupID, cErr := th.App.cpaGroupID()
|
||||
require.NoError(t, cErr)
|
||||
|
||||
t.Run("should fail if the field doesn't exist", func(t *testing.T) {
|
||||
invalidFieldID := model.NewId()
|
||||
_, appErr := th.App.PatchCPAValue(model.NewId(), invalidFieldID, "fieldValue")
|
||||
require.NotNil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("should create value if new field value", func(t *testing.T) {
|
||||
newField := &model.PropertyField{
|
||||
GroupID: cpaGroupID,
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, err := th.App.Srv().propertyService.CreatePropertyField(newField)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID := model.NewId()
|
||||
patchedValue, appErr := th.App.PatchCPAValue(userID, createdField.ID, "test value")
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, patchedValue)
|
||||
require.Equal(t, "test value", patchedValue.Value)
|
||||
require.Equal(t, userID, patchedValue.TargetID)
|
||||
|
||||
t.Run("should correctly patch the CPA property value", func(t *testing.T) {
|
||||
patch2, appErr := th.App.PatchCPAValue(userID, createdField.ID, "new patched value")
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, patch2)
|
||||
require.Equal(t, patchedValue.ID, patch2.ID)
|
||||
require.Equal(t, "new patched value", patch2.Value)
|
||||
require.Equal(t, userID, patch2.TargetID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("should fail if field is deleted", func(t *testing.T) {
|
||||
newField := &model.PropertyField{
|
||||
GroupID: cpaGroupID,
|
||||
Name: model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
}
|
||||
createdField, err := th.App.Srv().propertyService.CreatePropertyField(newField)
|
||||
require.NoError(t, err)
|
||||
err = th.App.Srv().propertyService.DeletePropertyField(createdField.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID := model.NewId()
|
||||
patchedValue, appErr := th.App.PatchCPAValue(userID, createdField.ID, "test value")
|
||||
require.NotNil(t, appErr)
|
||||
require.Nil(t, patchedValue)
|
||||
})
|
||||
}
|
||||
@@ -2041,6 +2041,28 @@ func (a *OpenTracingAppLayer) CreateBot(rctx request.CTX, bot *model.Bot) (*mode
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CreateCPAField(field *model.PropertyField) (*model.PropertyField, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateCPAField")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.CreateCPAField(field)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannel")
|
||||
@@ -3206,6 +3228,28 @@ func (a *OpenTracingAppLayer) DeleteBrandImage(rctx request.CTX) *model.AppError
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteCPAField(id string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteCPAField")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.DeleteCPAField(id)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteChannel")
|
||||
@@ -5484,6 +5528,50 @@ func (a *OpenTracingAppLayer) GetBulkReactionsForPosts(postIDs []string) (map[st
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetCPAField(fieldID string) (*model.PropertyField, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCPAField")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetCPAField(fieldID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCPAValue")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetCPAValue(valueID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannel")
|
||||
@@ -12677,6 +12765,50 @@ func (a *OpenTracingAppLayer) ListAutocompleteCommands(teamID string, T i18n.Tra
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) ListCPAFields() ([]*model.PropertyField, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListCPAFields")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.ListCPAFields()
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListCPAValues")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.ListCPAValues(userID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) ListDirectory(path string) ([]string, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListDirectory")
|
||||
@@ -13367,6 +13499,50 @@ func (a *OpenTracingAppLayer) PatchBot(rctx request.CTX, botUserId string, botPa
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchCPAField")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.PatchCPAField(fieldID, patch)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) PatchCPAValue(userID string, fieldID string, value string) (*model.PropertyValue, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchCPAValue")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.PatchCPAValue(userID, fieldID, value)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchChannel")
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
)
|
||||
|
||||
func (s *SqlPropertyFieldStore) propertyFieldToInsertMap(field *model.PropertyField) (map[string]any, error) {
|
||||
if field.Attrs == nil {
|
||||
field.Attrs = make(map[string]any)
|
||||
}
|
||||
|
||||
attrsJSON, err := json.Marshal(field.Attrs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "property_field_to_insert_map_marshal_attrs")
|
||||
@@ -39,6 +43,10 @@ func (s *SqlPropertyFieldStore) propertyFieldToInsertMap(field *model.PropertyFi
|
||||
}
|
||||
|
||||
func (s *SqlPropertyFieldStore) propertyFieldToUpdateMap(field *model.PropertyField) (map[string]any, error) {
|
||||
if field.Attrs == nil {
|
||||
field.Attrs = make(map[string]any)
|
||||
}
|
||||
|
||||
attrsJSON, err := json.Marshal(field.Attrs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "property_field_to_update_map_marshal_attrs")
|
||||
|
||||
@@ -685,6 +685,17 @@ func (c *Context) RequireRoleId() *Context {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) RequireFieldId() *Context {
|
||||
if c.Err != nil {
|
||||
return c
|
||||
}
|
||||
|
||||
if !model.IsValidId(c.Params.FieldId) {
|
||||
c.SetInvalidURLParam("field_id")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) RequireSchemeId() *Context {
|
||||
if c.Err != nil {
|
||||
return c
|
||||
|
||||
@@ -111,6 +111,9 @@ type Params struct {
|
||||
|
||||
// Cloud
|
||||
InvoiceId string
|
||||
|
||||
// Custom Profile Attributes
|
||||
FieldId string
|
||||
}
|
||||
|
||||
func ParamsFromRequest(r *http.Request) *Params {
|
||||
@@ -178,6 +181,7 @@ func ParamsFromRequest(r *http.Request) *Params {
|
||||
params.ExcludeHome, _ = strconv.ParseBool(query.Get("exclude_home"))
|
||||
params.ExcludeRemote, _ = strconv.ParseBool(query.Get("exclude_remote"))
|
||||
params.ChannelBookmarkId = props["bookmark_id"]
|
||||
params.FieldId = props["field_id"]
|
||||
params.Scope = query.Get("scope")
|
||||
|
||||
if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 {
|
||||
|
||||
@@ -4974,6 +4974,54 @@
|
||||
"id": "app.custom_group.unique_name",
|
||||
"translation": "group name is not unique"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.cpa_group_id.app_error",
|
||||
"translation": "Cannot register Custom Profile Attributes property group"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.create_property_field.app_error",
|
||||
"translation": "Unable to create Custom Profile Attribute field"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.get_property_field.app_error",
|
||||
"translation": "Unable to get Custom Profile Attribute field"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.limit_reached.app_error",
|
||||
"translation": "Custom Profile Attributes field limit reached"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.list_property_values.app_error",
|
||||
"translation": "Unable to get custom profile attribute values"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.property_field_delete.app_error",
|
||||
"translation": "Unable to delete Custom Profile Attribute field"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.property_field_not_found.app_error",
|
||||
"translation": "Custom Profile Attribute field not found"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.property_field_update.app_error",
|
||||
"translation": "Unable to update Custom Profile Attribute field"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.property_value_creation.app_error",
|
||||
"translation": "Cannot create property value"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.property_value_list.app_error",
|
||||
"translation": "Unable to retrieve property values"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.property_value_update.app_error",
|
||||
"translation": "Cannot update property value"
|
||||
},
|
||||
{
|
||||
"id": "app.custom_profile_attributes.search_property_fields.app_error",
|
||||
"translation": "Unable to search Custom Profile Attribute fields"
|
||||
},
|
||||
{
|
||||
"id": "app.delete_scheduled_post.delete_error",
|
||||
"translation": "Failed to delete scheduled post from database."
|
||||
|
||||
@@ -600,6 +600,26 @@ func (c *Client4) limitsRoute() string {
|
||||
return "/limits"
|
||||
}
|
||||
|
||||
func (c *Client4) customProfileAttributesRoute() string {
|
||||
return "/custom_profile_attributes"
|
||||
}
|
||||
|
||||
func (c *Client4) userCustomProfileAttributesRoute(userID string) string {
|
||||
return fmt.Sprintf("%s/%s", c.userRoute(userID), c.customProfileAttributesRoute())
|
||||
}
|
||||
|
||||
func (c *Client4) customProfileAttributeFieldsRoute() string {
|
||||
return fmt.Sprintf("%s/fields", c.customProfileAttributesRoute())
|
||||
}
|
||||
|
||||
func (c *Client4) customProfileAttributeFieldRoute(fieldID string) string {
|
||||
return fmt.Sprintf("%s/%s", c.customProfileAttributeFieldsRoute(), fieldID)
|
||||
}
|
||||
|
||||
func (c *Client4) customProfileAttributeValuesRoute() string {
|
||||
return fmt.Sprintf("%s/values", c.customProfileAttributesRoute())
|
||||
}
|
||||
|
||||
func (c *Client4) GetServerLimits(ctx context.Context) (*ServerLimits, *Response, error) {
|
||||
r, err := c.DoAPIGet(ctx, c.limitsRoute()+"/users", "")
|
||||
if err != nil {
|
||||
@@ -9383,3 +9403,96 @@ func (c *Client4) RestorePostVersion(ctx context.Context, postId, versionId stri
|
||||
}
|
||||
return restoredPost, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) CreateCPAField(ctx context.Context, field *PropertyField) (*PropertyField, *Response, error) {
|
||||
buf, err := json.Marshal(field)
|
||||
if err != nil {
|
||||
return nil, nil, NewAppError("CreateCPAField", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
r, err := c.DoAPIPostBytes(ctx, c.customProfileAttributeFieldsRoute(), buf)
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var pf PropertyField
|
||||
if err := json.NewDecoder(r.Body).Decode(&pf); err != nil {
|
||||
return nil, nil, NewAppError("CreateCPAField", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return &pf, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) ListCPAFields(ctx context.Context) ([]*PropertyField, *Response, error) {
|
||||
r, err := c.DoAPIGet(ctx, c.customProfileAttributeFieldsRoute(), "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var fields []*PropertyField
|
||||
if err := json.NewDecoder(r.Body).Decode(&fields); err != nil {
|
||||
return nil, nil, NewAppError("ListCPAFields", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return fields, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) PatchCPAField(ctx context.Context, fieldID string, patch *PropertyFieldPatch) (*PropertyField, *Response, error) {
|
||||
buf, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
return nil, nil, NewAppError("PatchCPAField", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
r, err := c.DoAPIPatchBytes(ctx, c.customProfileAttributeFieldRoute(fieldID), buf)
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var pf PropertyField
|
||||
if err := json.NewDecoder(r.Body).Decode(&pf); err != nil {
|
||||
return nil, nil, NewAppError("PatchCPAField", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return &pf, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) DeleteCPAField(ctx context.Context, fieldID string) (*Response, error) {
|
||||
r, err := c.DoAPIDelete(ctx, c.customProfileAttributeFieldRoute(fieldID))
|
||||
if err != nil {
|
||||
return BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) ListCPAValues(ctx context.Context, userID string) (map[string]string, *Response, error) {
|
||||
r, err := c.DoAPIGet(ctx, c.userCustomProfileAttributesRoute(userID), "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
fields := make(map[string]string)
|
||||
if err := json.NewDecoder(r.Body).Decode(&fields); err != nil {
|
||||
return nil, nil, NewAppError("ListCPAValues", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return fields, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) PatchCPAValues(ctx context.Context, values map[string]string) (map[string]string, *Response, error) {
|
||||
buf, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return nil, nil, NewAppError("PatchCPAValues", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
r, err := c.DoAPIPatchBytes(ctx, c.customProfileAttributeValuesRoute(), buf)
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var patchedValues map[string]string
|
||||
if err := json.NewDecoder(r.Body).Decode(&patchedValues); err != nil {
|
||||
return nil, nil, NewAppError("PatchCPAValues", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return patchedValues, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
6
server/public/model/custom_profile_attributes.go
Normal file
6
server/public/model/custom_profile_attributes.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
const CustomProfileAttributesPropertyGroupName = "custom_profile_attributes"
|
||||
@@ -57,6 +57,8 @@ type FeatureFlags struct {
|
||||
ExperimentalAuditSettingsSystemConsoleUI bool
|
||||
|
||||
ExperimentalCrossTeamSearch bool
|
||||
|
||||
CustomProfileAttributes bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@@ -81,6 +83,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.NotificationMonitoring = true
|
||||
f.ExperimentalAuditSettingsSystemConsoleUI = false
|
||||
f.ExperimentalCrossTeamSearch = false
|
||||
f.CustomProfileAttributes = false
|
||||
}
|
||||
|
||||
// ToMap returns the feature flags as a map[string]string
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
package model
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PropertyFieldType string
|
||||
|
||||
@@ -29,6 +32,21 @@ type PropertyField struct {
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
}
|
||||
|
||||
func (pf *PropertyField) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": pf.ID,
|
||||
"group_id": pf.GroupID,
|
||||
"name": pf.Name,
|
||||
"type": pf.Type,
|
||||
"attrs": pf.Attrs,
|
||||
"target_id": pf.TargetID,
|
||||
"target_type": pf.TargetType,
|
||||
"create_at": pf.CreateAt,
|
||||
"update_at": pf.UpdateAt,
|
||||
"delete_at": pf.DeleteAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (pf *PropertyField) PreSave() {
|
||||
if pf.ID == "" {
|
||||
pf.ID = NewId()
|
||||
@@ -73,6 +91,56 @@ func (pf *PropertyField) IsValid() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pf *PropertyField) SanitizeInput() {
|
||||
pf.Name = strings.TrimSpace(pf.Name)
|
||||
}
|
||||
|
||||
type PropertyFieldPatch struct {
|
||||
Name *string `json:"name"`
|
||||
Type *PropertyFieldType `json:"type"`
|
||||
Attrs *map[string]any `json:"attrs"`
|
||||
TargetID *string `json:"target_id"`
|
||||
TargetType *string `json:"target_type"`
|
||||
}
|
||||
|
||||
func (pfp *PropertyFieldPatch) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": pfp.Name,
|
||||
"type": pfp.Type,
|
||||
"attrs": pfp.Attrs,
|
||||
"target_id": pfp.TargetID,
|
||||
"target_type": pfp.TargetType,
|
||||
}
|
||||
}
|
||||
|
||||
func (pfp *PropertyFieldPatch) SanitizeInput() {
|
||||
if pfp.Name != nil {
|
||||
pfp.Name = NewPointer(strings.TrimSpace(*pfp.Name))
|
||||
}
|
||||
}
|
||||
|
||||
func (pf *PropertyField) Patch(patch *PropertyFieldPatch) {
|
||||
if patch.Name != nil {
|
||||
pf.Name = *patch.Name
|
||||
}
|
||||
|
||||
if patch.Type != nil {
|
||||
pf.Type = *patch.Type
|
||||
}
|
||||
|
||||
if patch.Attrs != nil {
|
||||
pf.Attrs = *patch.Attrs
|
||||
}
|
||||
|
||||
if patch.TargetID != nil {
|
||||
pf.TargetID = *patch.TargetID
|
||||
}
|
||||
|
||||
if patch.TargetType != nil {
|
||||
pf.TargetType = *patch.TargetType
|
||||
}
|
||||
}
|
||||
|
||||
type PropertyFieldSearchOpts struct {
|
||||
GroupID string
|
||||
TargetType string
|
||||
|
||||
Reference in New Issue
Block a user