Skip to content

Instantly share code, notes, and snippets.

@chancancode
Last active July 2, 2017 09:45
Show Gist options
  • Save chancancode/fdc58ea5e94059288ffddb4e400ddeed to your computer and use it in GitHub Desktop.
Save chancancode/fdc58ea5e94059288ffddb4e400ddeed to your computer and use it in GitHub Desktop.
New Twiddle
import Ember from 'ember';
import { task } from 'ember-concurrency';
const { inject: { service } } = Ember;
export default Ember.Controller.extend({
github: service()
});
import Ember from 'ember';
import { task } from 'ember-concurrency';
import { E_BAD_CREDENTIALS, E_BAD_OTP } from '../services/github';
const { computed: { and, equal, not }, inject: { service } } = Ember;
export default Ember.Controller.extend({
github: service(),
username: null,
password: null,
otp: null,
hasSignIn: and('username', 'password'),
disableSignIn: not('hasSignIn'),
disableVerify: not('otp'),
step: 'credentials',
isCredentials: equal('step', 'credentials'),
isOTP: equal('step', 'otp'),
submit: task(function * () {
let {
github, username, password, otp, isOTP
} = this.getProperties(
'github', 'username', 'password', 'otp', 'isOTP'
);
try {
let result = yield github.get('login').perform(username, password, otp);
this.setProperties({
username: null,
password: null,
otp: null,
step: 'credentials'
});
this.transitionToRoute('index');
} catch(error) {
if (error.message === E_BAD_CREDENTIALS) {
this.setProperties({
step: 'credentials',
password: null,
otp: null
});
throw new Error('Incorrect username or password.');
} else if (error.message === E_BAD_OTP) {
this.setProperties({
step: 'otp',
otp: null
});
if (isOTP) {
throw new Error('Two-factor authentication failed.');
} else {
return;
}
}
throw error;
}
}).restartable()
});
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: 'none',
rootURL: config.rootURL
});
Router.map(function() {
this.route('login');
});
export default Router;
import Ember from 'ember';
const { inject: { service } } = Ember;
export default Ember.Route.extend({
github: service(),
async beforeModel() {
try {
await this.get('github.validateToken').perform();
} catch(error) {
this.replaceWith('login');
}
}
});
import Ember from 'ember';
import { isUnauthorizedError } from 'ember-ajax/errors';
import { task } from 'ember-concurrency';
const { inject: { service } } = Ember;
const TOKEN_NAME = 'Ember Core Dashboard';
const GITHUB_HOST = 'https://api.github.com';
const GITHUB_HEADERS = Object.freeze({
'Content-Type': 'application/vnd.github.v3+json'
});
export const E_BAD_CREDENTIALS = 'BAD CREDENTIALS';
export const E_BAD_OTP = 'BAD OTP';
export const E_BAD_TOKEN = 'BAD TOKEN';
export default Ember.Service.extend({
ajax: service(),
token: null, // Hardcode a token here to make development easier
login: task(function * (username, password, otp) {
this.set('token', null);
let ajax = this.get('ajax');
try {
let url = `${GITHUB_HOST}/authorizations`;
let auth = btoa(`${username}:${password}`);
let headers = {
'Authorization': `Basic ${auth}`,
'X-GitHub-OTP': otp,
...GITHUB_HEADERS
};
while (true) {
let response = yield ajax.raw(url, { headers });
let token = response.payload.find(t => t.app.name === TOKEN_NAME);
if (token) {
url = `${GITHUB_HOST}/authorizations/${token.id}`;
yield ajax.raw(url, {
method: 'DELETE',
headers
});
break;
}
let match = response.jqXHR.getResponseHeader("Link").match(/<(.+)>; rel="next"/);
if (match) {
url = match[1];
} else {
break;
}
}
url = `${GITHUB_HOST}/authorizations`;
let response = yield ajax.raw(url, {
method: 'POST',
headers,
data: JSON.stringify({
note: 'Ember Core Dashboard'
})
});
this.set('token', response.payload.token);
return true;
} catch(error) {
if (isUnauthorizedError(error.response)) {
if (error.jqXHR.getResponseHeader('X-GitHub-OTP')) {
throw new Error(E_BAD_OTP);
} else {
throw new Error(E_BAD_CREDENTIALS);
}
}
throw error;
}
}).restartable(),
validateToken: task(function * () {
let { ajax, token } = this.getProperties('ajax', 'token');
try {
if (!token) {
throw new Error('Missing token');
}
let url = `${GITHUB_HOST}/user`;
let headers = {
'Authorization': `token ${token}`,
...GITHUB_HEADERS
};
yield ajax.request(url, { headers });
return true;
} catch(error) {
this.set('token', null);
throw new Error(E_BAD_TOKEN);
}
}).restartable()
});
* {
box-sizing: border-box;
}
body {
margin: 12px 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 12pt;
color: #333;
background: #f9f9f9;
}
h1 {
margin: 32px 0 24px;
color: #999;
font-weight: lighter;
font-size: 12px;
text-align: center;
}
h2 {
margin: 0.67em 0;
font-weight: 300;
font-size: 24px;
text-align: center;
}
.narrow {
width: 340px;
margin: auto;
}
.error {
padding: 15px 20px;
margin: 0 auto;
margin-bottom: 10px;
font-size: 13px;
border-style: solid;
border-width: 1px;
border-radius: 5px;
color: #86181d;
background-color: #ffdce0;
border: 1px solid rgba(27,31,35,0.15);
}
.box {
background-color: #fff;
border: 1px solid #d8dee2;
border-radius: 5px;
padding: 20px;
font-size: 14px;
}
label {
display: block;
font-weight: 600;
}
input {
display: block;
width: 100%;
margin-top: 7px;
margin-bottom: 15px;
padding: 6px 8px;
font-size: 14px;
line-height: 20px;
color: #24292e;
vertical-align: middle;
border: 1px solid #d1d5da;
border-radius: 3px;
outline: none;
box-shadow: inset 0 1px 2px rgba(27,31,35,0.075);
}
input:focus {
border-color: #2188ff;
outline: none;
box-shadow: inset 0 1px 2px rgba(27,31,35,0.075), 0 0 0 0.2em rgba(3,102,214,0.3);
}
button {
display: block;
width: 100%;
text-align: center;
padding: 6px 12px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
user-select: none;
background-repeat: repeat-x;
background-position: -1px -1px;
background-size: 110% 110%;
border: 1px solid rgba(27,31,35,0.2);
border-radius: 0.25em;
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 6px 0px;
color: #24292e;
background-color: #eff3f6;
background-image: linear-gradient(-180deg, #fafbfc 0%, #eff3f6 90%);
}
button:focus {
box-shadow: 0 0 0 0.2em rgba(3,102,214,0.3);
}
button:disabled {
color: rgba(36,41,46,0.4);
background-color: #eff3f6;
background-image: none;
border-color: rgba(27,31,35,0.2);
box-shadow: none;
cursor: default;
}
button.primary {
margin-top: 20px;
color: #fff;
background-color: #28a745;
background-image: linear-gradient(-180deg, #34d058 0%, #28a745 90%);
}
button.primary:focus {
box-shadow: 0 0 0 0.2em rgba(52,208,88,0.3);
}
button.primary:disabled {
color: rgba(255,255,255,0.75);
background-color: #94d3a2;
background-image: none;
border-color: rgba(27,31,35,0.2);
box-shadow: none;
}
button.danger {
color: #cb2431;
background-color: #fafbfc;
}
<h1>Ember Core Dashboard</h1>
{{outlet}}
<div class="narrow">
<h2>Tasks</h2>
<div class="box">
<button>Create Release Blog Post</button>
<button>Something Else</button>
<button>One More Thing™</button>
</div>
</div>
<div class="narrow">
{{#if isCredentials}}
<h2>Sign in to GitHub</h2>
{{else if isOTP}}
<h2>Two-factor authentication</h2>
{{/if}}
{{#if submit.last.isError}}
<p class="error">{{submit.last.error.message}}</p>
{{/if}}
<div class="box">
<form>
{{#if isCredentials}}
<label>
Username
{{input value=username}}
</label>
<label>
Password
{{input type="password" value=password}}
</label>
{{#if submit.isIdle}}
<button class="primary" disabled={{disableSignIn}} {{action (perform submit)}}>Sign in</button>
{{else}}
<button class="primary" disabled>Signing in...</button>
{{/if}}
{{else if isOTP}}
<p>
<label>
Authentication code
{{input value=otp}}
</label>
</p>
<p>
{{#if submit.isIdle}}
<button class="primary" disabled={{disableVerify}} {{action (perform submit)}}>Verify</button>
{{else}}
<button class="primary" disabled>Verifying...</button>
{{/if}}
</p>
{{/if}}
</form>
</div>
</div>
{
"version": "0.12.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.12.0",
"ember-template-compiler": "2.12.0",
"ember-testing": "2.12.0"
},
"addons": {
"ember-ajax": "3.0.0",
"ember-concurrency": "0.8.7",
"ember-data": "2.12.1"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment