285 files changed
+4754
-2202
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import RESTAdapter from '@ember-data/adapter/rest';
|
||||
import AdapterBuildURLMixin from '../mixins/adapter-build-url';
|
||||
import config from 'ember-get-config';
|
||||
import { get } from '@ember/object';
|
||||
import { get, getWithDefault } from '@ember/object';
|
||||
import { InvalidError } from '@ember-data/adapter/error';
|
||||
import { dasherize } from '@ember/string';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
@@ -36,7 +37,9 @@ function prenormalizeArrayResponse(response) {
|
||||
return payloadIsBlank(response) ? {items: []} : response;
|
||||
}
|
||||
|
||||
export default class ApplicationAdapter extends RESTAdapter {
|
||||
export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
AdapterBuildURLMixin
|
||||
) {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
@@ -55,6 +58,60 @@ export default class ApplicationAdapter extends RESTAdapter {
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Prepends a scope to the URL prefix. Precedence is given to the ID passed
|
||||
* via `adapterOptions.scopeID`. If this value isn't found in
|
||||
* `adapterOptions`, we look to the `scope.scope_id` field on the snapshot,
|
||||
* which most resources have.
|
||||
*
|
||||
* Note: scopes themselves never receive a scope URL prefix.
|
||||
*
|
||||
* @override
|
||||
* @param {string} path
|
||||
* @param {string} parentURL
|
||||
* @param {string} modelName
|
||||
* @param {string} id
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlPrefix(path, parentURL, modelName, id, snapshot) {
|
||||
const prefix = super.urlPrefix(...arguments);
|
||||
const isScope = modelName === 'scope';
|
||||
let scopePath = '';
|
||||
let scopeID = '';
|
||||
if (snapshot) {
|
||||
// Not all snapshots have `attr` (such as array snapshots),
|
||||
// so we do this sort of ugly check.
|
||||
if (snapshot.attr) {
|
||||
const parentScope = snapshot.attr('scope');
|
||||
if (parentScope) scopeID = parentScope.attr('scope_id');
|
||||
}
|
||||
// Attempt to get scopeID from adapterOptions and fallback on its current
|
||||
// value if it wasn't passed through options.
|
||||
scopeID = getWithDefault(snapshot, 'adapterOptions.scopeID', scopeID);
|
||||
}
|
||||
// Only non-scope resources need a scope path, since scope resources
|
||||
// aren't technically "scoped" the same way.
|
||||
// Ensure a slash is added between prefix + scope path if needed.
|
||||
if (!isScope && scopeID && prefix.charAt(prefix.length - 1) !== '/') {
|
||||
scopePath = `/scopes/${scopeID}`;
|
||||
}
|
||||
return `${prefix}${scopePath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a custom method after a colon, if a method is passed via the
|
||||
* snapshot's `adapterOptions.method` field.
|
||||
* @param {string} modelName
|
||||
* @param {string} id
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlSuffix(modelName, id, snapshot={}) {
|
||||
const method = getWithDefault(snapshot, 'adapterOptions.method', '');
|
||||
return method ? `:${method}` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the type to a dasherized string used in our API paths.
|
||||
*
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import ScopeAdapterMixin from '../mixins/scope-adapter';
|
||||
|
||||
export default class AuthMethodAdapter extends ApplicationAdapter.extend(
|
||||
ScopeAdapterMixin
|
||||
) {
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Auth method is scoped to org only.
|
||||
* @type {boolean}
|
||||
*/
|
||||
includeProject = false;
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import ScopeAdapterMixin from '../mixins/scope-adapter';
|
||||
|
||||
export default class GroupAdapter extends ApplicationAdapter.extend(
|
||||
ScopeAdapterMixin
|
||||
) {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Group is scoped to org only, for now.
|
||||
* @type {boolean}
|
||||
*/
|
||||
includeProject = false;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import ScopeAdapterMixin from '../mixins/scope-adapter';
|
||||
|
||||
export default class HostCatalogAdapter extends ApplicationAdapter.extend(
|
||||
ScopeAdapterMixin
|
||||
) { }
|
||||
@@ -1,14 +0,0 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import ScopeAdapterMixin from '../mixins/scope-adapter';
|
||||
|
||||
export default class ProjectAdapter extends ApplicationAdapter.extend(
|
||||
ScopeAdapterMixin
|
||||
) {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Project is scoped to org only, technically.
|
||||
* @type {boolean}
|
||||
*/
|
||||
includeProject = false;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import ScopeAdapterMixin from '../mixins/scope-adapter';
|
||||
|
||||
export default class RoleAdapter extends ApplicationAdapter.extend(
|
||||
ScopeAdapterMixin
|
||||
) {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Role is scoped to org only, for now.
|
||||
* @type {boolean}
|
||||
*/
|
||||
includeProject = false;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import { serializeIntoHash } from '@ember-data/adapter/-private';
|
||||
|
||||
export default class ScopeAdapter extends ApplicationAdapter {
|
||||
|
||||
/**
|
||||
* Identical to default method except that we pass a query parameter
|
||||
* indicating the parent scope.
|
||||
* @override
|
||||
* @param {Store} store
|
||||
* @param {Model} type
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {Promise} promise
|
||||
*/
|
||||
createRecord(store, type, snapshot) {
|
||||
const parentScopeID = snapshot.attr('scope').attr('scope_id');
|
||||
let url =
|
||||
this.buildURL(type.modelName, null, snapshot, 'createRecord');
|
||||
// TODO this should be generalized into `buildURL`, but currently Ember Data
|
||||
// doesn't provide a way to manage query parameters. They are passed on
|
||||
// to the underlying ajax/fetch service.
|
||||
url = `${url}?scope_id=${parentScopeID}`;
|
||||
const data = serializeIntoHash(store, type, snapshot);
|
||||
return this.ajax(url, 'POST', { data });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import ScopeAdapterMixin from '../mixins/scope-adapter';
|
||||
|
||||
export default class UserAdapter extends ApplicationAdapter.extend(
|
||||
ScopeAdapterMixin
|
||||
) {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Project is scoped to org only, technically.
|
||||
* @type {boolean}
|
||||
*/
|
||||
includeProject = false;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import BaseModel from '../../models/base';
|
||||
import { attr } from '@ember-data/model';
|
||||
|
||||
/**
|
||||
* Project contains all fields related to a Project resource
|
||||
*/
|
||||
export default class GeneratedProjectModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@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('boolean', {
|
||||
description: 'Whether the resource is disabled',
|
||||
})
|
||||
disabled;
|
||||
}
|
||||
+5
-1
@@ -4,10 +4,14 @@ import { attr } from '@ember-data/model';
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class GeneratedOrgModel extends BaseModel {
|
||||
export default class GeneratedScopeModel extends BaseModel {
|
||||
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource'
|
||||
}) type;
|
||||
|
||||
@attr('string', {
|
||||
description: 'Optional name for identification purposes',
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
import Mixin from '@ember/object/mixin';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
/**
|
||||
* Overrides URL-building methods in an adpater for greater flexibility.
|
||||
* Specifically enhances calls to `urlPrefix` with `modelName`, `id`, and
|
||||
* `snapshot`, which aren't normally passed. And adds support for a
|
||||
* `urlSuffix` method, the return value of which is appended to the end of
|
||||
* a generated URL.
|
||||
*
|
||||
* While this mixin provides an enhanced API, it doesn't actually change the
|
||||
* way URLs are generated by default. Override methods in the adapter that
|
||||
* uses this mixin to alter generated URLs.
|
||||
*/
|
||||
export default Mixin.create({
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* This override behaves identically to the default method by default.
|
||||
* It exists only to document the additional paramters to the method.
|
||||
* @override
|
||||
* @param {string} path
|
||||
* @param {string} parentURL
|
||||
* @param {string} modelName
|
||||
* @param {string} id
|
||||
* @param {object} snapshot
|
||||
* @return {string} urlPrefix
|
||||
*/
|
||||
urlPrefix(/* path, parentURL, modelName, id, snapshot */) {
|
||||
// What's this super syntax?
|
||||
// Ember mixins are still based on the older Ember Object, rather than
|
||||
// native ES classes. Thus we have to call super in this strange way.
|
||||
// https://blog.emberjs.com/2019/01/26/emberjs-native-class-update-2019-edition.html
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a string to be appended to a URL. Empty string by default.
|
||||
* @param {string} modelName
|
||||
* @param {string} id
|
||||
* @param {?object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlSuffix(/* modelName, id, snapshot */) {
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides the default `_buildURL` with a nearly identical version.
|
||||
* This method simply passes arguments into the `urlPrefix` method for
|
||||
* convenience, where by default it receives no arguments.
|
||||
*
|
||||
* Adds a suffix, generated by the `urlSuffix` method, to the URL.
|
||||
* @override
|
||||
* @param {String} modelName
|
||||
* @param {String} id
|
||||
* @return {String} url
|
||||
*/
|
||||
_buildURL(modelName, id, snapshot) {
|
||||
let path;
|
||||
let url = [];
|
||||
const host = get(this, 'host');
|
||||
const prefix = this.urlPrefix(null, null, modelName, id, snapshot);
|
||||
const suffix = this.urlSuffix(modelName, id, snapshot);
|
||||
if (modelName) {
|
||||
path = this.pathForType(modelName);
|
||||
if (path) url.push(path);
|
||||
}
|
||||
if (id) url.push(encodeURIComponent(id));
|
||||
if (prefix) url.unshift(prefix);
|
||||
url = url.join('/');
|
||||
if (!host && url && url.charAt(0) !== '/') url = '/' + url;
|
||||
return `${url}${suffix}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides the default method simply to pass an additional argument
|
||||
* `snapshot` to the `_buildURL` method.
|
||||
* @override
|
||||
* @param {string} id
|
||||
* @param {string} modelName
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlForFindRecord(id, modelName, snapshot) {
|
||||
return this._buildURL(modelName, id, snapshot);
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides the default method simply to pass an additional argument
|
||||
* `snapshot` to the `_buildURL` method.
|
||||
* @override
|
||||
* @param {string} modelName
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlForFindAll(modelName, snapshot) {
|
||||
return this._buildURL(modelName, null, snapshot);
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides the default method simply to pass an additional argument
|
||||
* `snapshot` to the `_buildURL` method.
|
||||
* @override
|
||||
* @param {string} id
|
||||
* @param {string} modelName
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlForFindBelongsTo(id, modelName, snapshot) {
|
||||
return this._buildURL(modelName, id, snapshot);
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides the default method simply to pass an additional argument
|
||||
* `snapshot` to the `_buildURL` method.
|
||||
* @override
|
||||
* @param {string} modelName
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlForCreateRecord(modelName, snapshot) {
|
||||
return this._buildURL(modelName, null, snapshot);
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides the default method simply to pass an additional argument
|
||||
* `snapshot` to the `_buildURL` method.
|
||||
* @override
|
||||
* @param {string} id
|
||||
* @param {string} modelName
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlForUpdateRecord(id, modelName, snapshot) {
|
||||
return this._buildURL(modelName, id, snapshot);
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides the default method simply to pass an additional argument
|
||||
* `snapshot` to the `_buildURL` method.
|
||||
* @override
|
||||
* @param {string} id
|
||||
* @param {string} modelName
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlForDeleteRecord(id, modelName, snapshot) {
|
||||
return this._buildURL(modelName, id, snapshot);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import Mixin from '@ember/object/mixin';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Add this mixin to resource adapters which should be accessed via an
|
||||
* org/project-scoped URL. This mixin appends a scope path based on the scope
|
||||
* reported by `ScopeService`.
|
||||
*/
|
||||
export default Mixin.create({
|
||||
// =services
|
||||
|
||||
scope: service(),
|
||||
|
||||
// =properties
|
||||
|
||||
/**
|
||||
* If true and the current scope has a project, will include the project in
|
||||
* the resource path. Set to false for resources that are scoped exclusively
|
||||
* to org.
|
||||
* @type {boolean}
|
||||
*/
|
||||
includeProject: true,
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Appends a scope path to the URL prefix, if a scope is set.
|
||||
* @override
|
||||
* @return {string}
|
||||
*/
|
||||
urlPrefix() {
|
||||
const prefix = this._super(...arguments);
|
||||
let scopePath = this.scopePath;
|
||||
// Ensure a slash is added between prefix + scope path if needed.
|
||||
if (scopePath && prefix.charAt(prefix.length - 1) !== '/') {
|
||||
scopePath = `/${scopePath}`;
|
||||
}
|
||||
return `${prefix}${scopePath}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a scope path for the current scope as reported by `ScopeService`.
|
||||
* If no scope is set, returns empty string.
|
||||
* @return {string}
|
||||
*/
|
||||
get scopePath() {
|
||||
const { org, project } = this.scope;
|
||||
const scopePath = [];
|
||||
if (org) scopePath.push('orgs', org.id);
|
||||
if (project && this.includeProject) scopePath.push('projects', project.id);
|
||||
return scopePath.join('/');
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,63 @@
|
||||
import Model from '@ember-data/model';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
import { computed } from '@ember/object';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
/**
|
||||
* Base model class provides common functionality for all models.
|
||||
*
|
||||
* One of the most interesting features of most resources is the
|
||||
* `scope` field, which arrives from the API as an embedded JSON fragment.
|
||||
* It contains information such as the parent scope ID and type. This scope ID
|
||||
* is then used when generation scoped resource URLs via the
|
||||
* `adapterOptions.scopeID` option (see `save` method below).
|
||||
*
|
||||
* For convenience, developers may interact with scope in a number of ways.
|
||||
* The simplest method, when you don't care about other information, is to set
|
||||
* the `scopeID`, e.g. `record.scopeID = 'o_123'`. The scope JSON fragment
|
||||
* may be set directly, `record.scope = { scope_id: 'o_123' }`.
|
||||
* For convenience, you may even set the scope using a `Scope` model:
|
||||
* `record.scopeModel = scopeModelInstance`, which automatically converts the
|
||||
* scope to a JSON fragment of the proper shape and assigns it to `scope`.
|
||||
*
|
||||
* A record's scope may be read in one of two ways:
|
||||
*
|
||||
* 1) As a JSON fragment: `record.scope`,
|
||||
* 2) Or as an ID: `record.scopeID`.
|
||||
*/
|
||||
export default class BaseModel extends Model {
|
||||
|
||||
// =relationships
|
||||
|
||||
/**
|
||||
* Scope of this resource, if any, represented as a JSON fragment.
|
||||
* @type {FragmentScope}
|
||||
*/
|
||||
@fragment('fragment-scope') scope;
|
||||
|
||||
// =attributes
|
||||
|
||||
set scopeModel(model) {
|
||||
if (model) {
|
||||
const json = model.serialize();
|
||||
json.scope_id = model.id;
|
||||
this.scope = json;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience for getting and setting the parent scope ID.
|
||||
* @type {string
|
||||
*/
|
||||
@computed('scope.scope_id')
|
||||
get scopeID() {
|
||||
return get(this, 'scope.scope_id');
|
||||
}
|
||||
set scopeID(id) {
|
||||
if (!this.scope) this.scope = {};
|
||||
this.scope.scope_id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Names are optional on models in our API. Thus we need to fallback on ID
|
||||
* for display purposes.
|
||||
@@ -29,4 +83,5 @@ export default class BaseModel extends Model {
|
||||
get cannotSave() {
|
||||
return !this.canSave;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { computed } from '@ember/object';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { scopeTypes } from './scope';
|
||||
|
||||
export default class FragmentScope extends Fragment {
|
||||
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description: 'ID of the related scope'
|
||||
}) scope_id;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource'
|
||||
}) type;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isGlobal() { return this.type === scopeTypes.global; }
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isOrg() { return this.type === scopeTypes.org; }
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isProject() { return this.type === scopeTypes.project; }
|
||||
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import GeneratedOrgModel from '../generated/models/org';
|
||||
|
||||
export default class OrgModel extends GeneratedOrgModel {}
|
||||
@@ -1,3 +0,0 @@
|
||||
import GeneratedProjectModel from '../generated/models/project';
|
||||
|
||||
export default class ProjectModel extends GeneratedProjectModel {}
|
||||
@@ -0,0 +1,46 @@
|
||||
import GeneratedScopeModel from '../generated/models/scope';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export const scopeTypes = {
|
||||
global: 'global',
|
||||
org: 'org',
|
||||
project: 'project'
|
||||
};
|
||||
|
||||
export default class ScopeModel extends GeneratedScopeModel {
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isGlobal() {
|
||||
return this.type === scopeTypes.global;
|
||||
}
|
||||
// There is only one global scope and it cannot be created by clients,
|
||||
// thus no set.
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isOrg() {
|
||||
return this.type === scopeTypes.org;
|
||||
}
|
||||
set isOrg(value) {
|
||||
if (value) this.type = scopeTypes.org;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('type')
|
||||
get isProject() {
|
||||
return this.type === scopeTypes.project;
|
||||
}
|
||||
set isProject(value) {
|
||||
if (value) this.type = scopeTypes.project;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import RESTSerializer from '@ember-data/serializer/rest';
|
||||
import { underscore } from '@ember/string';
|
||||
import { get } from '@ember/object';
|
||||
import { copy } from 'ember-copy';
|
||||
|
||||
/**
|
||||
* Manages serialization/normalization of data to/from the API.
|
||||
*/
|
||||
export default class ApplicationSerializer extends RESTSerializer {
|
||||
|
||||
// =attributes
|
||||
|
||||
attrs = {
|
||||
scope: { serialize: false }
|
||||
};
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
@@ -91,7 +99,7 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
|
||||
/**
|
||||
* In our API, singluar resources are _unrooted_, whereas Ember Data expects
|
||||
* them to be rooted under their model name, e.g. `{project: {…}}`.
|
||||
* them to be rooted under their model name, e.g. `{modelName: {…}}`.
|
||||
* This method makes this transformation to accommodate Ember Data.
|
||||
*
|
||||
* @method normalizeSingleResponse
|
||||
@@ -110,7 +118,7 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
primaryModelClass.modelName
|
||||
);
|
||||
// Copy the unrooted payload under the expected root key name.
|
||||
transformedPayload[payloadKey] = copy(payload);
|
||||
transformedPayload[payloadKey] = copy(payload, true);
|
||||
// Return the result of normalizing the transformed payload.
|
||||
return super.normalizeSingleResponse(
|
||||
store,
|
||||
@@ -120,4 +128,22 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
requestType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One consequence of using model fragments to represent the embedded scope
|
||||
* is that they cannot have `id` fields. We still need to know scope IDs,
|
||||
* so we copy the `scope.id` value into the `scope.scope_id` field.
|
||||
* @override
|
||||
* @see FragmentScope
|
||||
* @param {Model} typeClass
|
||||
* @param {Object} hash
|
||||
* @return {Object}
|
||||
*/
|
||||
normalize(typeClass, hash) {
|
||||
const normalizedHash = copy(hash, true);
|
||||
const scopeID = get(normalizedHash, 'scope.id');
|
||||
if (scopeID) normalizedHash.scope.scope_id = scopeID;
|
||||
return super.normalize(typeClass, normalizedHash);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import Service from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
/**
|
||||
* This module includes two parts:
|
||||
*
|
||||
* - `Scope` class: a data structure representing an org/project tuple
|
||||
* - `ScopeService`: manages the scope and exposes it to the application
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a _scope_, which is defined as an organization/project tuple.
|
||||
* This class is intended as a simple data structure and is used by the
|
||||
* scope service (see below).
|
||||
* Immutable.
|
||||
*/
|
||||
class Scope {
|
||||
/**
|
||||
* @type {?object}
|
||||
* @private
|
||||
*/
|
||||
#org = null;
|
||||
|
||||
/**
|
||||
* @type {?object}
|
||||
* @private
|
||||
*/
|
||||
#project = null;
|
||||
|
||||
/**
|
||||
* Initializes an instance of `Scope` with optional org and project.
|
||||
* A scope with only project is invalid and in thi scenario, project will
|
||||
* be null.
|
||||
* @param {?object} org
|
||||
* @param {?object} project
|
||||
*/
|
||||
constructor(org = null, project = null) {
|
||||
if (org) this.#org = org;
|
||||
if (org && project) this.#project = project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Org for this scope. Immutable.
|
||||
* @type {?object}
|
||||
*/
|
||||
get org() {
|
||||
return this.#org;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project for this scope.
|
||||
* @type {?object}
|
||||
*/
|
||||
get project() {
|
||||
return this.#project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON representation of this scope.
|
||||
* @return {?object}
|
||||
*/
|
||||
toJSON() {
|
||||
const { org, project } = this;
|
||||
return { org, project };
|
||||
}
|
||||
}
|
||||
|
||||
export { Scope };
|
||||
|
||||
/**
|
||||
* The scope service manages the current scope, making it available throughout
|
||||
* the Ember application. Use `service.org` and `service.project` directly,
|
||||
* rather than interacting with the internal `scope` member.
|
||||
*
|
||||
* @example
|
||||
* // inject the scope service
|
||||
* import { inject as service } from '@ember/service';
|
||||
* class MyClass {
|
||||
* @service scope;
|
||||
* }
|
||||
*
|
||||
* // set an org, project, or both
|
||||
* this.scope.org = {id: 1};
|
||||
* this.scope.project = {id: 2};
|
||||
*
|
||||
* // retrieve the org and/or project
|
||||
* const org = this.scope.org;
|
||||
* const project = this.scope.project;
|
||||
*/
|
||||
export default class ScopeService extends Service {
|
||||
/**
|
||||
* The currently selected scope. This is an internal member only.
|
||||
* @type {?Scope}
|
||||
*/
|
||||
@tracked scope = new Scope();
|
||||
|
||||
/**
|
||||
* When setting org, `service.org = {...}`, a new scope is instantiated,
|
||||
* since scopes are immutable.
|
||||
* @type {?object}
|
||||
*/
|
||||
@computed('scope.org')
|
||||
get org() {
|
||||
return this.scope ? this.scope.org : null;
|
||||
}
|
||||
set org(org) {
|
||||
this.scope = new Scope(org, this.project);
|
||||
}
|
||||
|
||||
/**
|
||||
* When setting project, `service.project = {...}`, a new scope is
|
||||
* instantiated, since scopes are immutable.
|
||||
* @type {?object}
|
||||
*/
|
||||
@computed('scope.project')
|
||||
get project() {
|
||||
return this.scope ? this.scope.project : null;
|
||||
}
|
||||
set project(project) {
|
||||
this.scope = new Scope(this.org, project);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/adapters/auth-method';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/adapters/group';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/adapters/host-catalog';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/adapters/project';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/adapters/role';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/adapters/scope';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/adapters/user';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/fragment-scope';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/models/org';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/models/project';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/scope';
|
||||
@@ -1 +0,0 @@
|
||||
export { default, Scope } from 'api/services/scope';
|
||||
@@ -41,6 +41,7 @@
|
||||
"ember-cli-uglify": "^3.0.0",
|
||||
"ember-copy": "^1.0.0",
|
||||
"ember-data": "^3.16.0",
|
||||
"ember-data-model-fragments": "5.0.0-beta.0",
|
||||
"ember-disable-prototype-extensions": "^1.1.3",
|
||||
"ember-export-application-global": "^2.0.1",
|
||||
"ember-get-config": "^0.2.4",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ApplicationRoute extends Route {
|
||||
@service scope;
|
||||
|
||||
model() {
|
||||
this.store.findAll('project');
|
||||
this.store.findAll('scope');
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,86 @@ module('Unit | Adapter | application', function (hooks) {
|
||||
});
|
||||
|
||||
test('it generates correct URL prefixes', function (assert) {
|
||||
assert.expect(2);
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
assert.ok(config.api.namespace);
|
||||
assert.equal(adapter.urlPrefix(), config.api.namespace);
|
||||
});
|
||||
|
||||
test('it generates URL prefixes with optional `scopeID` from adapterOptions', function (assert) {
|
||||
assert.expect(2);
|
||||
const scopeID = 'global';
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
const prefix = adapter.urlPrefix(null, null, null, null, {
|
||||
adapterOptions: { scopeID }
|
||||
});
|
||||
assert.ok(config.api.namespace);
|
||||
assert.equal(prefix, `v1/scopes/${scopeID}`);
|
||||
});
|
||||
|
||||
test('it does not prefixes URLs with `scopeID` for scope resources', function (assert) {
|
||||
assert.expect(1);
|
||||
const scopeID = 'global';
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
const prefix = adapter.urlPrefix(null, null, 'scope', null, {
|
||||
adapterOptions: { scopeID }
|
||||
});
|
||||
assert.equal(prefix, `v1`);
|
||||
});
|
||||
|
||||
test('it generates correct default URL suffixes', function (assert) {
|
||||
assert.expect(1);
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
assert.equal(adapter.urlPrefix(), config.api.namespace);
|
||||
assert.equal(adapter.urlSuffix(), '');
|
||||
});
|
||||
|
||||
test('it generates URL suffixes with optional `method` from adapterOptions', function (assert) {
|
||||
assert.expect(2);
|
||||
const method = 'set-something';
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
const suffix = adapter.urlSuffix(null, null, {
|
||||
adapterOptions: { method }
|
||||
});
|
||||
assert.ok(config.api.namespace);
|
||||
assert.equal(suffix, ':set-something');
|
||||
});
|
||||
|
||||
test('it generates correct complete URLs', function (assert) {
|
||||
assert.expect(6);
|
||||
const scopeID = 'o_1';
|
||||
const method = 'my-custom-method';
|
||||
const mockSnapshot = {
|
||||
adapterOptions: {
|
||||
scopeID,
|
||||
method
|
||||
}
|
||||
};
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
// test URL generation for each request type...
|
||||
const findRecordURL = adapter.buildURL('user', '1', mockSnapshot, 'findRecord');
|
||||
assert.equal(findRecordURL, '/v1/scopes/o_1/users/1:my-custom-method');
|
||||
const findAllURL = adapter.buildURL('user', null, mockSnapshot, 'findAll');
|
||||
assert.equal(findAllURL, '/v1/scopes/o_1/users:my-custom-method');
|
||||
const findBelongsToURL = adapter.buildURL('user', '2', mockSnapshot, 'findBelongsTo');
|
||||
assert.equal(findBelongsToURL, '/v1/scopes/o_1/users/2:my-custom-method');
|
||||
const createRecordURL = adapter.buildURL('user', null, mockSnapshot, 'createRecord');
|
||||
assert.equal(createRecordURL, '/v1/scopes/o_1/users:my-custom-method');
|
||||
const updateRecordURL = adapter.buildURL('user', '3', mockSnapshot, 'updateRecord');
|
||||
assert.equal(updateRecordURL, '/v1/scopes/o_1/users/3:my-custom-method');
|
||||
const deleteRecordURL = adapter.buildURL('user', '4', mockSnapshot, 'deleteRecord');
|
||||
assert.equal(deleteRecordURL, '/v1/scopes/o_1/users/4:my-custom-method');
|
||||
});
|
||||
|
||||
test('it can request records through the store from a specified scope', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.server.get('/v1/scopes/p_456/groups', () => {
|
||||
assert.ok(true, 'Scoped resource URL was requested.');
|
||||
return {items: []};
|
||||
});
|
||||
await store.findAll('group', {
|
||||
adapterOptions: { scopeID: 'p_456' }
|
||||
});
|
||||
});
|
||||
|
||||
test('it rewrites PUT to PATCH, but leaves others unchanged', function (assert) {
|
||||
@@ -37,10 +114,10 @@ module('Unit | Adapter | application', function (hooks) {
|
||||
|
||||
test('it prenormalizes "empty" responses into a form that the fetch-manager will not reject', async function (assert) {
|
||||
assert.expect(1);
|
||||
const adapter = this.owner.lookup('adapter:project');
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.server.get('/v1/projects', () => new Response({}));
|
||||
const prenormalized = await adapter.findAll(store, {modelName: 'project'}, null, []);
|
||||
this.server.get('/v1/users', () => new Response({}));
|
||||
const prenormalized = await adapter.findAll(store, {modelName: 'user'}, null, []);
|
||||
assert.deepEqual(prenormalized, {items: []});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import config from 'ember-get-config';
|
||||
|
||||
module('Unit | Adapter | auth method', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it generates URL prefixes without project scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
service.org = { id: 1 };
|
||||
service.project = { id: 2 };
|
||||
const adapter = this.owner.lookup('adapter:auth-method');
|
||||
assert.equal(adapter.urlPrefix(), `${config.api.namespace}/orgs/1`);
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import config from 'ember-get-config';
|
||||
|
||||
module('Unit | Adapter | group', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let adapter = this.owner.lookup('adapter:group');
|
||||
assert.ok(adapter);
|
||||
});
|
||||
|
||||
test('it generates URL prefixes without project scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
service.org = { id: 1 };
|
||||
service.project = { id: 2 };
|
||||
const adapter = this.owner.lookup('adapter:group');
|
||||
assert.equal(adapter.urlPrefix(), `${config.api.namespace}/orgs/1`);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Adapter | host catalog', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let adapter = this.owner.lookup('adapter:host-catalog');
|
||||
assert.ok(adapter);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import config from 'ember-get-config';
|
||||
|
||||
module('Unit | Adapter | project', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it generates URL prefixes without project scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
service.org = { id: 1 };
|
||||
service.project = { id: 2 };
|
||||
const adapter = this.owner.lookup('adapter:project');
|
||||
assert.equal(adapter.urlPrefix(), `${config.api.namespace}/orgs/1`);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import config from 'ember-get-config';
|
||||
|
||||
module('Unit | Adapter | role', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it generates URL prefixes without project scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
service.org = { id: 1 };
|
||||
service.project = { id: 2 };
|
||||
const adapter = this.owner.lookup('adapter:role');
|
||||
assert.equal(adapter.urlPrefix(), `${config.api.namespace}/orgs/1`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Adapter | scope', function(hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it generates correct createRecord URLs with a scope_id query parameter', function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/scopes', (schema, { queryParams }) =>{
|
||||
assert.equal(queryParams.scope_id, 'o_1');
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
const scope = store.createRecord('scope', {
|
||||
scopeID: 'o_1'
|
||||
});
|
||||
const type = scope.constructor;
|
||||
const snapshot = scope._createSnapshot();
|
||||
const adapter = this.owner.lookup('adapter:scope');
|
||||
adapter.createRecord(store, type, snapshot);
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import config from 'ember-get-config';
|
||||
|
||||
module('Unit | Adapter | user', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let adapter = this.owner.lookup('adapter:user');
|
||||
assert.ok(adapter);
|
||||
});
|
||||
|
||||
test('it generates URL prefixes without project scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
service.org = { id: 1 };
|
||||
service.project = { id: 2 };
|
||||
const adapter = this.owner.lookup('adapter:user');
|
||||
assert.equal(adapter.urlPrefix(), `${config.api.namespace}/orgs/1`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import RESTAdapter from '@ember-data/adapter/rest';
|
||||
import AdapterBuildURLMixin from 'api/mixins/adapter-build-url';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Mixin | build-url', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it does not interfere with basic URLs', function (assert) {
|
||||
assert.expect(1);
|
||||
const Adapter = RESTAdapter.extend(AdapterBuildURLMixin);
|
||||
this.owner.register('adapter:adapter', Adapter);
|
||||
const adapter = this.owner.lookup('adapter:adapter');
|
||||
const url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/models/1');
|
||||
});
|
||||
|
||||
test('it does not interfere with namespaced URLs', function (assert) {
|
||||
assert.expect(1);
|
||||
const Adapter = RESTAdapter.extend(AdapterBuildURLMixin, {
|
||||
namespace: 'api/v1'
|
||||
});
|
||||
this.owner.register('adapter:adapter', Adapter);
|
||||
const adapter = this.owner.lookup('adapter:adapter');
|
||||
const url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/api/v1/models/1');
|
||||
});
|
||||
|
||||
test('it can prepend a prefix', function (assert) {
|
||||
assert.expect(1);
|
||||
const Adapter = RESTAdapter.extend(AdapterBuildURLMixin, {
|
||||
urlPrefix() {
|
||||
return '/foo/bars'
|
||||
}
|
||||
});
|
||||
this.owner.register('adapter:adapter', Adapter);
|
||||
const adapter = this.owner.lookup('adapter:adapter');
|
||||
const url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/foo/bars/models/1');
|
||||
});
|
||||
|
||||
test('it can append a suffix', function (assert) {
|
||||
assert.expect(1);
|
||||
const Adapter = RESTAdapter.extend(AdapterBuildURLMixin, {
|
||||
urlSuffix() {
|
||||
return ':method'
|
||||
}
|
||||
});
|
||||
this.owner.register('adapter:adapter', Adapter);
|
||||
const adapter = this.owner.lookup('adapter:adapter');
|
||||
const url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/models/1:method');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import RESTAdapter from '@ember-data/adapter/rest';
|
||||
import ScopeAdapterMixin from 'api/mixins/scope-adapter';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Mixin | scope-adapter', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it does not interfere with basic URLs', function (assert) {
|
||||
assert.expect(1);
|
||||
//const service = this.owner.lookup('service:scope');
|
||||
const ScopeAdapter = RESTAdapter.extend(ScopeAdapterMixin);
|
||||
this.owner.register('adapter:scope', ScopeAdapter);
|
||||
const adapter = this.owner.lookup('adapter:scope');
|
||||
const url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/models/1');
|
||||
});
|
||||
|
||||
test('it does not interfere with namespaced URLs', function (assert) {
|
||||
assert.expect(1);
|
||||
//const service = this.owner.lookup('service:scope');
|
||||
const ScopeAdapter = RESTAdapter.extend(ScopeAdapterMixin, {
|
||||
namespace: 'api/v1',
|
||||
});
|
||||
this.owner.register('adapter:scope', ScopeAdapter);
|
||||
const adapter = this.owner.lookup('adapter:scope');
|
||||
const url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/api/v1/models/1');
|
||||
});
|
||||
|
||||
test('it adds a scope to basic URLs when scope is set', function (assert) {
|
||||
assert.expect(4);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
service.org = { id: 1 };
|
||||
const ScopeAdapter = RESTAdapter.extend(ScopeAdapterMixin, {
|
||||
namespace: 'api/v1',
|
||||
});
|
||||
this.owner.register('adapter:scope', ScopeAdapter);
|
||||
const adapter = this.owner.lookup('adapter:scope');
|
||||
let url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/api/v1/orgs/1/models/1');
|
||||
service.project = { id: 2 };
|
||||
url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/api/v1/orgs/1/projects/2/models/1');
|
||||
service.org = { id: 'foobar' };
|
||||
url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/api/v1/orgs/foobar/projects/2/models/1');
|
||||
// can't have a project without an org
|
||||
service.org = null;
|
||||
url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/api/v1/models/1');
|
||||
});
|
||||
|
||||
test('it does not add a project scope to basic URLs when includeProject is false', function (assert) {
|
||||
assert.expect(1);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
service.org = { id: 1 };
|
||||
service.project = { id: 2 };
|
||||
const ScopeAdapter = RESTAdapter.extend(ScopeAdapterMixin, {
|
||||
namespace: 'api/v1',
|
||||
includeProject: false,
|
||||
});
|
||||
this.owner.register('adapter:scope', ScopeAdapter);
|
||||
const adapter = this.owner.lookup('adapter:scope');
|
||||
let url = adapter._buildURL('model', 1);
|
||||
assert.equal(url, '/api/v1/orgs/1/models/1');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Model | base', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
@@ -11,17 +13,54 @@ module('Unit | Model | base', function (hooks) {
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it may have a scope fragment', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: 'o_1',
|
||||
type: 'scope'
|
||||
}
|
||||
});
|
||||
store.push({
|
||||
data: {
|
||||
id: '123abc',
|
||||
type: 'user',
|
||||
attributes: {
|
||||
scope: {
|
||||
scope_id: 'o_1',
|
||||
type: 'scope'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const scope = store.peekRecord('scope', 'o_1');
|
||||
const model = store.peekRecord('user', '123abc');
|
||||
assert.equal(model.scope.scope_id, scope.id);
|
||||
});
|
||||
|
||||
test('it may accept a `scopeModel` for convenience, instead of a fragment', function (assert) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const scope = store.createRecord('scope', {id: 'o_1', type: 'org'});
|
||||
const model = store.createRecord('user', '123abc');
|
||||
assert.notEqual(model.scopeID, scope.id);
|
||||
model.scopeModel = scope;
|
||||
assert.equal(model.scopeID, scope.id);
|
||||
assert.equal(model.scope.isOrg, true);
|
||||
});
|
||||
|
||||
test('it has a displayName attribute', function (assert) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: '123abc',
|
||||
type: 'project',
|
||||
type: 'user',
|
||||
attributes: {},
|
||||
},
|
||||
});
|
||||
const model = store.peekRecord('project', '123abc');
|
||||
const model = store.peekRecord('user', '123abc');
|
||||
assert.equal(model.name, null);
|
||||
assert.equal(model.displayName, '123abc');
|
||||
model.name = 'Test';
|
||||
@@ -34,15 +73,15 @@ module('Unit | Model | base', function (hooks) {
|
||||
store.push({
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'project',
|
||||
type: 'user',
|
||||
attributes: {},
|
||||
},
|
||||
});
|
||||
const model = store.peekRecord('project', '1');
|
||||
const model = store.peekRecord('user', '1');
|
||||
assert.equal(model.canSave, false);
|
||||
assert.equal(model.cannotSave, true);
|
||||
// Should be able to save if dirty
|
||||
model.name = 'Project';
|
||||
model.name = 'User';
|
||||
assert.equal(model.hasDirtyAttributes, true);
|
||||
assert.equal(model.canSave, true);
|
||||
assert.equal(model.cannotSave, false);
|
||||
@@ -53,4 +92,61 @@ module('Unit | Model | base', function (hooks) {
|
||||
assert.equal(model.cannotSave, true);
|
||||
model.transitionTo('loaded');
|
||||
});
|
||||
|
||||
test('it saves records to a scoped URL', async function (assert) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const model = store.createRecord('user', { scopeID: 'o_123' });
|
||||
this.server.post('/v1/scopes/o_123/users', () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.')
|
||||
return {};
|
||||
});
|
||||
await model.save();
|
||||
this.server.patch('/v1/scopes/o_123/users', () => {
|
||||
assert.ok(true, 'Correctly scoped update record URL was requested.')
|
||||
return {};
|
||||
});
|
||||
await model.save();
|
||||
this.server.delete('/v1/scopes/o_123/users', () => {
|
||||
assert.ok(true, 'Correctly scoped delete record URL was requested.')
|
||||
return {};
|
||||
});
|
||||
await model.destroyRecord();
|
||||
});
|
||||
|
||||
test('it saves to a custom scoped URL if requested', async function (assert) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const customScopeID = 'global';
|
||||
const scope = {scope_id: 'o_123'};
|
||||
const model = store.createRecord('user', { scope });
|
||||
this.server.post('/v1/scopes/global/users', () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.')
|
||||
return {};
|
||||
});
|
||||
await model.save({adapterOptions: {scopeID: customScopeID}});
|
||||
this.server.patch('/v1/scopes/global/users', () => {
|
||||
assert.ok(true, 'Correctly scoped update record URL was requested.')
|
||||
return {};
|
||||
});
|
||||
await model.save({adapterOptions: {scopeID: customScopeID}});
|
||||
this.server.delete('/v1/scopes/global/users', () => {
|
||||
assert.ok(true, 'Correctly scoped delete record URL was requested.')
|
||||
return {};
|
||||
});
|
||||
await model.destroyRecord({adapterOptions: {scopeID: customScopeID}});
|
||||
});
|
||||
|
||||
test('it saves records to a URL with a custom method if requested', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const method = 'my-custom-method';
|
||||
const scope = {scope_id: 'o_123'};
|
||||
const model = store.createRecord('user', { scope });
|
||||
this.server.post(`/v1/scopes/o_123/users:${method}`, () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.')
|
||||
return {};
|
||||
});
|
||||
await model.save({adapterOptions: {method}});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Model | org', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('org', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Model | project', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('project', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Model | scope', function(hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it may have a scope fragment', async function(assert) {
|
||||
assert.expect(6);
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.server.get('/v1/scopes', () => ({
|
||||
items: [
|
||||
{id: 'global', type: 'global'},
|
||||
{id: 'o_1', type: 'org', scope: {scope_id: 'global'}},
|
||||
{id: 'o_2', type: 'org', scope: {scope_id: 'global'}},
|
||||
{id: 'p_1', type: 'project', scope: {scope_id: 'o_1'}},
|
||||
{id: 'p_2', type: 'project', scope: {scope_id: 'o_1'}},
|
||||
{id: 'p_3', type: 'project', scope: {scope_id: 'o_2'}},
|
||||
]
|
||||
}));
|
||||
const scopes = await store.findAll('scope');
|
||||
// check integrity of scope relationships
|
||||
assert.notOk(await scopes.firstObject.get('scope'), 'Global scope has no parent');
|
||||
assert.equal(await scopes.objectAt(1).get('scope.scope_id'), 'global', 'Org 1 parent scope is global');
|
||||
assert.equal(await scopes.objectAt(2).get('scope.scope_id'), 'global', 'Org 2 parent scope is global');
|
||||
assert.equal(await scopes.objectAt(3).get('scope.scope_id'), 'o_1', 'Project 1 parent scope is org 1');
|
||||
assert.equal(await scopes.objectAt(4).get('scope.scope_id'), 'o_1', 'Project 2 parent scope is org 1');
|
||||
assert.equal(await scopes.objectAt(5).get('scope.scope_id'), 'o_2', 'Project 3 parent scope is org 2');
|
||||
});
|
||||
|
||||
test('it has isType boolean getters and setters', async function(assert) {
|
||||
assert.expect(9);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const model = store.createRecord('scope', {type: 'global'});
|
||||
assert.ok(model.isGlobal);
|
||||
assert.notOk(model.isOrg);
|
||||
assert.notOk(model.isProject);
|
||||
model.isOrg = true;
|
||||
assert.notOk(model.isGlobal);
|
||||
assert.ok(model.isOrg);
|
||||
assert.notOk(model.isProject);
|
||||
model.isProject = true;
|
||||
assert.notOk(model.isGlobal);
|
||||
assert.notOk(model.isOrg);
|
||||
assert.ok(model.isProject);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -14,33 +14,36 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
test('it serializes records', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('project', {
|
||||
name: 'Project',
|
||||
const record = store.createRecord('user', {
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
scope: {
|
||||
scope_id: 'global',
|
||||
type: 'global'
|
||||
}
|
||||
});
|
||||
const serializedRecord = record.serialize();
|
||||
assert.deepEqual(serializedRecord, {
|
||||
name: 'Project',
|
||||
description: 'Description',
|
||||
//disabled: false, // TODO: disabled is temporarily disabled
|
||||
name: 'User',
|
||||
description: 'Description'
|
||||
});
|
||||
});
|
||||
|
||||
test('it normalizes array records from an `items` root key', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('project');
|
||||
const projectModelClass = store.createRecord('project').constructor;
|
||||
const serializer = store.serializerFor('user');
|
||||
const userModelClass = store.createRecord('user').constructor;
|
||||
const payload = {
|
||||
items: [
|
||||
{ id: 1, name: 'Project 1' },
|
||||
{ id: 2, name: 'Project 2' },
|
||||
{ id: 3, name: 'Project 3' },
|
||||
{ id: 1, name: 'User 1', scope: { id: 'o_123' } },
|
||||
{ id: 2, name: 'User 2', scope: { id: 'o_123' } },
|
||||
{ id: 3, name: 'User 3', scope: { id: 'o_123' } },
|
||||
],
|
||||
};
|
||||
const normalizedArray = serializer.normalizeArrayResponse(
|
||||
store,
|
||||
projectModelClass,
|
||||
userModelClass,
|
||||
payload
|
||||
);
|
||||
assert.deepEqual(normalizedArray, {
|
||||
@@ -48,20 +51,20 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'project',
|
||||
attributes: { name: 'Project 1' },
|
||||
type: 'user',
|
||||
attributes: { name: 'User 1', scope: { scope_id: 'o_123' } },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'project',
|
||||
attributes: { name: 'Project 2' },
|
||||
type: 'user',
|
||||
attributes: { name: 'User 2', scope: { scope_id: 'o_123' } },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'project',
|
||||
attributes: { name: 'Project 3' },
|
||||
type: 'user',
|
||||
attributes: { name: 'User 3', scope: { scope_id: 'o_123' } },
|
||||
relationships: {},
|
||||
},
|
||||
],
|
||||
@@ -71,23 +74,24 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
test('it normalizes single, unrooted records', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('project');
|
||||
const projectModelClass = store.createRecord('project').constructor;
|
||||
const serializer = store.serializerFor('user');
|
||||
const userModelClass = store.createRecord('user').constructor;
|
||||
const payload = {
|
||||
id: '1',
|
||||
name: 'Project 1',
|
||||
name: 'User 1',
|
||||
scope: { id: 'o_123' }
|
||||
};
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
projectModelClass,
|
||||
userModelClass,
|
||||
payload
|
||||
);
|
||||
assert.deepEqual(normalized, {
|
||||
included: [],
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'project',
|
||||
attributes: { name: 'Project 1' },
|
||||
type: 'user',
|
||||
attributes: { name: 'User 1', scope: { scope_id: 'o_123' } },
|
||||
relationships: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { Scope } from '../../../services/scope';
|
||||
|
||||
module('Unit | Service | scope class', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it provides a scope class', function (assert) {
|
||||
assert.expect(1);
|
||||
assert.ok(Scope);
|
||||
});
|
||||
|
||||
test('it has initializable org and project members', function (assert) {
|
||||
assert.expect(2);
|
||||
const org = { id: 1 };
|
||||
const project = { id: 2 };
|
||||
const scope = new Scope(org, project);
|
||||
assert.equal(scope.org.id, 1);
|
||||
assert.equal(scope.project.id, 2);
|
||||
});
|
||||
|
||||
test('it has a null project if org is null, even if project was initialized to non-null', function (assert) {
|
||||
assert.expect(2);
|
||||
const org = null;
|
||||
const project = { id: 2 };
|
||||
const scope = new Scope(org, project);
|
||||
assert.notOk(scope.org);
|
||||
assert.notOk(scope.project);
|
||||
});
|
||||
|
||||
test('it serializes to JSON', function (assert) {
|
||||
assert.expect(2);
|
||||
let scope = new Scope({ id: 1 });
|
||||
assert.deepEqual(scope.toJSON(), {
|
||||
org: { id: 1 },
|
||||
project: null,
|
||||
});
|
||||
scope = new Scope({ id: 1 }, { id: 2 });
|
||||
assert.deepEqual(scope.toJSON(), {
|
||||
org: { id: 1 },
|
||||
project: { id: 2 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module('Unit | Service | scope', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it returns nulls when scope is not set', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
assert.equal(service.org, null);
|
||||
assert.equal(service.project, null);
|
||||
});
|
||||
|
||||
test('it aliases org and project to a scope instance', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:scope');
|
||||
const org = { id: 1 };
|
||||
const project = { id: 2 };
|
||||
service.org = org;
|
||||
service.project = project;
|
||||
assert.equal(service.org, service.scope.org);
|
||||
assert.equal(service.project, service.scope.project);
|
||||
});
|
||||
});
|
||||
Whitespace-only changes.
@@ -0,0 +1,87 @@
|
||||
import BaseAuthenticator from 'ember-simple-auth/authenticators/base';
|
||||
import { resolve, reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
|
||||
/**
|
||||
*
|
||||
* This authenticator is intended for use with Ember Simple Auth.
|
||||
*
|
||||
* Simple id/password base authenticator sends credentials to an endpoint
|
||||
* specified by the `buildAuthEndpointURL`. If the HTTP response code is in the
|
||||
* success range, authentication resolves. Otherwise it rejects.
|
||||
*
|
||||
* Upon session invalidation, deauthentication is attempted at the URL generated
|
||||
* by `buildDeauthEndpointURL`, but is not guaranteed.
|
||||
*
|
||||
* This authenticator should not be used directly because it does not specify
|
||||
* an `buildAuthEndpointURL` or `buildDeauthEndpointURL` of its own.
|
||||
* To use, generate an application authenticator in your app
|
||||
* `authenticators/application.js` and extend this class.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* import PasswordAuthenticator from 'auth/authenticators/password';
|
||||
* export default class ApplicationAuthenticator extends PasswordAuthenticator {
|
||||
* authEndpoint = '/api/authenticate';
|
||||
* deauthEndpoint = '/api/deauthenticate';
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export default class PasswordAuthenticator extends BaseAuthenticator {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Restores the session if data is present. Otherwise rejects.
|
||||
* @override
|
||||
* @param {object} data
|
||||
* @return {Promise}
|
||||
*/
|
||||
restore(data) {
|
||||
return data ? resolve(data) : reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts credentials to the URL specified in `authEndpoint` and resolves
|
||||
* if a success HTTP status code is received, otherwise rejects.
|
||||
*
|
||||
* If requesting cookies, be sure to use the cookie session store. Otherwise
|
||||
* use the local storage session store.
|
||||
*
|
||||
* @override
|
||||
* @param {object} creds
|
||||
* @param {string} creds.identification is sent to server as `name`
|
||||
* @param {string} creds.password
|
||||
* @param {boolean} requestCookies request cookie tokens (default `true`)
|
||||
* @return {Promise}
|
||||
*/
|
||||
async authenticate(
|
||||
{ identification: name, password },
|
||||
requestCookies = true,
|
||||
options
|
||||
) {
|
||||
const body = JSON.stringify({
|
||||
token_type: requestCookies ? 'cookie' : null,
|
||||
credentials: { name, password },
|
||||
});
|
||||
const authEndpointURL = this.buildAuthEndpointURL(options);
|
||||
const response = await fetch(authEndpointURL, { method: 'post', body });
|
||||
const json = await response.json();
|
||||
return response.status < 400 ? resolve(json) : reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts to the `deauthEntpoint` on a best-effort basis and then returns.
|
||||
* Deauthentication with the server is not guaranteed and request failures
|
||||
* are ignored.
|
||||
*
|
||||
* @override
|
||||
* @return {Promise}
|
||||
*/
|
||||
invalidate(options) {
|
||||
const deauthEndpointURL = this.buildDeauthEndpointURL(options);
|
||||
fetch(deauthEndpointURL, { method: 'post' }).catch(() => {
|
||||
/* no op */
|
||||
});
|
||||
return super.invalidate(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import BaseSessionStore from 'ember-simple-auth/session-stores/base';
|
||||
import { resolve, reject } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
*
|
||||
* This session store is intended for use with Ember Simple Auth when requesting
|
||||
* that the server send cookies (see authenticators).
|
||||
*
|
||||
* The cookie session store is used to maintain session state for auth servers
|
||||
* that send cookies. In this strategy, the server sets any cookies needed for
|
||||
* future authorization. This store determines if a session is authenticated
|
||||
* by looking for the presence of a cookie with name specified in `cookieName`.
|
||||
*
|
||||
* This session store should not be used directly because it does not specify
|
||||
* `cookiePath` or `cookieName`. To use, create an application session store
|
||||
* and extend this class, being sure to specify these attributes.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* import CookieSessionStore from 'auth/session-stores/cookie';
|
||||
* export default class ApplicationSessionStore extends CookieSessionStore {
|
||||
* cookiePath = '/';
|
||||
* cookieName = 'auth-token';
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export default class CookieSessionStore extends BaseSessionStore {
|
||||
// =services
|
||||
|
||||
@service cookies;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* The name of the cookie that stores the name of the authenticator, derived
|
||||
* from the name of the session cookie. The authenticator name is used
|
||||
* internally by Ember Simple Auth, since it supports simultaneous
|
||||
* authentication schemes within a single application.
|
||||
* @type {string}
|
||||
*/
|
||||
get authenticatorCookieName() {
|
||||
return `${this.cookieName}-authenticator`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the authenticator used, if any.
|
||||
* @type {?string}
|
||||
*/
|
||||
get authenticatorName() {
|
||||
return this.cookies.read(this.authenticatorCookieName);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if both session and authenticator cookies are present.
|
||||
* False otherwise.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get sessionExists() {
|
||||
const tokenExists = this.cookies.exists(this.cookieName);
|
||||
const authenticatorExists = this.cookies.exists(
|
||||
this.authenticatorCookieName
|
||||
);
|
||||
return tokenExists && authenticatorExists;
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* This method is called automatically on initialization to perform cleanup
|
||||
* if necessary.
|
||||
*
|
||||
* Sessions exist only if both the session and authenticator cookies are
|
||||
* present. If either one is missing, we want to cleanup to be sure they
|
||||
* are both absent.
|
||||
*/
|
||||
init() {
|
||||
if (!this.sessionExists) this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* While the primary session cookie is persisted by the server, not the
|
||||
* application, we still need to "remember" the name of the authenticator
|
||||
* used to authenticate to properly restore a session. Thus when this method
|
||||
* is called with an authenticator name, we save it into a cookie with name
|
||||
* `authenticatorCookieName`. If this method is called without an
|
||||
* authenticator, all related cookies are cleared.
|
||||
* @param {object} data
|
||||
* @return {Promise}
|
||||
*/
|
||||
persist(data) {
|
||||
const authenticator = get(data, 'authenticated.authenticator');
|
||||
const path = this.cookiePath;
|
||||
if (authenticator) {
|
||||
this.cookies.write(this.authenticatorCookieName, authenticator, { path });
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
return resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* If a session exists (as reported by `sessionExists`), then this method
|
||||
* resolves with the authentication object expected by Ember Simple Auth,
|
||||
* which must specify the name of the authenticator used to authenticate.
|
||||
*
|
||||
* If the session does not exist, all related cookies are cleared and this
|
||||
* method rejects.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
restore() {
|
||||
if (this.sessionExists) {
|
||||
return resolve({
|
||||
authenticated: {
|
||||
authenticator: this.authenticatorName,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.clear();
|
||||
return reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears both cookies related to the session: the server specified session
|
||||
* cookie as well as the authenticator name cookie.
|
||||
*/
|
||||
clear() {
|
||||
const path = this.cookiePath;
|
||||
this.cookies.clear(this.cookieName, { path });
|
||||
this.cookies.clear(this.authenticatorCookieName, { path });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import LocalStorageSessionStore from 'ember-simple-auth/session-stores/local-storage';
|
||||
|
||||
/**
|
||||
*
|
||||
* This session store is intended for use with Ember Simple Auth when requesting
|
||||
* that the server send a full token (e.g. cookies are not requested,
|
||||
* see authenticators).
|
||||
*
|
||||
* NOTE: when using a local storage scheme, authorization is not provided
|
||||
* automatically. In order to achieve authorization... TODO
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* import LocalStorageSessionStore from 'auth/session-stores/local-storage';
|
||||
* export default class ApplicationSessionStore extends LocalStorageSessionStore {
|
||||
*
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export default class AuthLocalStorageSessionStore extends LocalStorageSessionStore {}
|
||||
@@ -33,16 +33,20 @@
|
||||
"ember-cli-dependency-checker": "^3.2.0",
|
||||
"ember-cli-eslint": "^5.1.0",
|
||||
"ember-cli-inject-live-reload": "^2.0.2",
|
||||
"ember-cli-mirage": "^1.1.7",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-template-lint": "^1.0.0-beta.3",
|
||||
"ember-cli-uglify": "^3.0.0",
|
||||
"ember-cookies": "^0.5.2",
|
||||
"ember-data": "^3.16.0",
|
||||
"ember-disable-prototype-extensions": "^1.1.3",
|
||||
"ember-export-application-global": "^2.0.1",
|
||||
"ember-fetch": "^8.0.1",
|
||||
"ember-load-initializers": "^2.1.1",
|
||||
"ember-maybe-import-regenerator": "^0.1.6",
|
||||
"ember-qunit": "^4.6.0",
|
||||
"ember-resolver": "^7.0.0",
|
||||
"ember-simple-auth": "^3.0.0",
|
||||
"ember-source": "~3.16.0",
|
||||
"ember-source-channel-url": "^2.0.1",
|
||||
"ember-try": "^1.4.0",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import PasswordAuthenticator from 'auth/authenticators/password';
|
||||
|
||||
export default class ApplicationPasswordAuthenticator extends PasswordAuthenticator {
|
||||
|
||||
buildAuthEndpointURL() {
|
||||
return '/authenticate';
|
||||
}
|
||||
|
||||
buildDeauthEndpointURL() {
|
||||
return '/deauthenticate';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import CookieSessionStore from 'auth/session-stores/cookie';
|
||||
|
||||
export default class ApplicationCookieSessionStore extends CookieSessionStore {
|
||||
cookiePath = '/';
|
||||
cookieName = 'my-cookie';
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import LocalStorageSessionStore from 'auth/session-stores/local-storage';
|
||||
|
||||
export default class ApplicationLocalStorageSessionStore extends LocalStorageSessionStore {}
|
||||
@@ -0,0 +1,22 @@
|
||||
export default function () {
|
||||
// These comments are here to help you get started. Feel free to delete them.
|
||||
/*
|
||||
Config (with defaults).
|
||||
|
||||
Note: these only affect routes defined *after* them!
|
||||
*/
|
||||
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
|
||||
// this.namespace = ''; // make this `/api`, for example, if your API is namespaced
|
||||
// this.timing = 400; // delay for each request, automatically set to 0 during testing
|
||||
/*
|
||||
Shorthand cheatsheet:
|
||||
|
||||
this.get('/posts');
|
||||
this.post('/posts');
|
||||
this.get('/posts/:id');
|
||||
this.put('/posts/:id'); // or this.patch
|
||||
this.del('/posts/:id');
|
||||
|
||||
https://www.ember-cli-mirage.com/docs/route-handlers/shorthands
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function (/* server */) {
|
||||
/*
|
||||
Seed your development database using your factories.
|
||||
This data will not be loaded in your tests.
|
||||
*/
|
||||
// server.createList('post', 10);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { JSONAPISerializer } from 'ember-cli-mirage';
|
||||
|
||||
export default JSONAPISerializer.extend({});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { Response } from 'miragejs';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Authenticator | password', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it authenticates to the specified authEndpoint', async function (assert) {
|
||||
assert.expect(2);
|
||||
const authenticator = this.owner.lookup('authenticator:password');
|
||||
this.server.post(authenticator.buildAuthEndpointURL(), (schema, request) => {
|
||||
const json = JSON.parse(request.requestBody);
|
||||
assert.ok(json.token_type, 'Requested token cookies by default');
|
||||
return new Response(200);
|
||||
});
|
||||
await authenticator.authenticate({}).then(() => {
|
||||
assert.ok(true, 'authentication succeeded');
|
||||
});
|
||||
});
|
||||
|
||||
test('it can authenticate without requesting cookies', async function (assert) {
|
||||
assert.expect(2);
|
||||
const authenticator = this.owner.lookup('authenticator:password');
|
||||
this.server.post(authenticator.buildAuthEndpointURL(), (schema, request) => {
|
||||
const json = JSON.parse(request.requestBody);
|
||||
assert.notOk(json.token_type, 'Did not request tokens cookies');
|
||||
return new Response(200);
|
||||
});
|
||||
await authenticator.authenticate({}, false).then(() => {
|
||||
assert.ok(true, 'authentication succeeded');
|
||||
});
|
||||
});
|
||||
|
||||
test('it authenticates with the expected payload', async function (assert) {
|
||||
assert.expect(1);
|
||||
const authenticator = this.owner.lookup('authenticator:password');
|
||||
this.server.post(authenticator.buildAuthEndpointURL(), (schema, request) => {
|
||||
const json = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(json, {
|
||||
token_type: 'cookie',
|
||||
credentials: {
|
||||
name: 'foo',
|
||||
password: 'bar',
|
||||
},
|
||||
});
|
||||
return new Response(200);
|
||||
});
|
||||
const creds = {
|
||||
identification: 'foo',
|
||||
password: 'bar',
|
||||
};
|
||||
await authenticator.authenticate(creds);
|
||||
});
|
||||
|
||||
test('it rejects if the endpoint sends an error status code', async function (assert) {
|
||||
assert.expect(1);
|
||||
const authenticator = this.owner.lookup('authenticator:password');
|
||||
this.server.post(authenticator.buildAuthEndpointURL(), () => {
|
||||
return new Response(400);
|
||||
});
|
||||
await authenticator.authenticate({}).catch(() => {
|
||||
assert.ok(true, 'authentication failed');
|
||||
});
|
||||
});
|
||||
|
||||
test('it deauthenticates on invalidation', async function (assert) {
|
||||
assert.expect(1);
|
||||
const authenticator = this.owner.lookup('authenticator:password');
|
||||
this.server.post(authenticator.buildDeauthEndpointURL(), () => {
|
||||
assert.ok(true, 'deauthentication occurred');
|
||||
return new Response(200);
|
||||
});
|
||||
await authenticator.invalidate();
|
||||
});
|
||||
|
||||
test('it invalidation succeeds even if the deauthentication request fails', async function (assert) {
|
||||
assert.expect(2);
|
||||
const authenticator = this.owner.lookup('authenticator:password');
|
||||
this.server.post(authenticator.buildDeauthEndpointURL(), () => {
|
||||
assert.ok(true, 'deauthentication was requested');
|
||||
return new Response(500);
|
||||
});
|
||||
await authenticator
|
||||
.invalidate()
|
||||
.then(() => assert.ok(true))
|
||||
.catch(() => assert.notOk(true, 'uh oh, this should not happen'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Session Store | cookie', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let sessionStore;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
sessionStore = this.owner.lookup('session-store:cookie');
|
||||
sessionStore.clear();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
sessionStore.clear();
|
||||
});
|
||||
|
||||
test('it restores a session if both necessary cookies are present (session exists)', async function (assert) {
|
||||
assert.expect(2);
|
||||
sessionStore.cookies.write(sessionStore.cookieName, 'token-value', {
|
||||
path: '/',
|
||||
});
|
||||
sessionStore.cookies.write(
|
||||
sessionStore.authenticatorCookieName,
|
||||
'authenticator-name',
|
||||
{ path: '/' }
|
||||
);
|
||||
assert.ok(sessionStore.sessionExists);
|
||||
const session = await sessionStore.restore();
|
||||
assert.deepEqual(session, {
|
||||
authenticated: {
|
||||
authenticator: 'authenticator-name',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it rejects restore if the session does not exist', async function (assert) {
|
||||
assert.expect(2);
|
||||
assert.notOk(sessionStore.sessionExists);
|
||||
await sessionStore.restore().catch(() => {});
|
||||
assert.ok(true, 'Rejected');
|
||||
});
|
||||
|
||||
test('it cleans up cookies on restore rejection', async function (assert) {
|
||||
assert.expect(4);
|
||||
// For session cookie
|
||||
sessionStore.cookies.write(sessionStore.cookieName, 'token-value', {
|
||||
path: '/',
|
||||
});
|
||||
assert.ok(
|
||||
sessionStore.cookies.exists(sessionStore.cookieName),
|
||||
'cookie exists'
|
||||
);
|
||||
await sessionStore.restore().catch(() => {});
|
||||
assert.notOk(
|
||||
sessionStore.cookies.exists(sessionStore.cookieName),
|
||||
'cookie was cleared'
|
||||
);
|
||||
// For authenticator name cookie
|
||||
sessionStore.cookies.write(
|
||||
sessionStore.authenticatorCookieName,
|
||||
'authenticator-name',
|
||||
{ path: '/' }
|
||||
);
|
||||
assert.ok(
|
||||
sessionStore.cookies.exists(sessionStore.authenticatorCookieName),
|
||||
'cookie exists'
|
||||
);
|
||||
await sessionStore.restore().catch(() => {});
|
||||
assert.notOk(
|
||||
sessionStore.cookies.exists(sessionStore.authenticatorCookieName),
|
||||
'cookie was cleared'
|
||||
);
|
||||
});
|
||||
|
||||
test('it persists only an authenticator name cookie, since the server persists the session cookie', async function (assert) {
|
||||
assert.expect(4);
|
||||
assert.notOk(sessionStore.cookies.exists(sessionStore.cookieName));
|
||||
assert.notOk(
|
||||
sessionStore.cookies.exists(sessionStore.authenticatorCookieName)
|
||||
);
|
||||
sessionStore.persist({ authenticated: { authenticator: 'foobar' } });
|
||||
assert.notOk(
|
||||
sessionStore.cookies.exists(sessionStore.cookieName),
|
||||
'session cookie was not set'
|
||||
);
|
||||
assert.equal(
|
||||
sessionStore.cookies.read(sessionStore.authenticatorCookieName),
|
||||
'foobar'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
.rose-form-input-field {
|
||||
@include type.type(m);
|
||||
box-sizing: border-box;
|
||||
margin-bottom: sizing.rems(xs);
|
||||
padding: sizing.rems(xs);
|
||||
color: var(--stark);
|
||||
|
||||
@@ -9,7 +9,7 @@ $width-breakpoints: (
|
||||
xlarge: 1200px,
|
||||
large: 1024px,
|
||||
medium: 768px,
|
||||
small: 480px,
|
||||
small: 540px,
|
||||
xsmall: 320px,
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
"lint:addons:rose": "echo \"\n\nlint: addons/rose\n\" && cd addons/rose && yarn lint",
|
||||
"lint:ui:core": "echo \"\n\nlint: ui/core\n\" && cd ui/core && yarn lint",
|
||||
"lint:ui:connect-client": "echo \"\n\nlint: ui/connect-client\n\" && cd ui/connect-client && yarn lint",
|
||||
"test": "yarn test:addons:api && yarn test:addons:auth && yarn test:addons:rose && yarn test:ui:core && yarn test:ui:connect-client",
|
||||
"test": "yarn test:addons:api && yarn test:addons:auth && yarn test:addons:rose && yarn test:ui:core",
|
||||
"test:addons:api": "echo \"\n\ntest: addons/api\n\" && cd addons/api && yarn test",
|
||||
"test:addons:auth": "echo \"\n\ntest: addons/auth\n\" && cd addons/auth && yarn test",
|
||||
"test:addons:rose": "echo \"\n\ntest: addons/rose\n\" && cd addons/rose && yarn test",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import BasePasswordAuthenticator from 'auth/authenticators/password';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* A username/password authenticator that authenticates with
|
||||
* a scope/auth-method endpoint. Authentication occurs on an auth method
|
||||
* endpoint, while deauthentication occurs on a scope endpoint.
|
||||
*/
|
||||
export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Generates an auth method URL with which to authenticate.
|
||||
* @override
|
||||
* @param {object} options
|
||||
* @param {string} options.scope.scope_id
|
||||
* @param {string} options.authMethod.id
|
||||
* @return {string}
|
||||
*/
|
||||
buildAuthEndpointURL({
|
||||
scope: { id: scopeID },
|
||||
authMethod: { id: authMethodID },
|
||||
}) {
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const options = { adapterOptions: { scopeID, method: 'authenticate' } };
|
||||
return adapter.buildURL('auth-method', authMethodID, options, 'findRecord');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a scope URL with which to deauthenticate.
|
||||
* @override
|
||||
* @param {object} options
|
||||
* @param {string} scopeID
|
||||
* @return {string}
|
||||
*/
|
||||
buildDeauthEndpointURL({ org_id: scopeID }) {
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const options = { adapterOptions: { method: 'deauthenticate' } };
|
||||
return adapter.buildURL('scope', scopeID, options, 'findRecord');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ApplicationController extends Controller {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
export default class OrgsOrgGroupsGroupController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@alias('model.displayName') breadCrumb;
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
export default class OrgsOrgProjectsProjectController extends Controller {
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@alias('model.displayName') breadCrumb;
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
export default class OrgsOrgProjectsProjectHostCatalogsHostCatalogController extends Controller {
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@alias('model.displayName') breadCrumb;
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
export default class OrgsOrgRolesRoleController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@alias('model.displayName') breadCrumb;
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
export default class OrgsOrgUsersUserController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@alias('model.displayName') breadCrumb;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeController extends Controller {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
}
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgGroupsController extends Controller {
|
||||
export default class ScopesScopeGroupsController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -9,6 +9,7 @@ export default class OrgsOrgGroupsController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Translated groups breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
@@ -0,0 +1,13 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeGroupsGroupController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* A group breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.model.displayName;
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgProjectsNewController extends Controller {
|
||||
|
||||
export default class ScopesScopeGroupsNewController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -10,10 +9,10 @@ export default class OrgsOrgProjectsNewController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Translated new group breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.intl.t('titles.new');
|
||||
}
|
||||
|
||||
}
|
||||
+1
-3
@@ -1,8 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgProjectsController extends Controller {
|
||||
|
||||
export default class ScopesScopeProjectsController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -15,5 +14,4 @@ export default class OrgsOrgProjectsController extends Controller {
|
||||
get breadCrumb() {
|
||||
return this.intl.t('resources.projects');
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgUsersNewController extends Controller {
|
||||
export default class ScopesScopeProjectsNewController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -0,0 +1,12 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeProjectsProjectController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.model.displayName;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgProjectsProjectHostCatalogsController extends Controller {
|
||||
export default class ScopesScopeProjectsProjectHostCatalogsController extends Controller {
|
||||
|
||||
// =services
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogController extends Controller {
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.model.displayName;
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -1,7 +1,8 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgGroupsNewController extends Controller {
|
||||
export default class ScopesScopeProjectsProjectHostCatalogsNewController extends Controller {
|
||||
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -9,6 +10,7 @@ export default class OrgsOrgGroupsNewController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Translated new role breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgRolesController extends Controller {
|
||||
export default class ScopesScopeRolesController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -9,10 +9,10 @@ export default class OrgsOrgRolesController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Translated roles breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.intl.t('resources.roles');
|
||||
}
|
||||
|
||||
}
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgProjectsProjectHostCatalogsNewController extends Controller {
|
||||
|
||||
export default class ScopesScopeRolesNewController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -10,10 +9,10 @@ export default class OrgsOrgProjectsProjectHostCatalogsNewController extends Con
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Translated new role breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.intl.t('titles.new');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeRolesRoleController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* A role breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.model.displayName;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgUsersController extends Controller {
|
||||
export default class ScopesScopeUsersController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -9,10 +9,10 @@ export default class OrgsOrgUsersController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Translated users breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.intl.t('resources.users');
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgRolesNewController extends Controller {
|
||||
export default class ScopesScopeUsersNewController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@@ -9,10 +9,10 @@ export default class OrgsOrgRolesNewController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Translated new user breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.intl.t('titles.new');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeUsersUserController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* An user breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.model.displayName;
|
||||
}
|
||||
}
|
||||
+19
-19
@@ -7,32 +7,32 @@ export default class Router extends EmberRouter {
|
||||
}
|
||||
|
||||
Router.map(function () {
|
||||
this.route('orgs', function () {
|
||||
this.route('org', { path: ':org_id' }, function () {
|
||||
this.route('projects', function () {
|
||||
this.route('scopes', function () {
|
||||
this.route('scope', { path: ':scope_id' }, function () {
|
||||
this.route('authenticate', function () {
|
||||
this.route('method', { path: ':auth_method_id' });
|
||||
});
|
||||
this.route('users', function () {
|
||||
this.route('user', { path: ':user_id' }, function () {});
|
||||
this.route('new');
|
||||
});
|
||||
this.route('groups', function () {
|
||||
this.route('group', { path: ':group_id' }, function () {});
|
||||
this.route('new');
|
||||
});
|
||||
this.route('roles', function () {
|
||||
this.route('role', { path: ':role_id' }, function () {});
|
||||
this.route('new');
|
||||
});
|
||||
this.route('projects', function() {
|
||||
this.route('project', { path: ':project_id' }, function() {
|
||||
this.route('host-catalogs', function() {
|
||||
this.route('host-catalog', { path: ':host_catalog_id' }, function() {});
|
||||
this.route('new');
|
||||
this.route('host-catalog', { path: ':host_catalog_id' }, function() {});
|
||||
});
|
||||
});
|
||||
this.route('new');
|
||||
});
|
||||
|
||||
this.route('users', function() {
|
||||
this.route('user', { path: ':user_id' }, function() {});
|
||||
this.route('new');
|
||||
});
|
||||
|
||||
this.route('roles', function() {
|
||||
this.route('role', { path: ':role_id' }, function() {});
|
||||
this.route('new');
|
||||
});
|
||||
|
||||
this.route('groups', function() {
|
||||
this.route('group', { path: ':group_id' }, function() {});
|
||||
this.route('new');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,62 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
|
||||
import Ember from 'ember';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { later } from '@ember/runloop';
|
||||
|
||||
/**
|
||||
* Entry route for the application.
|
||||
*/
|
||||
export default class ApplicationRoute extends Route {}
|
||||
export default class ApplicationRoute extends Route.extend(
|
||||
ApplicationRouteMixin
|
||||
) {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =attributes
|
||||
|
||||
routeIfUnauthenticated = 'index';
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* After becoming authenticated, does nothing. This overrides the default
|
||||
* behavior of the ApplicationRouteMixin, which is to redirect after auth.
|
||||
* We'll handle this redirect manually in sub routes.
|
||||
* @override
|
||||
*/
|
||||
sessionAuthenticated() {
|
||||
// no op
|
||||
}
|
||||
|
||||
/**
|
||||
* When the session ends, redirect to authenticate and reload the page to
|
||||
* purge any in-memory state.
|
||||
*/
|
||||
async sessionInvalidated() {
|
||||
// Catch error in this transition, since it will be aborted by the
|
||||
// scope auth route when it redirects to the first auth method.
|
||||
await this.transitionTo(this.routeIfUnauthenticated).catch(() => {});
|
||||
// The Ember way of accessing globals...
|
||||
const document = getOwner(this).lookup('service:-document').documentElement;
|
||||
// defaultView === window, but without using globals directly
|
||||
const { location } = document.parentNode.defaultView;
|
||||
// Wait a beat, then reload the page...
|
||||
// This is mostly to give the deauth request a chance to fire.
|
||||
/* istanbul ignore if */
|
||||
if (!Ember.testing) later(location, location.reload, 150);
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Delegates invalidation to the session service.
|
||||
*/
|
||||
@action
|
||||
invalidateSession() {
|
||||
this.session.invalidate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class IndexRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Redirects to scopes route for further processing.
|
||||
*/
|
||||
redirect() {
|
||||
this.transitionTo('scopes');
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OrgsRoute extends Route {
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns all orgs from the store.
|
||||
* @return {Promise[OrgModel]}
|
||||
*/
|
||||
model() {
|
||||
return this.store.findAll('org');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OrgsOrgRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service scope;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns an org by id.
|
||||
* @param {object} params
|
||||
* @return {Promise{OrgModel}}
|
||||
*/
|
||||
model({ org_id: id }) {
|
||||
return this.store.findRecord('org', id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the org to the scope.
|
||||
* @param {object} org
|
||||
*/
|
||||
afterModel(org) {
|
||||
this.scope.org = org;
|
||||
return super.afterModel(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OrgsOrgGroupsRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns all groups from store.
|
||||
* @return {Promise[GroupModel]}
|
||||
*/
|
||||
model() {
|
||||
return this.store.findAll('group');
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class OrgsOrgGroupsGroupRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@service notify;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Return a group with id
|
||||
* @param {object} params
|
||||
* @return {Promise{GroupModel}}
|
||||
*/
|
||||
model({ group_id: id }) {
|
||||
return this.store.findRecord('group', id);
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Rollback changes on group.
|
||||
* @param {GroupModel} group
|
||||
*/
|
||||
@action
|
||||
cancel(group) {
|
||||
group.rollbackAttributes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle saving a group.
|
||||
* @param {GroupModel} group
|
||||
* @param {Event} e
|
||||
*/
|
||||
@action
|
||||
async save(group) {
|
||||
try {
|
||||
await group.save();
|
||||
this.notify.success(this.intl.t('notify.save-success'));
|
||||
} catch (error) {
|
||||
// TODO: replace with translated strings
|
||||
this.notify.error(error.message, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete group and redirect to groups index.
|
||||
* @param {GroupModel} group
|
||||
*/
|
||||
@action
|
||||
async delete(group) {
|
||||
try {
|
||||
await group.destroyRecord();
|
||||
this.transitionTo('orgs.org.groups');
|
||||
this.notify.success(this.intl.t('notify.group.delete-success'));
|
||||
} catch (error) {
|
||||
// TODO: replace with translated strings
|
||||
this.notify.error(error.message, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OrgsOrgGroupsGroupIndexRoute extends Route {
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OrgsOrgGroupsIndexRoute extends Route {}
|
||||
@@ -1,50 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class OrgsOrgGroupsNewRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@service notify;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns a new unsaved group.
|
||||
* @return {Promise{GroupModel}}
|
||||
*/
|
||||
model() {
|
||||
return this.store.createRecord('group');
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Rollback changes on group by destroying unsaved instance
|
||||
* and redirect to group index.
|
||||
* @param {GroupModel} group
|
||||
*/
|
||||
@action
|
||||
cancel(group) {
|
||||
group.rollbackAttributes();
|
||||
this.transitionTo('orgs.org.groups');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle saving a group.
|
||||
* @param {GroupModel} group
|
||||
* @param {Event} e
|
||||
*/
|
||||
@action
|
||||
async save(group) {
|
||||
try {
|
||||
await group.save();
|
||||
this.transitionTo('orgs.org.groups.group', group);
|
||||
this.notify.success(this.intl.t('notify.group.create-success'));
|
||||
} catch (error) {
|
||||
// TODO: replace with translated strings
|
||||
this.notify.error(error.message, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OrgsOrgProjectsRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns all projects from the store.
|
||||
* @return {Promise[ProjectModel]}
|
||||
*/
|
||||
model() {
|
||||
return this.store.findAll('project');
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OrgsOrgProjectsIndexRoute extends Route { }
|
||||
Loaded 100 of 285 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user