Merge branch 'main' into add-project-to-targets-view
This commit is contained in:
28 files changed
+809
-31
No files matched your search
@@ -0,0 +1,98 @@
|
||||
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({ scope: { id: scopeID } }) {
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const options = { adapterOptions: { method: 'deauthenticate' } };
|
||||
return adapter.buildURL('scope', scopeID, options, 'findRecord');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an auth token validation URL used to check tokens on restoration.
|
||||
* @override
|
||||
* @param {string} tokenID
|
||||
* @return {string}
|
||||
*/
|
||||
buildTokenValidationEndpointURL(tokenID) {
|
||||
const adapter = this.store.adapterFor('auth-token');
|
||||
return adapter.buildURL('auth-token', tokenID, {}, 'findRecord');
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts the authenticate response, if any, and assigns the returned
|
||||
* token to all future requests via `addTokenToAuthorization`.
|
||||
* Returns the response data as normal.
|
||||
* @override
|
||||
*/
|
||||
authenticate() {
|
||||
return super.authenticate(...arguments).then((data) => {
|
||||
const token = data?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When restoring the session (say, on load), assigns the session token
|
||||
* to all future requests via `addTokenToAuthorization`.
|
||||
* @override
|
||||
* @param {object} data
|
||||
* @return {object}
|
||||
*/
|
||||
restore(data) {
|
||||
const token = data?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return super.restore(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a token string all future requests via the `Authorization` header
|
||||
* on the application adapter prototype. Only application and adapters that
|
||||
* extend the application adapter receive this header (which is most).
|
||||
* @param {string} token
|
||||
*/
|
||||
addTokenToAuthorization(token) {
|
||||
const adapterPrototype = this.store.adapterFor('application').constructor
|
||||
.prototype;
|
||||
const headers = adapterPrototype?.headers;
|
||||
if (!headers) adapterPrototype.headers = {};
|
||||
adapterPrototype.headers.Authorization = null;
|
||||
adapterPrototype.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* This helper returns true or false based on the state of the ember-loading
|
||||
* service's `isLoading` attribute.
|
||||
*/
|
||||
export default class extends Helper {
|
||||
|
||||
// =services
|
||||
|
||||
@service loading;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns true if "something" is loading according to ember-loading.
|
||||
* @return {boolean}
|
||||
*/
|
||||
compute() {
|
||||
return this.loading.isLoading;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,9 +7,12 @@ export default class Router extends EmberRouter {
|
||||
}
|
||||
|
||||
Router.map(function () {
|
||||
this.route('scopes', function () {
|
||||
this.route('scope', { path: ':scope_id' }, function () {
|
||||
this.route('targets', function () {});
|
||||
this.route('scopes', function() {
|
||||
this.route('scope', { path: ':scope_id' }, function() {
|
||||
this.route('targets');
|
||||
this.route('authenticate', function() {
|
||||
this.route('method', { path: ':auth_method_id' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import Route from '@ember/routing/route';
|
||||
/* eslint-disable-next-line ember/no-mixins */
|
||||
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
|
||||
export default class ApplicationRoute extends Route.extend(
|
||||
ApplicationRouteMixin
|
||||
) {
|
||||
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =attributes
|
||||
|
||||
routeIfUnauthenticated = 'index';
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks into ember-loading to kick off loading indicator in the
|
||||
* application template.
|
||||
* @return {boolean} always returns true
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
loading() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the session if a 401 error occurs and returns false to
|
||||
* prevent further error handling.
|
||||
* Returns true in all other cases, allowing error handling to occur (such
|
||||
* as displaying the `error.hbs` template, if one exists).
|
||||
* @param {Error} e
|
||||
*/
|
||||
@action
|
||||
error(e) {
|
||||
const isUnauthenticated = A(e?.errors)?.firstObject?.isUnauthenticated;
|
||||
if (isUnauthenticated) {
|
||||
this.session.invalidate();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +1,12 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class IndexRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service ipc;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Invokes an IPC method and returns the result.
|
||||
* @return {Promise}
|
||||
*/
|
||||
model() {
|
||||
return this.ipc.invoke('getFoobars', {
|
||||
id: '1',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to scopes route for further processing.
|
||||
*/
|
||||
redirect() {
|
||||
this.transitionTo('scopes');
|
||||
this.replaceWith('scopes');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default class ScopesIndexRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* If authenticated, redirects to the scope route of the authenticted scope.
|
||||
* If unauthenticated, redirects to the first scope that was loaded (if any).
|
||||
* @param {[ScopeModel]} model
|
||||
* @param {?ScopeModel} model.firstObject
|
||||
*/
|
||||
redirect() {
|
||||
const authenticatedScopeID = get(
|
||||
this.session,
|
||||
'data.authenticated.scope.id'
|
||||
);
|
||||
if (authenticatedScopeID) {
|
||||
this.replaceWith('scopes.scope', authenticatedScopeID);
|
||||
} else {
|
||||
this.replaceWith('scopes.scope', 'global');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { A } from '@ember/array';
|
||||
export default class ScopesScopeRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
@service scope;
|
||||
|
||||
// =methods
|
||||
@@ -45,24 +46,33 @@ export default class ScopesScopeRoute extends Route {
|
||||
orgs = await this.store
|
||||
.query('scope', { scope_id: 'global' })
|
||||
.catch(() => A([]));
|
||||
// Then pull out the "selected" scopes, if relevant
|
||||
|
||||
// Then pull out the "selected" org, if relevant
|
||||
let selectedOrg;
|
||||
if (model.isGlobal || model.isOrg) selectedOrg = model;
|
||||
|
||||
let projects;
|
||||
if (model.isGlobal) {
|
||||
projects = orgs.map((id) => this.store.query('scope', { scope_id: id }));
|
||||
}
|
||||
if (model.isOrg) {
|
||||
projects = this.store.query('scope', { scope_id: model.id });
|
||||
projects = await this.store.query('scope', { scope_id: model.id });
|
||||
}
|
||||
|
||||
// Update the scope service with the current scope(s);
|
||||
this.scope.org = selectedOrg;
|
||||
this.scope.projects = await projects;
|
||||
this.scope.projects = projects;
|
||||
this.scopes = { orgs, selectedOrg };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
redirect(model) {
|
||||
const { isAuthenticated } = this.session;
|
||||
const { isGlobal } = model;
|
||||
if (isAuthenticated && isGlobal && this.scopes.orgs?.length) {
|
||||
this.replaceWith('scopes.scope', this.scopes.orgs.firstObject.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the scopes hash to the controller context (see `afterModel`).
|
||||
* @param {Controller} controller
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
export default class ScopesScopeAuthenticateRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =methods
|
||||
|
||||
beforeModel() {
|
||||
if (this.session.isAuthenticated) this.replaceWith('scopes.scope.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all auth methods for the current scope, along with the current
|
||||
* scope and all scopes (for org navigation).
|
||||
* @return {Promise} `{scope, scopes, authMethods}`
|
||||
*/
|
||||
model() {
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
return hash({
|
||||
scope: this.modelFor('scopes.scope'),
|
||||
scopes: this.modelFor('scopes').filter(scope => scope.isOrg),
|
||||
authMethods: this.store.query('auth-method', { scope_id })
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeAuthenticateIndexRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* This index route exists solely to redirect the user on to the
|
||||
* first auth method.
|
||||
* @param {object} model
|
||||
* @param {[AuthMethodModel]} model.authMethods
|
||||
*/
|
||||
redirect({ authMethods }) {
|
||||
const firstAuthMethod = authMethods.firstObject;
|
||||
if (firstAuthMethod) {
|
||||
this.replaceWith('scopes.scope.authenticate.method', firstAuthMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
|
||||
export default class ScopesScopeAuthenticateMethodRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
@service notify;
|
||||
@service intl;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns an auth method by id.
|
||||
* @param {object} params
|
||||
* @return {Promise{AuthMethodModel}}
|
||||
*/
|
||||
model({ auth_method_id: id }) {
|
||||
const adapterOptions = { scopeID: this.modelFor('scopes.scope').id };
|
||||
return this.store.findRecord('auth-method', id, adapterOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the password field on the controller so it is not
|
||||
* inadvertantly retained in memory.
|
||||
*/
|
||||
resetCredentials() {
|
||||
this.controller.setProperties({
|
||||
password: null,
|
||||
});
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Delegates authentication to the `password` authenticator.
|
||||
* If authentication fails, notifies with an alert.
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
async authenticate(creds) {
|
||||
this.resetCredentials();
|
||||
const authenticatorName = 'authenticator:password';
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const authMethod = this.currentModel;
|
||||
const requestCookies = false;
|
||||
try {
|
||||
await this.session.authenticate(
|
||||
authenticatorName,
|
||||
creds,
|
||||
requestCookies,
|
||||
{ scope, authMethod }
|
||||
);
|
||||
this.replaceWith('index');
|
||||
} catch (e) {
|
||||
const errorMessage = this.intl.t('errors.authentication-failed.title');
|
||||
this.notify.error(errorMessage, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,23 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeIndexRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Redirects to scopes route for further processing.
|
||||
* Redirects to scopes/scope/authenticate for further processing.
|
||||
* If already authenticated, redirects to scopes/scope/scopes to display
|
||||
* list of sub scopes.
|
||||
*/
|
||||
redirect() {
|
||||
this.transitionTo('scopes.scope.targets');
|
||||
redirect(model) {
|
||||
if (!this.session.isAuthenticated) {
|
||||
this.replaceWith('scopes.scope.authenticate');
|
||||
} else {
|
||||
this.replaceWith('scopes.scope.targets');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { all } from 'rsvp';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { all } from 'rsvp';
|
||||
|
||||
export default class ScopesScopeTargetsRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
@service scope;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* If arriving here unauthenticated, redirect to index for further processing.
|
||||
*/
|
||||
beforeModel() {
|
||||
if (!this.session.isAuthenticated) this.transitionTo('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all targets under current scope.
|
||||
* @return {Promise{[TargetModel]}}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import LocalStorageSessionStore from 'auth/session-stores/local-storage';
|
||||
|
||||
export default class ApplicationSessionStore extends LocalStorageSessionStore {}
|
||||
@@ -0,0 +1,96 @@
|
||||
// This file is essentially a rip of the default ember notify styles with some minor tweaks for positioning.
|
||||
|
||||
// sass-lint:disable no-vendor-prefixes
|
||||
@-webkit-keyframes 'ember-notify-show' {
|
||||
from {
|
||||
left: -310px;
|
||||
}
|
||||
|
||||
to {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@keyframes 'ember-notify-show' {
|
||||
from {
|
||||
left: -310px;
|
||||
}
|
||||
|
||||
to {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes 'ember-notify-hide' {
|
||||
from {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
left: -310px;
|
||||
}
|
||||
}
|
||||
@keyframes 'ember-notify-hide' {
|
||||
from {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
left: -310px;
|
||||
}
|
||||
}
|
||||
|
||||
.ember-notify-cn {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.ember-notify {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 10px;
|
||||
position: relative;
|
||||
width: 300px;
|
||||
|
||||
.close {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.ember-notify-show {
|
||||
-webkit-animation-duration: 500ms;
|
||||
-webkit-animation-name: ember-notify-show;
|
||||
-webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1.275);
|
||||
animation-duration: 500ms;
|
||||
animation-name: ember-notify-show;
|
||||
animation-timing-function: cubic-bezier(.175, .885, .32, 1.275);
|
||||
}
|
||||
|
||||
.ember-notify-hide {
|
||||
-webkit-animation-duration: 250ms;
|
||||
-webkit-animation-name: ember-notify-hide;
|
||||
-webkit-animation-timing-function: cubic-bezier(.6, -.28, .735, .045);
|
||||
animation-duration: 250ms;
|
||||
animation-name: ember-notify-hide;
|
||||
animation-timing-function: cubic-bezier(.6, -.28, .735, .045);
|
||||
|
||||
&:hover {
|
||||
-moz-animation-play-state: paused;
|
||||
-webkit-animation-play-state: paused;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 680px) {
|
||||
.ember-notify-cn {
|
||||
box-sizing: border-box;
|
||||
padding-left: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ember-notify {
|
||||
float: right;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,175 @@
|
||||
@use 'rose/variables/sizing';
|
||||
@use 'rose/variables/media';
|
||||
|
||||
@import 'rose';
|
||||
@import 'notify';
|
||||
|
||||
.rose-layout-centered {
|
||||
> .rose-layout-page {
|
||||
width: sizing.rems(l) * 18;
|
||||
}
|
||||
|
||||
@media (max-width: media.width(small)) {
|
||||
display: block;
|
||||
|
||||
> .rose-layout-page {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rose-nav-sidebar {
|
||||
margin-top: sizing.rems(xxl);
|
||||
|
||||
& + .rose-nav-sidebar {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.rose-icon {
|
||||
margin-right: sizing.rems(xs);
|
||||
}
|
||||
}
|
||||
|
||||
.rose-form {
|
||||
&:not(.full-width) {
|
||||
width: 66%;
|
||||
}
|
||||
|
||||
.rose-form-input {
|
||||
width: 100%;
|
||||
|
||||
.rose-form-input-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.rose-dropdown {
|
||||
$iconWidth: sizing.rems(m) + sizing.rems(s);
|
||||
$indent: $iconWidth + sizing.rems(xs);
|
||||
|
||||
.rose-form-radio-label {
|
||||
display: flex;
|
||||
|
||||
.rose-icon {
|
||||
padding-right: sizing.rems(xxs);
|
||||
}
|
||||
}
|
||||
|
||||
.indent-label-1 {
|
||||
.rose-form-radio-label {
|
||||
padding-left: $indent;
|
||||
}
|
||||
}
|
||||
|
||||
.indent-label-2 {
|
||||
.rose-form-radio-label {
|
||||
padding-left: $indent + $iconWidth;
|
||||
}
|
||||
}
|
||||
|
||||
.is-project {
|
||||
.rose-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin-bottom: sizing.rems(m);
|
||||
|
||||
legend {
|
||||
@extend .rose-form-label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rose-form-checkbox {
|
||||
margin-bottom: 0;
|
||||
|
||||
.rose-form-checkbox-label-text {
|
||||
color: var(--stark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rose-header {
|
||||
position: relative;
|
||||
|
||||
.logotype {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rose-dropdown {
|
||||
.rose-icon {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
&.loading {
|
||||
&::after {
|
||||
animation: gradient 1s linear infinite;
|
||||
background: repeating-linear-gradient(to right, var(--subtle), var(--subtle), var(--info), var(--subtle), var(--subtle));
|
||||
background-position: -100% 0;
|
||||
background-size: 200% auto;
|
||||
content: ' ';
|
||||
height: 3px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
|
||||
@keyframes gradient {
|
||||
0% { background-position: -100% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyable {
|
||||
white-space: nowrap;
|
||||
|
||||
.copyable-button {
|
||||
margin: 0;
|
||||
padding: 0 sizing.rems(xxs);
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
mark {
|
||||
animation: fadeBackground 0.75s linear;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
|
||||
@keyframes fadeBackground {
|
||||
0% { background-color: var(--action-subtler-1); }
|
||||
100% { background-color: transparent; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-link {
|
||||
.large {
|
||||
svg {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class*='session-status-'] {
|
||||
color: var(--ui-gray-subtler-2);
|
||||
}
|
||||
|
||||
.session-status-active {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<Rose::Layout::Global as |layout|>
|
||||
|
||||
<layout.header>
|
||||
<Rose::Header as |header|>
|
||||
<Rose::Header class="{{if (is-loading) " loading"}}" as |header|>
|
||||
<header.brand @logo="logo-app" @text={{app-name}} />
|
||||
|
||||
<header.nav>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{{outlet}}
|
||||
@@ -0,0 +1 @@
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
@onSubmit={{route-action "authenticate" (hash
|
||||
identification=this.identification
|
||||
password=this.password
|
||||
)}}
|
||||
@cancel={{false}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="identification"
|
||||
@type="text"
|
||||
@value={{this.identification}}
|
||||
@label={{t "form.login_name.label"}} />
|
||||
|
||||
<form.input
|
||||
@name="password"
|
||||
@type="password"
|
||||
@value={{this.password}}
|
||||
@label={{t "form.password.label"}} />
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "actions.authenticate"}}
|
||||
@showCancel={{false}} />
|
||||
|
||||
</Rose::Form>
|
||||
@@ -15,6 +15,9 @@ module.exports = function (defaults) {
|
||||
onlyIncluded: true,
|
||||
extension: 'scss',
|
||||
},
|
||||
emberNotify: {
|
||||
importCss: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Use `app.import` to add additional libraries to the generated
|
||||
|
||||
@@ -61,13 +61,18 @@
|
||||
"ember-inline-svg": "^1.0.0",
|
||||
"ember-intl": "^5.5.0",
|
||||
"ember-load-initializers": "^2.1.1",
|
||||
"ember-loading": "^0.4.0",
|
||||
"ember-maybe-import-regenerator": "^0.1.6",
|
||||
"ember-modifier": "^2.1.1",
|
||||
"ember-notify": "^6.0.0",
|
||||
"ember-page-title": "^6.0.3",
|
||||
"ember-qunit": "^4.6.0",
|
||||
"ember-resolver": "^8.0.0",
|
||||
"ember-route-action-helper": "^2.0.8",
|
||||
"ember-simple-auth": "^3.0.1",
|
||||
"ember-source": "~3.20.2",
|
||||
"ember-template-lint": "^2.14.0",
|
||||
"ember-uuid": "^2.1.0",
|
||||
"ember-welcome-page": "^4.0.0",
|
||||
"eslint": "^7.5.0",
|
||||
"eslint-plugin-ember": "^8.9.1",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
|
||||
module('Unit | Authenticator | password', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
let store;
|
||||
let applicationAdapter;
|
||||
let session;
|
||||
let authenticator;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
store = this.owner.lookup('service:store');
|
||||
applicationAdapter = store.adapterFor('application');
|
||||
session = this.owner.lookup('service:session');
|
||||
authenticator = this.owner.lookup('authenticator:password');
|
||||
session.invalidate();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
session.invalidate();
|
||||
});
|
||||
|
||||
test('it adds an authorization header to application adapter on restore', async function (assert) {
|
||||
assert.expect(1);
|
||||
const mockData = { id: 'token1234', token: 'token1234' };
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(mockData.id), () => {
|
||||
return new Response(200);
|
||||
});
|
||||
authenticator.restore(mockData);
|
||||
assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234');
|
||||
});
|
||||
|
||||
test('it adds an authorization header to application adapter on authenticate', async function (assert) {
|
||||
assert.expect(1);
|
||||
const creds = { identification: 'foo', password: 'bar' };
|
||||
const scope = { id: 'global' };
|
||||
const authMethod = { id: 'paum_1234' };
|
||||
this.server.create('scope', { id: 'global', type: 'global' });
|
||||
this.server.create('auth-method', { id: authMethod.id, scopeId: 'global' });
|
||||
await authenticator.authenticate(creds, null, { scope, authMethod });
|
||||
assert.equal(
|
||||
applicationAdapter.headers.Authorization,
|
||||
'Bearer thetokenstring'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | application', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:application');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/index', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/index');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/authenticate', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/authenticate');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/authenticate/index', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/authenticate/index');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/authenticate/method', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/authenticate/method');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -9142,7 +9142,7 @@ ember-router-generator@^2.0.0:
|
||||
"@babel/traverse" "^7.4.5"
|
||||
recast "^0.18.1"
|
||||
|
||||
ember-simple-auth@^3.0.0:
|
||||
ember-simple-auth@^3.0.0, ember-simple-auth@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-simple-auth/-/ember-simple-auth-3.0.1.tgz#78bcd8e3d04da43154cf105931c0d3620a9f0694"
|
||||
integrity sha512-7D5s23nA5Nsonf37CyeQtksadV6Rgiz2rgCgRDnUObp3xnV71ljPtaUcZuq6kqqaZANYJi3uM8i4zVzmAHlKFQ==
|
||||
|
||||
Reference in new issue
Block a user