Skip to content

Commit

Permalink
feat(dev-api): INFRA-281 Emails 2FA and B2CAppAccessRight (#4484)
Browse files Browse the repository at this point in the history
* feat(dev-api): INFRA-281 added few tests

* feat(dev-api): INFRA-281 ConfirmEmailAction CRUD tests

* feat(dev-api): INFRA-281 Extend dev-portal message transports

* feat(dev-api): INFRA-281 fix message content

* feat(dev-api): INFRA-281 added ConfirmPhoneActionService tests

* feat(dev-api): INFRA-281 added B2CAppAccessRight model

* feat(dev-api): INFRA-281 added RegisterAppServiceUserService pre-draft

* feat(dev-api): INFRA-281 added RegisterAppServiceUserService draft

* feat(dev-api): INFRA-281 added RegisterAppServiceUserService tests

* feat(dev-api): INFRA-281 added some B2CAppAccessRight tests

* feat(dev-api): INFRA-281 few tests added

* feat(dev-api): INFRA-281 add import logic

* feat(dev-api): INFRA-281 added export fields and tests

* feat(dev-api): INFRA-281 more tests

* feat(dev-api): INFRA-281 added publish logic

* feat(dev-api): INFRA-281 added publish tests

* feat(dev-api): INFRA-281 added logs

* feat(dev-api): INFRA-281 fix tests

* fix(dev-api): INFRA-281 import tests fix

* fix(dev-api): INFRA-281 regenerate schema

* fix(dev-api): INFRA-281 fix env variables

* feat(keystone): INFRA-281 Select type reimplementation
  • Loading branch information
SavelevMatthew authored Mar 20, 2024
1 parent 5719ce7 commit a9051d8
Show file tree
Hide file tree
Showing 58 changed files with 5,567 additions and 1,190 deletions.
5 changes: 3 additions & 2 deletions apps/condo/domains/notification/constants/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ const MESSAGE_META = {
[DEV_PORTAL_MESSAGE_TYPE]: {
dv: { required: true },
body: { required: true },
subject: { required: false },
},
[SEND_BILLING_RECEIPTS_ON_PAYDAY_REMINDER_MESSAGE_TYPE]: {
dv: { required: true },
Expand Down Expand Up @@ -829,8 +830,8 @@ const MESSAGE_DELIVERY_OPTIONS = {
defaultTransports: [PUSH_TRANSPORT],
},
[DEV_PORTAL_MESSAGE_TYPE]: {
allowedTransports: [SMS_TRANSPORT],
defaultTransports: [SMS_TRANSPORT],
allowedTransports: [SMS_TRANSPORT, EMAIL_TRANSPORT],
defaultTransports: [SMS_TRANSPORT, EMAIL_TRANSPORT],
isAllowedToChangeDefaultTransport: false,
},
[SEND_BILLING_RECEIPTS_ON_PAYDAY_REMINDER_MESSAGE_TYPE]: {
Expand Down
1 change: 1 addition & 0 deletions apps/condo/lang/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,7 @@
"notification.messages.TICKET_COMMENT_CREATED.telegram.ticketType.organization": "(internal)",
"notification.messages.BILLING_RECEIPT_FILE_ADDED.email.subject": "{data.organization}. New billing receipt",
"notification.messages.SERVICE_USER_CREATED.email.subject": "Service account successfully registered",
"notification.messages.DEV_PORTAL_MESSAGE.email.subject": "{subject}",
"news.ConfirmDeleteTitle": "Delete news?",
"news.ConfirmDeleteMessage": "It cannot be restored. But you can recreate it.",
"news.CancelMessage": "Keep",
Expand Down
1 change: 1 addition & 0 deletions apps/condo/lang/ru/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,7 @@
"notification.messages.TICKET_COMMENT_CREATED.telegram.ticketType.organization": "(внутренний)",
"notification.messages.BILLING_RECEIPT_FILE_ADDED.email.subject": "{data.organization}. Квитанции за услуги ЖКХ",
"notification.messages.SERVICE_USER_CREATED.email.subject": "Сервисная учетная запись успешно зарегистрирована",
"notification.messages.DEV_PORTAL_MESSAGE.email.subject": "{subject}",
"news.ConfirmDeleteTitle": "Удалить новость?",
"news.ConfirmDeleteMessage": "Восстановить ее будет нельзя. Но можно создать заново.",
"news.CancelMessage": "Оставить",
Expand Down
3 changes: 3 additions & 0 deletions apps/condo/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -85091,6 +85091,9 @@ type Mutation {
},
"body": {
"required": true
},
"subject": {
"required": false
}
},
"SEND_BILLING_RECEIPTS_ON_PAYDAY_REMINDER_MESSAGE": {
Expand Down
3 changes: 3 additions & 0 deletions apps/condo/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40463,6 +40463,9 @@ export type Mutation = {
* },
* "body": {
* "required": true
* },
* "subject": {
* "required": false
* }
* },
* "SEND_BILLING_RECEIPTS_ON_PAYDAY_REMINDER_MESSAGE": {
Expand Down
1 change: 1 addition & 0 deletions apps/dev-api/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SMS_WHITE_LIST='{"+79990001234": "1234"}'
IP_WHITE_LIST='["::ffff:127.0.0.1"]'
SMS_PROVIDER=fake
EMAIL_PROVIDER=fake
FILE_FIELD_ADAPTER=local
18 changes: 18 additions & 0 deletions apps/dev-api/domains/common/gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ const SEND_MESSAGE_MUTATION = gql`
}
`

const REGISTER_SERVICE_USER_MUTATION = gql`
mutation registerNewServiceUser ($data: RegisterNewServiceUserInput!) {
result: registerNewServiceUser(data: $data) {
id
}
}
`

const GET_USER_EMAIL_QUERY = gql`
query getUserEmailById($id: ID!){
user: User(where: { id: $id }) {
email
}
}
`

module.exports = {
SEND_MESSAGE_MUTATION,
REGISTER_SERVICE_USER_MUTATION,
GET_USER_EMAIL_QUERY,
}
53 changes: 53 additions & 0 deletions apps/dev-api/domains/common/utils/email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const conf = require('@open-condo/config')

const { developmentClient, productionClient } = require('@dev-api/domains/common/utils/serverClients')

class FakeEmailAdapter {
async sendMessage (email, subject, body) {
console.log(JSON.stringify({ adapter: 'Fake Email Adapter', email, subject, body }))
}
}

class CondoEmailAdapter {
#client

constructor (serverClient) {
this.#client = serverClient
}

async sendMessage (email, subject, body) {
await this.#client.sendMessage({ email }, body, { subject })
}
}

class EmailAdapter {
#internalAdapter = null

constructor (type = 'faker') {
switch (type) {
case 'condo-dev':
this.#internalAdapter = new CondoEmailAdapter(developmentClient)
break
case 'condo-prod':
this.#internalAdapter = new CondoEmailAdapter(productionClient)
break
default:
this.#internalAdapter = new FakeEmailAdapter()
break
}
}

async sendMessage (email, subject, body) {
await this.#internalAdapter.sendMessage(email, subject, body)
}
}

const DEFAULT_EMAIL_ADAPTER = new EmailAdapter(conf['EMAIL_PROVIDER'] || 'fake')

async function sendMessage (email, subject, body) {
await DEFAULT_EMAIL_ADAPTER.sendMessage(email, subject, body)
}

module.exports = {
sendMessage,
}
3 changes: 2 additions & 1 deletion apps/dev-api/domains/common/utils/serverClients.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class CondoClient extends ApolloServerClient {
super(endpoint, authRequisites, opts)
}

async sendMessage (to, message) {
async sendMessage (to, message, extraMeta = {}) {
await this.executeAuthorizedMutation({
mutation: SEND_MESSAGE_MUTATION,
variables: {
Expand All @@ -49,6 +49,7 @@ class CondoClient extends ApolloServerClient {
meta: {
dv: 1,
body: message,
...extraMeta,
},
},
},
Expand Down
1 change: 1 addition & 0 deletions apps/dev-api/domains/common/utils/sms.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class SMSAdapter {
break
default:
this.#internalAdapter = new FakeSMSAdapter()
break
}
}

Expand Down
2 changes: 2 additions & 0 deletions apps/dev-api/domains/condo/gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ const CondoB2CAppGql = generateGqlQueries('B2CApp', '{ id }')
const CondoB2CAppBuildGql = generateGqlQueries('B2CAppBuild', '{ id }')
const CondoB2CAppPropertyGql = generateGqlQueries('B2CAppProperty', '{ id address deletedAt app { importId importRemoteSystem } }')
const CondoOIDCClientGql = generateGqlQueries('OidcClient', '{ id clientId payload name isEnabled }')
const CondoB2CAppAccessRightGql = generateGqlQueries('B2CAppAccessRight', '{ id app { id } user { id } }')

module.exports = {
CondoB2CAppGql,
CondoB2CAppBuildGql,
CondoB2CAppPropertyGql,
CondoOIDCClientGql,
CondoB2CAppAccessRightGql,
}
34 changes: 34 additions & 0 deletions apps/dev-api/domains/miniapp/access/B2CAppAccessRight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Generated by `createschema miniapp.B2CAppAccessRight 'app:Relationship:B2CApp:CASCADE; condoUserId:Text'`
*/

const { throwAuthenticationError } = require('@open-condo/keystone/apolloErrorFormatter')

const { canReadAppLinkedModelAsOwner } = require('@dev-api/domains/miniapp/utils/serverSchema/access')

async function canReadB2CAppAccessRights (args) {
const { authentication: { item: user } } = args
if (!user) return throwAuthenticationError()
if (user.deletedAt) return false

if (user.isAdmin || user.isSupport) return {}


return canReadAppLinkedModelAsOwner(args)
}

async function canManageB2CAppAccessRights ({ authentication: { item: user } }) {
if (!user) return throwAuthenticationError()
if (user.deletedAt) return false

return false
}

/*
Rules are logical functions that used for list access, and may return a boolean (meaning
all or no items are available) or a set of filters that limit the available items.
*/
module.exports = {
canReadB2CAppAccessRights,
canManageB2CAppAccessRights,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Generated by `createservice miniapp.RegisterAppServiceUserService`
*/
const { throwAuthenticationError } = require('@open-condo/keystone/apolloErrorFormatter')

const { canExecuteB2CAppMutationAsOwner } = require('@dev-api/domains/miniapp/utils/serverSchema/access')

async function canRegisterAppUserService (params) {
const { authentication: { item: user } } = params

if (!user) return throwAuthenticationError()
if (user.deletedAt) return false
if (user.isAdmin || user.isSupport) return true

return await canExecuteB2CAppMutationAsOwner(params)
}

/*
Rules are logical functions that used for list access, and may return a boolean (meaning
all or no items are available) or a set of filters that limit the available items.
*/
module.exports = {
canRegisterAppUserService,
}
2 changes: 2 additions & 0 deletions apps/dev-api/domains/miniapp/constants/constraints.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const B2C_APP_BUILD_UNIQUE_VERSION_CONSTRAINT = 'b2c_app_build_unique_version_app'
const B2C_APP_PUBLISH_REQUEST_UNIQUE_CONSTRAINT = 'b2c_app_publish_request_unique_app'
const B2C_APP_ACCESS_RIGHT_UNIQUE_APP_CONSTRAINT = 'b2c_app_access_right_unique_app'

module.exports = {
B2C_APP_BUILD_UNIQUE_VERSION_CONSTRAINT,
B2C_APP_PUBLISH_REQUEST_UNIQUE_CONSTRAINT,
B2C_APP_ACCESS_RIGHT_UNIQUE_APP_CONSTRAINT,
}
2 changes: 2 additions & 0 deletions apps/dev-api/domains/miniapp/constants/errors.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const INVALID_BUILD_VERSION = 'INVALID_BUILD_VERSION'
const FIRST_PUBLISH_WITHOUT_INFO = 'FIRST_PUBLISH_WITHOUT_INFO'
const APP_NOT_FOUND = 'APP_NOT_FOUND'
const ACCESS_RIGHT_ALREADY_EXISTS = 'ACCESS_RIGHT_ALREADY_EXISTS'
const CONDO_APP_NOT_FOUND = 'APP_NOT_FOUND'
const BUILD_NOT_FOUND = 'BUILD_NOT_FOUND'
const PUBLISH_NOT_ALLOWED = 'PUBLISH_NOT_ALLOWED'
Expand All @@ -20,4 +21,5 @@ module.exports = {
OIDC_CLIENT_NOT_FOUND,
INVALID_URL,
HTTPS_ONLY,
ACCESS_RIGHT_ALREADY_EXISTS,
}
12 changes: 12 additions & 0 deletions apps/dev-api/domains/miniapp/gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const EXPORT_FIELDS = AVAILABLE_ENVIRONMENTS.map(environment => `${environment}E
const B2C_APP_FIELDS = `{ name developer logo { publicUrl originalFilename } ${COMMON_FIELDS} ${EXPORT_FIELDS} }`
const B2CApp = generateGqlQueries('B2CApp', B2C_APP_FIELDS)

const B2C_APP_ACCESS_RIGHT_FIELDS = `{ app { id } condoUserId condoUserEmail environment ${COMMON_FIELDS} ${EXPORT_FIELDS} }`
const B2CAppAccessRight = generateGqlQueries('B2CAppAccessRight', B2C_APP_ACCESS_RIGHT_FIELDS)


const B2C_APP_BUILD_FIELDS = `{ app { id } version data { publicUrl originalFilename mimetype encoding } ${COMMON_FIELDS} ${EXPORT_FIELDS} }`
const B2CAppBuild = generateGqlQueries('B2CAppBuild', B2C_APP_BUILD_FIELDS)

Expand Down Expand Up @@ -78,10 +82,17 @@ const UPDATE_OIDC_CLIENT_URL_MUTATION = gql`
}
`

const REGISTER_APP_USER_SERVICE_MUTATION = gql`
mutation registerAppUserService ($data: RegisterAppUserServiceInput!) {
result: registerAppUserService(data: $data) { id }
}
`

/* AUTOGENERATE MARKER <CONST> */

module.exports = {
B2CApp,
B2CAppAccessRight,
B2CAppBuild,
B2CAppPublishRequest,
PUBLISH_B2C_APP_MUTATION,
Expand All @@ -93,5 +104,6 @@ module.exports = {
CREATE_OIDC_CLIENT_MUTATION,
GENERATE_OIDC_CLIENT_SECRET_MUTATION,
UPDATE_OIDC_CLIENT_URL_MUTATION,
REGISTER_APP_USER_SERVICE_MUTATION,
/* AUTOGENERATE MARKER <EXPORTS> */
}
90 changes: 90 additions & 0 deletions apps/dev-api/domains/miniapp/schema/B2CAppAccessRight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Generated by `createschema miniapp.B2CAppAccessRight 'app:Relationship:B2CApp:CASCADE; condoUserId:Text'`
*/

const get = require('lodash/get')

const { historical, versioned, uuided, tracked, softDeleted, dvAndSender } = require('@open-condo/keystone/plugins')
const { GQLListSchema } = require('@open-condo/keystone/schema')


const { GET_USER_EMAIL_QUERY } = require('@dev-api/domains/common/gql')
const { productionClient, developmentClient } = require('@dev-api/domains/common/utils/serverClients')
const access = require('@dev-api/domains/miniapp/access/B2CAppAccessRight')
const {
B2C_APP_ACCESS_RIGHT_UNIQUE_APP_CONSTRAINT,
} = require('@dev-api/domains/miniapp/constants/constraints')
const { AVAILABLE_ENVIRONMENTS, PROD_ENVIRONMENT } = require('@dev-api/domains/miniapp/constants/publishing')
const { exportable } = require('@dev-api/domains/miniapp/plugins/exportable')



const B2CAppAccessRight = new GQLListSchema('B2CAppAccessRight', {
schemaDoc:
'Link between service user and B2C App. ' +
'The existence of this connection means that ' +
'this condo user will have the rights to perform actions on behalf of the integration ' +
'and modify some B2CApp-related models, such as B2CAppProperty / B2CAppBuild ' +
'as soon as app will be published to specified environment',
fields: {
app: {
schemaDoc: 'Link to B2CApp',
type: 'Relationship',
ref: 'B2CApp',
isRequired: true,
knexOptions: { isNotNullable: true }, // Required relationship only!
kmigratorOptions: { null: false, on_delete: 'models.CASCADE' },
},
condoUserId: {
schemaDoc: 'ID of condo user, which will be linked to the published app',
type: 'Uuid',
isRequired: true,
},
condoUserEmail: {
schemaDoc: 'Email of service condo user linked to the published app',
type: 'Virtual',
graphQLReturnType: 'String',
resolver: async (item) => {
const { condoUserId, environment } = item
const serverClient = environment === PROD_ENVIRONMENT
? productionClient
: developmentClient
const response = await serverClient.executeAuthorizedQuery({
query: GET_USER_EMAIL_QUERY,
variables: { id: condoUserId },
})

return get(response, ['data', 'user', 'email'], null)
},
},
environment: {
schemaDoc: 'Condo environment',
type: 'Select',
options: AVAILABLE_ENVIRONMENTS,
isRequired: true,
graphQLReturnType: 'AppEnvironment',
},
},
kmigratorOptions: {
constraints: [
{
type: 'models.UniqueConstraint',
fields: ['environment', 'app'],
condition: 'Q(deletedAt__isnull=True)',
name: B2C_APP_ACCESS_RIGHT_UNIQUE_APP_CONSTRAINT,
},
],
},
plugins: [uuided(), versioned(), tracked(), softDeleted(), dvAndSender(), exportable(), historical()],
access: {
read: access.canReadB2CAppAccessRights,
create: access.canManageB2CAppAccessRights,
update: access.canManageB2CAppAccessRights,
delete: false,
auth: true,
},
})

module.exports = {
B2CAppAccessRight,
}
Loading

0 comments on commit a9051d8

Please sign in to comment.