diff --git a/ui-v2/app/adapters/oidc-provider.js b/ui-v2/app/adapters/oidc-provider.js
index 7d1da6df3c8a..f2b65da2e220 100644
--- a/ui-v2/app/adapters/oidc-provider.js
+++ b/ui-v2/app/adapters/oidc-provider.js
@@ -33,7 +33,7 @@ export default Adapter.extend({
${{
...Namespace(ns),
AuthMethod: id,
- RedirectURI: `${this.env.var('CONSUL_BASE_UI_URL')}/torii/redirect.html`,
+ RedirectURI: `${this.env.var('CONSUL_BASE_UI_URL')}/oidc/callback`,
}}
`;
},
diff --git a/ui-v2/app/components/auth-dialog/README.mdx b/ui-v2/app/components/auth-dialog/README.mdx
new file mode 100644
index 000000000000..676d55379661
--- /dev/null
+++ b/ui-v2/app/components/auth-dialog/README.mdx
@@ -0,0 +1,56 @@
+## AuthDialog
+
+```handlebars
+
+ {{#let components.AuthForm components.AuthProfile as |AuthForm AuthProfile|}}
+
+ Here's the login form:
+
+
+
+ Here's your profile:
+
+
+
+ {{/let}}
+
+```
+
+### Arguments
+
+A component to help orchestrate a login/logout flow.
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `dc` | `String` | | The name of the current datacenter |
+| `nspace` | `String` | | The name of the current namespace |
+| `onchange` | `Function` | | An action to fire when the users token has changed (logged in/logged out/token changed) |
+
+### Methods/Actions/api
+
+| Method/Action | Description |
+| --- | --- |
+| `login` | Login with a specified token |
+| `logout` | Logout (delete token) |
+| `token` | The current token itself (as a property not a method) |
+
+### Components
+
+| Name | Description |
+| --- | --- |
+| [`AuthForm`](../auth-form/README.mdx) | Renders an Authorization form |
+| [`AuthProfile`](../auth-profile/README.mdx) | Renders a User Profile |
+
+### Slots
+
+| Name | Description |
+| --- | --- |
+| `unauthorized` | This slot is only rendered when the user doesn't have a token |
+| `authorized` | This slot is only rendered whtn the user has a token.|
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/auth-dialog/chart.xstate.js b/ui-v2/app/components/auth-dialog/chart.xstate.js
new file mode 100644
index 000000000000..ce4730745393
--- /dev/null
+++ b/ui-v2/app/components/auth-dialog/chart.xstate.js
@@ -0,0 +1,34 @@
+export default {
+ id: 'auth-dialog',
+ initial: 'idle',
+ on: {
+ CHANGE: [
+ {
+ target: 'authorized',
+ cond: 'hasToken',
+ actions: ['login'],
+ },
+ {
+ target: 'unauthorized',
+ actions: ['logout'],
+ },
+ ],
+ },
+ states: {
+ idle: {
+ on: {
+ CHANGE: [
+ {
+ target: 'authorized',
+ cond: 'hasToken',
+ },
+ {
+ target: 'unauthorized',
+ },
+ ],
+ },
+ },
+ unauthorized: {},
+ authorized: {},
+ },
+};
diff --git a/ui-v2/app/components/auth-dialog/index.hbs b/ui-v2/app/components/auth-dialog/index.hbs
new file mode 100644
index 000000000000..e12a4d05d600
--- /dev/null
+++ b/ui-v2/app/components/auth-dialog/index.hbs
@@ -0,0 +1,40 @@
+
+
+
+
+
+ {{! This DataSource just permanently listens to any changes to the users }}
+ {{! token, whether thats a new token, a changed token or a deleted token }}
+
+ {{! This DataSink is just used for logging in from the form, }}
+ {{! or logging out via the exposed logout function }}
+
+ {{yield}}
+ {{#let (hash
+ login=(action sink.open)
+ logout=(action sink.open null)
+ token=token
+ ) (hash
+ AuthProfile=(component 'auth-profile' item=token)
+ AuthForm=(component 'auth-form' dc=dc nspace=nspace onsubmit=(action sink.open value="data"))
+ ) as |api components|}}
+
+ {{#yield-slot name="authorized"}}
+ {{yield api components}}
+ {{/yield-slot}}
+
+
+
+ {{#yield-slot name="unauthorized"}}
+ {{yield api components}}
+ {{/yield-slot}}
+
+ {{/let}}
+
+
diff --git a/ui-v2/app/components/auth-dialog/index.js b/ui-v2/app/components/auth-dialog/index.js
new file mode 100644
index 000000000000..041fee483b48
--- /dev/null
+++ b/ui-v2/app/components/auth-dialog/index.js
@@ -0,0 +1,42 @@
+import Component from '@ember/component';
+import Slotted from 'block-slots';
+import { inject as service } from '@ember/service';
+import { get } from '@ember/object';
+import chart from './chart.xstate';
+
+export default Component.extend(Slotted, {
+ tagName: '',
+ repo: service('repository/oidc-provider'),
+ init: function() {
+ this._super(...arguments);
+ this.chart = chart;
+ },
+ actions: {
+ hasToken: function() {
+ return typeof this.token.AccessorID !== 'undefined';
+ },
+ login: function() {
+ let prev = get(this, 'previousToken.AccessorID');
+ let current = get(this, 'token.AccessorID');
+ if (prev === null) {
+ prev = get(this, 'previousToken.SecretID');
+ }
+ if (current === null) {
+ current = get(this, 'token.SecretID');
+ }
+ let type = 'authorize';
+ if (typeof prev !== 'undefined' && prev !== current) {
+ type = 'use';
+ }
+ this.onchange({ data: get(this, 'token'), type: type });
+ },
+ logout: function() {
+ if (typeof get(this, 'previousToken.AuthMethod') !== 'undefined') {
+ // we are ok to fire and forget here
+ this.repo.logout(get(this, 'previousToken.SecretID'));
+ }
+ this.previousToken = null;
+ this.onchange({ data: null, type: 'logout' });
+ },
+ },
+});
diff --git a/ui-v2/app/components/auth-form/README.mdx b/ui-v2/app/components/auth-form/README.mdx
new file mode 100644
index 000000000000..942e9d813748
--- /dev/null
+++ b/ui-v2/app/components/auth-form/README.mdx
@@ -0,0 +1,18 @@
+## AuthForm
+
+```handlebars
+
+```
+
+### Methods/Actions/api
+
+| Method/Action | Description |
+| --- | --- |
+| `reset` | Reset the form back to its original empty/non-error state |
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/auth-form/chart.xstate.js b/ui-v2/app/components/auth-form/chart.xstate.js
new file mode 100644
index 000000000000..dc4b281059a3
--- /dev/null
+++ b/ui-v2/app/components/auth-form/chart.xstate.js
@@ -0,0 +1,55 @@
+export default {
+ id: 'auth-form',
+ initial: 'idle',
+ on: {
+ RESET: [
+ {
+ target: 'idle',
+ },
+ ],
+ },
+ states: {
+ idle: {
+ entry: ['clearError'],
+ on: {
+ SUBMIT: [
+ {
+ target: 'loading',
+ cond: 'hasValue',
+ },
+ {
+ target: 'error',
+ },
+ ],
+ },
+ },
+ loading: {
+ on: {
+ ERROR: [
+ {
+ target: 'error',
+ },
+ ],
+ },
+ },
+ error: {
+ exit: ['clearError'],
+ on: {
+ TYPING: [
+ {
+ target: 'idle',
+ },
+ ],
+ SUBMIT: [
+ {
+ target: 'loading',
+ cond: 'hasValue',
+ },
+ {
+ target: 'error',
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/ui-v2/app/components/auth-form/index.hbs b/ui-v2/app/components/auth-form/index.hbs
new file mode 100644
index 000000000000..2b25ccdd6e20
--- /dev/null
+++ b/ui-v2/app/components/auth-form/index.hbs
@@ -0,0 +1,100 @@
+
+ {{yield (hash
+ reset=(action dispatch "RESET")
+ focus=(action 'focus')
+ )}}
+
+ {{!FIXME: Call this reset or similar }}
+
+
+
+ {{#if error.status}}
+
+ {{#if value.Name}}
+ {{#if (eq error.status '403')}}
+ Consul login failed
+ We received a token from your OIDC provider but could not log in to Consul with it.
+ {{else if (eq error.status '401')}}
+ Could not log in to provider
+ The OIDC provider has rejected this access token. Please have an administrator check your auth method configuration.
+ {{else if (eq error.status '499')}}
+ SSO log in window closed
+ The OIDC provider window was closed. Please try again.
+ {{else}}
+ Error
+ {{error.detail}}
+ {{/if}}
+ {{else}}
+ {{#if (eq error.status '403')}}
+ Invalid token
+ The token entered does not exist. Please enter a valid token to log in.
+ {{else}}
+ Error
+ {{error.detail}}
+ {{/if}}
+ {{/if}}
+
Documentation
{{{concat ''}}}
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ui-v2/app/components/hashicorp-consul/index.js b/ui-v2/app/components/hashicorp-consul/index.js
index 6a6767a673ba..72f5f9f90e14 100644
--- a/ui-v2/app/components/hashicorp-consul/index.js
+++ b/ui-v2/app/components/hashicorp-consul/index.js
@@ -1,19 +1,12 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
-import { get, set, computed } from '@ember/object';
-import { getOwner } from '@ember/application';
+import { computed } from '@ember/object';
export default Component.extend({
dom: service('dom'),
- env: service('env'),
- feedback: service('feedback'),
- router: service('router'),
- http: service('repository/type/event-source'),
- client: service('client/http'),
- store: service('store'),
- settings: service('settings'),
didInsertElement: function() {
+ this._super(...arguments);
this.dom.root().classList.remove('template-with-vertical-menu');
},
// TODO: Right now this is the only place where we need permissions
@@ -25,108 +18,15 @@ export default Component.extend({
}) !== 'undefined'
);
}),
- forwardForACL: function(token) {
- let routeName = this.router.currentRouteName;
- const route = getOwner(this).lookup(`route:${routeName}`);
- // a null AccessorID means we are in legacy mode
- // take the user to the legacy acls
- // otherwise just refresh the page
- if (get(token, 'AccessorID') === null) {
- // returning false for a feedback action means even though
- // its successful, please skip this notification and don't display it
- return route.transitionTo('dc.acls');
- } else {
- // TODO: Ideally we wouldn't need to use env() at a component level
- // transitionTo should probably remove it instead if NSPACES aren't enabled
- if (this.env.var('CONSUL_NSPACES_ENABLED') && get(token, 'Namespace') !== this.nspace.Name) {
- if (!routeName.startsWith('nspace')) {
- routeName = `nspace.${routeName}`;
- }
- const nspace = get(token, 'Namespace');
- // you potentially have a new namespace
- if (typeof nspace !== 'undefined') {
- return route.transitionTo(`${routeName}`, `~${nspace}`, this.dc.Name);
- }
- // you are logging out, just refresh
- return route.refresh();
- } else {
- if (route.routeName === 'dc.acls.index') {
- return route.transitionTo('dc.acls.tokens.index');
- }
- return route.refresh();
- }
- }
- },
actions: {
- send: function(el, method, ...rest) {
- const component = this.dom.component(el);
- component.actions[method].apply(component, rest || []);
+ keypressClick: function(e) {
+ e.target.dispatchEvent(new MouseEvent('click'));
},
- changeToken: function(token = {}) {
- const prev = this.token;
- if (token === '') {
- token = {};
- }
- set(this, 'token', token);
- // if this is just the initial 'find out what the current token is'
- // then don't do anything
- if (typeof prev === 'undefined') {
- return;
- }
- let notification;
- let action = () => this.forwardForACL(token);
- switch (true) {
- case get(this, 'token.AccessorID') === null && get(this, 'token.SecretID') === null:
- // 'everything is null, 403 this needs deleting' token
- this.settings.delete('token');
- return;
- case get(prev, 'AccessorID') === null && get(prev, 'SecretID') === null:
- // we just had an 'everything is null, this needs deleting' token
- // reject and break so this acts differently to just logging out
- action = () => Promise.reject({});
- notification = 'authorize';
- break;
- case typeof get(prev, 'AccessorID') !== 'undefined' &&
- typeof get(this, 'token.AccessorID') !== 'undefined':
- // change of both Accessor and Secret, means use
- notification = 'use';
- break;
- case get(this, 'token.AccessorID') === null &&
- typeof get(this, 'token.SecretID') !== 'undefined':
- // legacy login, don't do anything as we don't use self for auth here but the endpoint itself
- // self is successful, but skip this notification and don't display it
- return this.forwardForACL(token);
- case typeof get(prev, 'AccessorID') === 'undefined' &&
- typeof get(this, 'token.AccessorID') !== 'undefined':
- // normal login
- notification = 'authorize';
- break;
- case (typeof get(prev, 'AccessorID') !== 'undefined' || get(prev, 'AccessorID') === null) &&
- typeof get(this, 'token.AccessorID') === 'undefined':
- //normal logout
- notification = 'logout';
- break;
- }
- this.actions.reauthorize.apply(this, [
- {
- type: notification,
- action: action,
- },
- ]);
+ open: function() {
+ this.authForm.focus();
},
- reauthorize: function(e) {
- this.client.abort();
- this.http.resetCache();
- this.store.init();
- const type = get(e, 'type');
- this.feedback.execute(
- e.action,
- type,
- function(type, e) {
- return type;
- },
- {}
- );
+ close: function() {
+ this.authForm.reset();
},
change: function(e) {
const win = this.dom.viewport();
diff --git a/ui-v2/app/components/jwt-source/README.mdx b/ui-v2/app/components/jwt-source/README.mdx
new file mode 100644
index 000000000000..51b1a12f2293
--- /dev/null
+++ b/ui-v2/app/components/jwt-source/README.mdx
@@ -0,0 +1,24 @@
+## JwtSource
+
+```handlebars
+
+```
+
+### Arguments
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `src` | `String` | | The source to subscribe to updates to, this should map to a string based URI |
+| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the jwt data, in this case the autorizationCode and the status |
+| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |
+
+This component will go through the steps of requesting a JWT token from a third party oauth provider. `src` should contain the full URL of the authorization URL for the 3rd party provider. Once the user has logged into the 3rd party provider the `onchange` event will be fired containing an event-like object whose data contains the JWT information.
+
+The component need only be place into the DOM in order to begin the OAuth dance.
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/jwt-source/index.js b/ui-v2/app/components/jwt-source/index.js
new file mode 100644
index 000000000000..da84dc672caa
--- /dev/null
+++ b/ui-v2/app/components/jwt-source/index.js
@@ -0,0 +1,31 @@
+import Component from '@ember/component';
+import { inject as service } from '@ember/service';
+import { fromPromise } from 'consul-ui/utils/dom/event-source';
+
+export default Component.extend({
+ repo: service('repository/oidc-provider'),
+ dom: service('dom'),
+ tagName: '',
+ onchange: function(e) {},
+ onerror: function(e) {},
+ init: function() {
+ this._super(...arguments);
+ this._listeners = this.dom.listeners();
+ },
+ willDestroy: function() {
+ this._super(...arguments);
+ this.repo.close();
+ this._listeners.remove();
+ },
+ didInsertElement: function() {
+ if (this.source) {
+ this.source.close();
+ }
+ // TODO: Could this use once? Double check but I don't think it can
+ this.source = fromPromise(this.repo.findCodeByURL(this.src));
+ this._listeners.add(this.source, {
+ message: e => this.onchange(e),
+ error: e => this.onerror(e),
+ });
+ },
+});
diff --git a/ui-v2/app/components/oidc-select/chart.xstate.js b/ui-v2/app/components/oidc-select/chart.xstate.js
new file mode 100644
index 000000000000..08e65961d8b8
--- /dev/null
+++ b/ui-v2/app/components/oidc-select/chart.xstate.js
@@ -0,0 +1,16 @@
+export default {
+ id: 'oidc-select',
+ initial: 'loading',
+ states: {
+ loaded: {},
+ loading: {
+ on: {
+ SUCCESS: [
+ {
+ target: 'loaded',
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/ui-v2/app/components/oidc-select/index.hbs b/ui-v2/app/components/oidc-select/index.hbs
new file mode 100644
index 000000000000..99b173be5fba
--- /dev/null
+++ b/ui-v2/app/components/oidc-select/index.hbs
@@ -0,0 +1,46 @@
+
+
+
+