diff --git a/addons/api/addon/generated/models/account.js b/addons/api/addon/generated/models/account.js new file mode 100644 index 000000000..1abfe3e3f --- /dev/null +++ b/addons/api/addon/generated/models/account.js @@ -0,0 +1,46 @@ +import BaseModel from '../../models/base'; +import { attr } from '@ember-data/model'; + +/** + * Account contains all fields related to an account resource + */ +export default class GeneratedAccountModel extends BaseModel { + + // =attributes + + @attr('string', { + description: 'The type of the resource, to help differentiate schemas' + }) type; + + @attr('string', { + description: 'The owning auth method ID.', + }) auth_method_id; + + @attr('string', { + description: 'Optional name for identification purposes', + }) + name; + + @attr('string', { + description: 'Optional user-set description for identification purposes', + }) + description; + + @attr('date', { + description: 'The time this resource was created\nOutput only.', + readOnly: true, + }) + created_time; + + @attr('date', { + description: 'The time this resource was last updated\nOutput only.', + readOnly: true, + }) + updated_time; + + @attr('number', { + description: 'Current version number of this resource.', + }) + version; + +} diff --git a/addons/api/addon/models/account.js b/addons/api/addon/models/account.js new file mode 100644 index 000000000..4c52347f5 --- /dev/null +++ b/addons/api/addon/models/account.js @@ -0,0 +1,36 @@ +import GeneratedAccountModel from '../generated/models/account'; +import { fragment } from 'ember-data-model-fragments/attributes'; + +export default class AccountModel extends GeneratedAccountModel { + + /** + * Attributes of this resource, if any, represented as a JSON fragment. + * @type {FragmentAccountAttributesModel} + */ + @fragment('fragment-account-attributes', { defaultValue: {} }) attributes; + + // =methods + + /** + * Save account password via the `set-password` method. + * See serializer and adapter for more information. + * @param {string} password + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + setPassword(password, options={ adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'set-password', + password + }; + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions + } + }); + } + +} diff --git a/addons/api/addon/models/fragment-account-attributes.js b/addons/api/addon/models/fragment-account-attributes.js new file mode 100644 index 000000000..76ee82487 --- /dev/null +++ b/addons/api/addon/models/fragment-account-attributes.js @@ -0,0 +1,10 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import { attr } from '@ember-data/model'; + +export default class FragmentAccountAttributesModel extends Fragment { + + @attr('string', { + description: 'The account login name' + }) login_name; + +} diff --git a/addons/api/addon/models/fragment-host-attributes.js b/addons/api/addon/models/fragment-host-attributes.js index 114da4bca..851b404b2 100644 --- a/addons/api/addon/models/fragment-host-attributes.js +++ b/addons/api/addon/models/fragment-host-attributes.js @@ -6,4 +6,5 @@ export default class FragmentHostAttributesModel extends Fragment { @attr('string', { description: 'The address (DNS or IP name) used to reach the host' }) address; + } diff --git a/addons/api/addon/serializers/account.js b/addons/api/addon/serializers/account.js new file mode 100644 index 000000000..c014c2361 --- /dev/null +++ b/addons/api/addon/serializers/account.js @@ -0,0 +1,39 @@ +import ApplicationSerializer from './application'; + +export default class AccountSerializer extends ApplicationSerializer { + + // =methods + + /** + * If `adapterOptions.method` is `set-password`, the serialization should + * include only **password** and the version. + * If `adapterOptions.password` is set, the serialization should + * include **password** in it's `attributes`. + * @override + * @param {Snapshot} snapshot + * @return {object} + */ + serialize(snapshot) { + const password = snapshot?.adapterOptions?.password; + let serialized = super.serialize(...arguments); + if (password && snapshot?.record?.isNew) serialized.attributes.password = password; + if (snapshot?.adapterOptions?.method === 'set-password') { + serialized = this.serializeForSetPassword(snapshot, password); + } + return serialized; + } + + /** + * Returns a payload containing only new password. + * @param {Snapshot} snapshot + * @param {string} password + * @return {object} + */ + serializeForSetPassword(snapshot, password) { + return { + version: snapshot.attr('version'), + password, + }; + } + +} diff --git a/addons/api/app/models/account.js b/addons/api/app/models/account.js new file mode 100644 index 000000000..83b737fe4 --- /dev/null +++ b/addons/api/app/models/account.js @@ -0,0 +1 @@ +export { default } from 'api/models/account'; diff --git a/addons/api/app/models/fragment-account-attributes.js b/addons/api/app/models/fragment-account-attributes.js new file mode 100644 index 000000000..8d0d8f53f --- /dev/null +++ b/addons/api/app/models/fragment-account-attributes.js @@ -0,0 +1 @@ +export { default } from 'api/models/fragment-account-attributes'; diff --git a/addons/api/app/serializers/account.js b/addons/api/app/serializers/account.js new file mode 100644 index 000000000..af9b2cc1c --- /dev/null +++ b/addons/api/app/serializers/account.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/account'; diff --git a/addons/api/tests/unit/models/account-test.js b/addons/api/tests/unit/models/account-test.js new file mode 100644 index 000000000..dcd3fe8bb --- /dev/null +++ b/addons/api/tests/unit/models/account-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | account', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let store = this.owner.lookup('service:store'); + let model = store.createRecord('account', {}); + assert.ok(model); + }); + +}); diff --git a/addons/api/tests/unit/serializers/account-test.js b/addons/api/tests/unit/serializers/account-test.js new file mode 100644 index 000000000..85f9b655f --- /dev/null +++ b/addons/api/tests/unit/serializers/account-test.js @@ -0,0 +1,97 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Serializer | account', function(hooks) { + setupTest(hooks); + + test('it serializes normally', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('account'); + const record = store.createRecord('account', { + type: 'password', + name: 'Name', + auth_method_id: '1', + description: 'Description', + attributes: { + login_name: 'Login Name', + }, + version: 1 + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = {}; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + type: 'password', + name: 'Name', + auth_method_id: '1', + description: 'Description', + attributes: { + login_name: 'Login Name' + }, + version: 1 + }); + }); + + test('it serializes password attribute when `adapterOptions.password` is true', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('account'); + const record = store.createRecord('account', { + type: 'password', + name: 'Name', + auth_method_id: '1', + description: 'Description', + attributes: { + login_name: 'Login Name', + }, + version: 1 + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = { + password: 'Password' + }; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + type: 'password', + name: 'Name', + auth_method_id: '1', + description: 'Description', + attributes: { + login_name: 'Login Name', + password: 'Password' + }, + version: 1 + }); + }); + + test('it does not serialize password for non-new records', function (assert) { + assert.expect(0); + }); + + test('it serializes only password when `adapterOptions.method` is set to `set-password`', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('account'); + const record = store.createRecord('account', { + type: 'password', + name: 'Name', + description: 'Description', + attributes: { + login_name: 'Login Name' + }, + auth_method_id: '1', + version: 1 + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = { + method: 'set-password', + password: 'Password' + }; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + password: 'Password', + version: 1 + }); + }); +}); diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 3b9e24cce..7f2ea5ba0 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -49,6 +49,30 @@ export default function() { this.patch('/auth-methods/:id'); this.del('/auth-methods/:id'); + // Auth Method Accounts + this.get('/accounts', ({ accounts }, { queryParams: { auth_method_id: authMethodId } }) => { + return accounts.where({ authMethodId }); + }); + this.post('/accounts'); + this.get('/accounts/:id'); + this.patch('/accounts/:id'); + this.del('/accounts/:id'); + this.post('/accounts/:idMethod', function ({ accounts }, { params: { idMethod } }) { + const attrs = this.normalizedRequestAttrs(); + const id = idMethod.split(':')[0]; + const method = idMethod.split(':')[1]; + const account = accounts.find(id); + const updatedAttrs = { + version: attrs.version, + attributes: account.attributes + }; + // Set new password + if (method === 'set-password') { + updatedAttrs.attributes.password = attrs.password; + } + return account.update(updatedAttrs); + }); + // Authenticate/deauthenticate routes this.post('/auth-methods/:id_method', authHandler); this.post('/scopes/:id_method', deauthHandler); diff --git a/ui/core/mirage/factories/account.js b/ui/core/mirage/factories/account.js new file mode 100644 index 000000000..5908d0a21 --- /dev/null +++ b/ui/core/mirage/factories/account.js @@ -0,0 +1,3 @@ +import factory from '../generated/factories/account'; + +export default factory.extend(); diff --git a/ui/core/mirage/factories/auth-method.js b/ui/core/mirage/factories/auth-method.js index aa15a1103..80254614a 100644 --- a/ui/core/mirage/factories/auth-method.js +++ b/ui/core/mirage/factories/auth-method.js @@ -1,3 +1,17 @@ import factory from '../generated/factories/auth-method'; +import { trait } from 'ember-cli-mirage'; -export default factory.extend(); +export default factory.extend({ + id: (i) => `auth-method-id-${i}`, + + /** + * Adds accounts to auth method. + */ + withAccounts: trait({ + afterCreate(authMethod, server) { + const { scope } = authMethod; + server.createList('account', 5, { scope, authMethod }); + } + }) + +}); diff --git a/ui/core/mirage/generated/factories/account.js b/ui/core/mirage/generated/factories/account.js new file mode 100644 index 000000000..a66184e1f --- /dev/null +++ b/ui/core/mirage/generated/factories/account.js @@ -0,0 +1,17 @@ +import { Factory } from 'ember-cli-mirage'; +import { random, date } from 'faker'; + +/** + * GeneratedAccountFactory + */ +export default Factory.extend({ + + type: 'password', + name: () => random.words(), + description: () => random.words(), + created_time: () => date.recent(), + updated_time: () => date.recent(), + version: () => random.number(), + attributes: () => ({ login_name: random.words() }), + +}); diff --git a/ui/core/mirage/models/account.js b/ui/core/mirage/models/account.js new file mode 100644 index 000000000..e6d3e303c --- /dev/null +++ b/ui/core/mirage/models/account.js @@ -0,0 +1,8 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + + scope: belongsTo(), + authMethod: belongsTo() + +}); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index 1524a7647..9492f92e3 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -15,7 +15,7 @@ export default function(server) { // Auth server.createList('auth-method', 3, { scope: globalScope }); - server.createList('auth-method', 3, { scope: orgScope }); + server.createList('auth-method', 3, { scope: orgScope }, 'withAccounts'); // Groups and Users server.createList('group', 5, { scope: orgScope }, 'withMembers'); diff --git a/ui/core/mirage/serializers/account.js b/ui/core/mirage/serializers/account.js new file mode 100644 index 000000000..85d38851b --- /dev/null +++ b/ui/core/mirage/serializers/account.js @@ -0,0 +1,12 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + modelName: 'account', + + _hashForModel(model) { + const json = ApplicationSerializer.prototype._hashForModel.apply(this, arguments); + json.auth_method_id = model.authMethodId; + delete json.attributes?.password; + return json; + } +});