2019-01-24 16:26:36 +00:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
2018-11-15 12:00:08 +00:00
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
2019-01-15 13:03:24 +00:00
|
|
|
package consoleql
|
2018-11-14 10:50:15 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"github.com/graphql-go/graphql"
|
2019-03-26 15:56:16 +00:00
|
|
|
"go.uber.org/zap"
|
2018-11-14 10:50:15 +00:00
|
|
|
|
2020-03-30 10:08:50 +01:00
|
|
|
"storj.io/common/uuid"
|
2019-11-14 19:46:15 +00:00
|
|
|
"storj.io/storj/private/post"
|
2019-01-15 13:03:24 +00:00
|
|
|
"storj.io/storj/satellite/console"
|
2019-03-02 15:22:20 +00:00
|
|
|
"storj.io/storj/satellite/mailservice"
|
2018-11-14 10:50:15 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-08-11 15:50:01 +01:00
|
|
|
// Mutation is graphql request that modifies data.
|
2018-11-14 10:50:15 +00:00
|
|
|
Mutation = "mutation"
|
|
|
|
|
2020-08-11 15:50:01 +01:00
|
|
|
// CreateProjectMutation is a mutation name for project creation.
|
2019-01-24 16:26:36 +00:00
|
|
|
CreateProjectMutation = "createProject"
|
2020-08-11 15:50:01 +01:00
|
|
|
// DeleteProjectMutation is a mutation name for project deletion.
|
2019-01-24 16:26:36 +00:00
|
|
|
DeleteProjectMutation = "deleteProject"
|
2020-09-10 10:32:35 +01:00
|
|
|
// UpdateProjectMutation is a mutation name for project name and description updating.
|
|
|
|
UpdateProjectMutation = "updateProject"
|
2019-01-24 16:26:36 +00:00
|
|
|
|
2020-08-11 15:50:01 +01:00
|
|
|
// AddProjectMembersMutation is a mutation name for adding new project members.
|
2019-01-24 16:26:36 +00:00
|
|
|
AddProjectMembersMutation = "addProjectMembers"
|
2020-08-11 15:50:01 +01:00
|
|
|
// DeleteProjectMembersMutation is a mutation name for deleting project members.
|
2019-01-24 16:26:36 +00:00
|
|
|
DeleteProjectMembersMutation = "deleteProjectMembers"
|
|
|
|
|
2020-08-11 15:50:01 +01:00
|
|
|
// CreateAPIKeyMutation is a mutation name for api key creation.
|
2019-01-24 16:26:36 +00:00
|
|
|
CreateAPIKeyMutation = "createAPIKey"
|
2020-08-11 15:50:01 +01:00
|
|
|
// DeleteAPIKeysMutation is a mutation name for api key deleting.
|
2019-02-13 11:34:40 +00:00
|
|
|
DeleteAPIKeysMutation = "deleteAPIKeys"
|
2019-01-24 16:26:36 +00:00
|
|
|
|
2020-08-11 15:50:01 +01:00
|
|
|
// AddPaymentMethodMutation is mutation name for adding new payment method.
|
2019-07-10 21:29:26 +01:00
|
|
|
AddPaymentMethodMutation = "addPaymentMethod"
|
2020-08-11 15:50:01 +01:00
|
|
|
// DeletePaymentMethodMutation is mutation name for deleting payment method.
|
2019-07-10 21:29:26 +01:00
|
|
|
DeletePaymentMethodMutation = "deletePaymentMethod"
|
2020-08-11 15:50:01 +01:00
|
|
|
// SetDefaultPaymentMethodMutation is mutation name setting payment method as default payment method.
|
2019-07-10 21:29:26 +01:00
|
|
|
SetDefaultPaymentMethodMutation = "setDefaultPaymentMethod"
|
|
|
|
|
2020-08-11 15:50:01 +01:00
|
|
|
// InputArg is argument name for all input types.
|
2019-01-24 16:26:36 +00:00
|
|
|
InputArg = "input"
|
2021-08-02 23:06:15 +01:00
|
|
|
// ProjectFields is a field name for project specific fields.
|
|
|
|
ProjectFields = "projectFields"
|
|
|
|
// ProjectLimits is a field name for project specific limits.
|
|
|
|
ProjectLimits = "projectLimits"
|
2020-08-11 15:50:01 +01:00
|
|
|
// FieldProjectID is field name for projectID.
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldProjectID = "projectID"
|
2020-08-11 15:50:01 +01:00
|
|
|
// FieldNewPassword is a field name for new password.
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldNewPassword = "newPassword"
|
2020-08-11 15:50:01 +01:00
|
|
|
// Secret is a field name for registration token for user creation during Vanguard release.
|
2019-03-19 17:55:43 +00:00
|
|
|
Secret = "secret"
|
2020-08-11 15:50:01 +01:00
|
|
|
// ReferrerUserID is a field name for passing referrer's user id.
|
2019-07-26 18:58:17 +01:00
|
|
|
ReferrerUserID = "referrerUserId"
|
2018-11-14 10:50:15 +00:00
|
|
|
)
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// rootMutation creates mutation for graphql populated by AccountsClient.
|
2019-04-04 15:56:20 +01:00
|
|
|
func rootMutation(log *zap.Logger, service *console.Service, mailService *mailservice.Service, types *TypeCreator) *graphql.Object {
|
2018-11-14 10:50:15 +00:00
|
|
|
return graphql.NewObject(graphql.ObjectConfig{
|
|
|
|
Name: Mutation,
|
|
|
|
Fields: graphql.Fields{
|
2018-11-26 10:47:23 +00:00
|
|
|
// creates project from input params
|
2019-01-24 16:26:36 +00:00
|
|
|
CreateProjectMutation: &graphql.Field{
|
2019-04-04 15:56:20 +01:00
|
|
|
Type: types.project,
|
2018-11-26 10:47:23 +00:00
|
|
|
Args: graphql.FieldConfigArgument{
|
2019-01-24 16:26:36 +00:00
|
|
|
InputArg: &graphql.ArgumentConfig{
|
2019-04-04 15:56:20 +01:00
|
|
|
Type: graphql.NewNonNull(types.projectInput),
|
2018-11-26 10:47:23 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2019-01-24 16:26:36 +00:00
|
|
|
var projectInput = fromMapProjectInfo(p.Args[InputArg].(map[string]interface{}))
|
2018-11-26 10:47:23 +00:00
|
|
|
|
2019-09-04 16:02:39 +01:00
|
|
|
project, err := service.CreateProject(p.Context, projectInput)
|
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2019-09-04 16:02:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return project, nil
|
2018-11-26 10:47:23 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
// deletes project by id, taken from input params
|
2019-01-24 16:26:36 +00:00
|
|
|
DeleteProjectMutation: &graphql.Field{
|
2019-04-04 15:56:20 +01:00
|
|
|
Type: types.project,
|
2018-11-26 10:47:23 +00:00
|
|
|
Args: graphql.FieldConfigArgument{
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldID: &graphql.ArgumentConfig{
|
2018-11-28 16:20:23 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.String),
|
2018-11-26 10:47:23 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, console.ErrUnauthorized.New("not implemented")
|
2018-11-26 10:47:23 +00:00
|
|
|
},
|
|
|
|
},
|
2020-09-10 10:32:35 +01:00
|
|
|
// updates project name and description.
|
|
|
|
UpdateProjectMutation: &graphql.Field{
|
2019-04-04 15:56:20 +01:00
|
|
|
Type: types.project,
|
2018-11-26 10:47:23 +00:00
|
|
|
Args: graphql.FieldConfigArgument{
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldID: &graphql.ArgumentConfig{
|
2018-11-28 16:20:23 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.String),
|
2018-11-26 10:47:23 +00:00
|
|
|
},
|
2021-08-02 23:06:15 +01:00
|
|
|
ProjectFields: &graphql.ArgumentConfig{
|
|
|
|
Type: graphql.NewNonNull(types.projectInput),
|
2020-09-10 10:32:35 +01:00
|
|
|
},
|
2021-08-02 23:06:15 +01:00
|
|
|
ProjectLimits: &graphql.ArgumentConfig{
|
|
|
|
Type: graphql.NewNonNull(types.projectLimit),
|
2018-11-26 10:47:23 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2021-08-24 22:12:07 +01:00
|
|
|
var projectInput, err = fromMapProjectInfoProjectLimits(p.Args[ProjectFields].(map[string]interface{}), p.Args[ProjectLimits].(map[string]interface{}))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-11-26 10:47:23 +00:00
|
|
|
|
2019-01-24 16:26:36 +00:00
|
|
|
inputID := p.Args[FieldID].(string)
|
2020-04-02 13:30:43 +01:00
|
|
|
projectID, err := uuid.FromString(inputID)
|
2018-11-26 10:47:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-08-02 23:06:15 +01:00
|
|
|
project, err := service.UpdateProject(p.Context, projectID, projectInput)
|
2019-09-04 16:02:39 +01:00
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2019-09-04 16:02:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return project, nil
|
2018-11-26 10:47:23 +00:00
|
|
|
},
|
|
|
|
},
|
2018-12-06 14:40:32 +00:00
|
|
|
// add user as member of given project
|
2019-01-24 16:26:36 +00:00
|
|
|
AddProjectMembersMutation: &graphql.Field{
|
2019-04-04 15:56:20 +01:00
|
|
|
Type: types.project,
|
2018-12-06 14:40:32 +00:00
|
|
|
Args: graphql.FieldConfigArgument{
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldProjectID: &graphql.ArgumentConfig{
|
2018-12-06 14:40:32 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.String),
|
|
|
|
},
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldEmail: &graphql.ArgumentConfig{
|
2018-12-20 15:36:32 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.NewList(graphql.String)),
|
2018-12-06 14:40:32 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2019-01-24 16:26:36 +00:00
|
|
|
pID, _ := p.Args[FieldProjectID].(string)
|
|
|
|
emails, _ := p.Args[FieldEmail].([]interface{})
|
2018-12-06 14:40:32 +00:00
|
|
|
|
2020-04-02 13:30:43 +01:00
|
|
|
projectID, err := uuid.FromString(pID)
|
2018-12-06 14:40:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-12-21 15:41:53 +00:00
|
|
|
var userEmails []string
|
|
|
|
for _, email := range emails {
|
|
|
|
userEmails = append(userEmails, email.(string))
|
2018-12-20 15:36:32 +00:00
|
|
|
}
|
|
|
|
|
2020-04-02 13:30:43 +01:00
|
|
|
project, err := service.GetProject(p.Context, projectID)
|
2019-03-06 15:42:19 +00:00
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2019-03-06 15:42:19 +00:00
|
|
|
}
|
|
|
|
|
2020-04-02 13:30:43 +01:00
|
|
|
users, err := service.AddProjectMembers(p.Context, projectID, userEmails)
|
2018-12-21 15:41:53 +00:00
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2018-12-20 15:36:32 +00:00
|
|
|
}
|
|
|
|
|
2019-03-26 15:56:16 +00:00
|
|
|
rootObject := p.Info.RootValue.(map[string]interface{})
|
|
|
|
origin := rootObject["origin"].(string)
|
|
|
|
signIn := origin + rootObject[SignInPath].(string)
|
|
|
|
|
2019-05-17 13:29:35 +01:00
|
|
|
for _, user := range users {
|
|
|
|
userName := user.ShortName
|
|
|
|
if user.ShortName == "" {
|
|
|
|
userName = user.FullName
|
2019-03-06 15:42:19 +00:00
|
|
|
}
|
2019-05-17 13:29:35 +01:00
|
|
|
|
2019-09-27 17:48:53 +01:00
|
|
|
contactInfoURL := rootObject[ContactInfoURL].(string)
|
|
|
|
letUsKnowURL := rootObject[LetUsKnowURL].(string)
|
|
|
|
termsAndConditionsURL := rootObject[TermsAndConditionsURL].(string)
|
|
|
|
|
2019-05-17 13:29:35 +01:00
|
|
|
mailService.SendRenderedAsync(
|
|
|
|
p.Context,
|
|
|
|
[]post.Address{{Address: user.Email, Name: userName}},
|
2022-07-14 14:44:06 +01:00
|
|
|
&console.ProjectInvitationEmail{
|
2019-09-27 17:48:53 +01:00
|
|
|
Origin: origin,
|
|
|
|
UserName: userName,
|
|
|
|
ProjectName: project.Name,
|
|
|
|
SignInLink: signIn,
|
|
|
|
LetUsKnowURL: letUsKnowURL,
|
|
|
|
TermsAndConditionsURL: termsAndConditionsURL,
|
|
|
|
ContactInfoURL: contactInfoURL,
|
2019-05-17 13:29:35 +01:00
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
2019-03-06 15:42:19 +00:00
|
|
|
|
2019-03-26 15:56:16 +00:00
|
|
|
return project, nil
|
2018-12-06 14:40:32 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
// delete user membership for given project
|
2019-01-24 16:26:36 +00:00
|
|
|
DeleteProjectMembersMutation: &graphql.Field{
|
2019-04-04 15:56:20 +01:00
|
|
|
Type: types.project,
|
2018-12-06 14:40:32 +00:00
|
|
|
Args: graphql.FieldConfigArgument{
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldProjectID: &graphql.ArgumentConfig{
|
2018-12-06 14:40:32 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.String),
|
|
|
|
},
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldEmail: &graphql.ArgumentConfig{
|
2018-12-20 15:36:32 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.NewList(graphql.String)),
|
2018-12-06 14:40:32 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2019-01-24 16:26:36 +00:00
|
|
|
pID, _ := p.Args[FieldProjectID].(string)
|
|
|
|
emails, _ := p.Args[FieldEmail].([]interface{})
|
2018-12-06 14:40:32 +00:00
|
|
|
|
2020-04-02 13:30:43 +01:00
|
|
|
projectID, err := uuid.FromString(pID)
|
2018-12-06 14:40:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-12-21 15:41:53 +00:00
|
|
|
var userEmails []string
|
|
|
|
for _, email := range emails {
|
|
|
|
userEmails = append(userEmails, email.(string))
|
2018-12-20 15:36:32 +00:00
|
|
|
}
|
|
|
|
|
2021-05-27 15:08:33 +01:00
|
|
|
project, err := service.GetProject(p.Context, projectID)
|
2018-12-21 15:41:53 +00:00
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2019-09-04 16:02:39 +01:00
|
|
|
}
|
|
|
|
|
2021-05-27 15:08:33 +01:00
|
|
|
err = service.DeleteProjectMembers(p.Context, project.ID, userEmails)
|
2019-09-04 16:02:39 +01:00
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2018-12-20 15:36:32 +00:00
|
|
|
}
|
|
|
|
|
2019-09-04 16:02:39 +01:00
|
|
|
return project, nil
|
2018-12-06 14:40:32 +00:00
|
|
|
},
|
|
|
|
},
|
2018-12-27 15:30:15 +00:00
|
|
|
// creates new api key
|
2019-01-24 16:26:36 +00:00
|
|
|
CreateAPIKeyMutation: &graphql.Field{
|
2019-04-04 15:56:20 +01:00
|
|
|
Type: types.createAPIKey,
|
2018-12-27 15:30:15 +00:00
|
|
|
Args: graphql.FieldConfigArgument{
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldProjectID: &graphql.ArgumentConfig{
|
2018-12-27 15:30:15 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.String),
|
|
|
|
},
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldName: &graphql.ArgumentConfig{
|
2018-12-27 15:30:15 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.String),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2020-04-02 13:30:43 +01:00
|
|
|
projectIDField, _ := p.Args[FieldProjectID].(string)
|
2019-01-24 16:26:36 +00:00
|
|
|
name, _ := p.Args[FieldName].(string)
|
2018-12-27 15:30:15 +00:00
|
|
|
|
2020-04-02 13:30:43 +01:00
|
|
|
projectID, err := uuid.FromString(projectIDField)
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-04-02 13:30:43 +01:00
|
|
|
info, key, err := service.CreateAPIKey(p.Context, projectID, name)
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return createAPIKey{
|
2019-05-24 17:51:27 +01:00
|
|
|
Key: key.Serialize(),
|
2018-12-27 15:30:15 +00:00
|
|
|
KeyInfo: info,
|
|
|
|
}, nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// deletes api key
|
2019-02-13 11:34:40 +00:00
|
|
|
DeleteAPIKeysMutation: &graphql.Field{
|
2019-04-04 15:56:20 +01:00
|
|
|
Type: graphql.NewList(types.apiKeyInfo),
|
2018-12-27 15:30:15 +00:00
|
|
|
Args: graphql.FieldConfigArgument{
|
2019-01-24 16:26:36 +00:00
|
|
|
FieldID: &graphql.ArgumentConfig{
|
2019-02-13 11:34:40 +00:00
|
|
|
Type: graphql.NewNonNull(graphql.NewList(graphql.String)),
|
2018-12-27 15:30:15 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2019-02-13 11:34:40 +00:00
|
|
|
paramKeysID, _ := p.Args[FieldID].([]interface{})
|
|
|
|
|
|
|
|
var keyIds []uuid.UUID
|
|
|
|
var keys []console.APIKeyInfo
|
|
|
|
for _, id := range paramKeysID {
|
2020-04-02 13:30:43 +01:00
|
|
|
keyID, err := uuid.FromString(id.(string))
|
2019-02-13 11:34:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-04-02 13:30:43 +01:00
|
|
|
key, err := service.GetAPIKeyInfo(p.Context, keyID)
|
2019-02-13 11:34:40 +00:00
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2019-02-13 11:34:40 +00:00
|
|
|
}
|
|
|
|
|
2020-04-02 13:30:43 +01:00
|
|
|
keyIds = append(keyIds, keyID)
|
2019-02-13 11:34:40 +00:00
|
|
|
keys = append(keys, *key)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2019-02-13 11:34:40 +00:00
|
|
|
err := service.DeleteAPIKeys(p.Context, keyIds)
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return nil, err
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2019-02-13 11:34:40 +00:00
|
|
|
return keys, nil
|
2018-12-27 15:30:15 +00:00
|
|
|
},
|
|
|
|
},
|
2019-07-10 21:29:26 +01:00
|
|
|
AddPaymentMethodMutation: &graphql.Field{
|
|
|
|
Type: graphql.Boolean,
|
2019-09-27 10:46:37 +01:00
|
|
|
Args: graphql.FieldConfigArgument{},
|
2019-07-10 21:29:26 +01:00
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2019-09-27 10:46:37 +01:00
|
|
|
return nil, nil
|
2019-07-10 21:29:26 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
DeletePaymentMethodMutation: &graphql.Field{
|
|
|
|
Type: graphql.Boolean,
|
2019-09-27 10:46:37 +01:00
|
|
|
Args: graphql.FieldConfigArgument{},
|
2019-07-10 21:29:26 +01:00
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2019-09-27 10:46:37 +01:00
|
|
|
return nil, nil
|
2019-07-10 21:29:26 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
SetDefaultPaymentMethodMutation: &graphql.Field{
|
|
|
|
Type: graphql.Boolean,
|
2019-09-27 10:46:37 +01:00
|
|
|
Args: graphql.FieldConfigArgument{},
|
2019-07-10 21:29:26 +01:00
|
|
|
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
2019-09-27 10:46:37 +01:00
|
|
|
return nil, nil
|
2019-07-10 21:29:26 +01:00
|
|
|
},
|
|
|
|
},
|
2018-11-14 10:50:15 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|