Merge pull request #89 from hashicorp/refactor-scopes

Refactor scopes
This commit is contained in:
Susmitha Girumala authored and GitHub committed 2020-07-30 13:45:42 -04:00
commit f52c4d3b74
285 files changed
+4754 -2202

No files matched your search

+59 -2
View File
@@ -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.
*
-16
View File
@@ -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;
}
-14
View File
@@ -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
) { }
-14
View File
@@ -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;
}
-14
View File
@@ -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;
}
+27
View File
@@ -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 });
}
}
-14
View File
@@ -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;
}
@@ -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);
}
});
-53
View File
@@ -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('/');
},
});
+55
View File
@@ -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;
}
}
+36
View File
@@ -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; }
}
-3
View File
@@ -1,3 +0,0 @@
import GeneratedOrgModel from '../generated/models/org';
export default class OrgModel extends GeneratedOrgModel {}
-3
View File
@@ -1,3 +0,0 @@
import GeneratedProjectModel from '../generated/models/project';
export default class ProjectModel extends GeneratedProjectModel {}
+46
View File
@@ -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;
}
}
+28 -2
View File
@@ -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);
}
}
-123
View File
@@ -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
View File
@@ -1 +0,0 @@
export { default } from 'api/adapters/auth-method';
-1
View File
@@ -1 +0,0 @@
export { default } from 'api/adapters/group';
-1
View File
@@ -1 +0,0 @@
export { default } from 'api/adapters/host-catalog';
-1
View File
@@ -1 +0,0 @@
export { default } from 'api/adapters/project';
-1
View File
@@ -1 +0,0 @@
export { default } from 'api/adapters/role';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/adapters/scope';
-1
View File
@@ -1 +0,0 @@
export { default } from 'api/adapters/user';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-scope';
-1
View File
@@ -1 +0,0 @@
export { default } from 'api/models/org';
-1
View File
@@ -1 +0,0 @@
export { default } from 'api/models/project';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/scope';
-1
View File
@@ -1 +0,0 @@
export { default, Scope } from 'api/services/scope';
+1
View File
@@ -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');
});
});
+101 -5
View File
@@ -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}});
});
});
-13
View File
@@ -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);
});
});
View File
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);
}
}
+134
View File
@@ -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 {}
+4
View File
@@ -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 {}
+22
View File
@@ -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
View File
@@ -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",
+45
View File
@@ -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');
}
}
+8
View File
@@ -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;
}
+8
View File
@@ -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;
}
@@ -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;
}
}
@@ -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,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,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,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;
}
}
@@ -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() {
@@ -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');
}
}
@@ -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;
}
}
@@ -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');
}
}
@@ -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
View File
@@ -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');
});
});
});
});
+57 -1
View File
@@ -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();
}
}
+12
View File
@@ -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');
}
}
-15
View File
@@ -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');
}
}
-28
View File
@@ -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);
}
}
-13
View File
@@ -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 {}
-50
View File
@@ -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 });
}
}
}
-13
View File
@@ -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