mirror of
https://github.com/TryGhost/Ghost-Admin.git
synced 2023-12-14 02:33:04 +01:00
Ghost.org OAuth support (#278)
issue TryGhost/Ghost#7452, requires TryGhost/Ghost#7451 - use a `ghostOAuth` config flag to switch between the old-style per-install auth and centralized OAuth auth based on config provided by the server - add OAuth flows for: - setup - sign-in - sign-up - re-authenticate - add custom `oauth-ghost` authenticator to support our custom data structure - add test helpers to stub successful/failed oauth authentication - hide change password form if using OAuth (temporary - a way to change password via oauth provider will be added later)
This commit is contained in:
parent
7385c26b71
commit
0a163d7333
41
app/authenticators/oauth2-ghost.js
Normal file
41
app/authenticators/oauth2-ghost.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||||
|
import Oauth2Authenticator from './oauth2';
|
||||||
|
import computed from 'ember-computed';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
|
import run from 'ember-runloop';
|
||||||
|
import {assign} from 'ember-platform';
|
||||||
|
import {isEmpty} from 'ember-utils';
|
||||||
|
import {wrap} from 'ember-array/utils';
|
||||||
|
|
||||||
|
export default Oauth2Authenticator.extend({
|
||||||
|
serverTokenEndpoint: computed('ghostPaths.apiRoot', function () {
|
||||||
|
return `${this.get('ghostPaths.apiRoot')}/authentication/ghost`;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// TODO: all this is doing is changing the `data` structure, we should
|
||||||
|
// probably create our own token auth, maybe look at
|
||||||
|
// https://github.com/jpadilla/ember-simple-auth-token
|
||||||
|
authenticate(identification, password, scope = []) {
|
||||||
|
return new RSVP.Promise((resolve, reject) => {
|
||||||
|
// const data = { 'grant_type': 'password', username: identification, password };
|
||||||
|
let data = identification;
|
||||||
|
let serverTokenEndpoint = this.get('serverTokenEndpoint');
|
||||||
|
let scopesString = wrap(scope).join(' ');
|
||||||
|
if (!isEmpty(scopesString)) {
|
||||||
|
data.scope = scopesString;
|
||||||
|
}
|
||||||
|
this.makeRequest(serverTokenEndpoint, data).then((response) => {
|
||||||
|
run(() => {
|
||||||
|
let expiresAt = this._absolutizeExpirationTime(response.expires_in);
|
||||||
|
this._scheduleAccessTokenRefresh(response.expires_in, expiresAt, response.refresh_token);
|
||||||
|
if (!isEmpty(expiresAt)) {
|
||||||
|
response = assign(response, {'expires_at': expiresAt});
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
}, (xhr) => {
|
||||||
|
run(null, reject, xhr.responseJSON || xhr.responseText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -12,8 +12,10 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||||
submitting: false,
|
submitting: false,
|
||||||
authenticationError: null,
|
authenticationError: null,
|
||||||
|
|
||||||
|
config: injectService(),
|
||||||
notifications: injectService(),
|
notifications: injectService(),
|
||||||
session: injectService(),
|
session: injectService(),
|
||||||
|
torii: injectService(),
|
||||||
|
|
||||||
identification: computed('session.user.email', function () {
|
identification: computed('session.user.email', function () {
|
||||||
return this.get('session.user.email');
|
return this.get('session.user.email');
|
||||||
|
@ -35,8 +37,7 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
_passwordConfirm() {
|
||||||
confirm() {
|
|
||||||
// Manually trigger events for input fields, ensuring legacy compatibility with
|
// Manually trigger events for input fields, ensuring legacy compatibility with
|
||||||
// browsers and password managers that don't send proper events on autofill
|
// browsers and password managers that don't send proper events on autofill
|
||||||
$('#login').find('input').trigger('change');
|
$('#login').find('input').trigger('change');
|
||||||
|
@ -45,7 +46,7 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||||
|
|
||||||
this.validate({property: 'signin'}).then(() => {
|
this.validate({property: 'signin'}).then(() => {
|
||||||
this._authenticate().then(() => {
|
this._authenticate().then(() => {
|
||||||
this.get('notifications').closeAlerts('post.save');
|
this.get('notifications').closeAlerts();
|
||||||
this.send('closeModal');
|
this.send('closeModal');
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error && error.errors) {
|
if (error && error.errors) {
|
||||||
|
@ -64,6 +65,41 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||||
}, () => {
|
}, () => {
|
||||||
this.get('hasValidated').pushObject('password');
|
this.get('hasValidated').pushObject('password');
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_oauthConfirm() {
|
||||||
|
// TODO: remove duplication between signin/signup/re-auth
|
||||||
|
let authStrategy = 'authenticator:oauth2-ghost';
|
||||||
|
|
||||||
|
this.toggleProperty('submitting');
|
||||||
|
this.set('authenticationError', '');
|
||||||
|
|
||||||
|
this.get('torii')
|
||||||
|
.open('ghost-oauth2', {type: 'signin'})
|
||||||
|
.then((authentication) => {
|
||||||
|
this.get('session').set('skipAuthSuccessHandler', true);
|
||||||
|
|
||||||
|
this.get('session').authenticate(authStrategy, authentication).finally(() => {
|
||||||
|
this.get('session').set('skipAuthSuccessHandler', undefined);
|
||||||
|
|
||||||
|
this.toggleProperty('submitting');
|
||||||
|
this.get('notifications').closeAlerts();
|
||||||
|
this.send('closeModal');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.toggleProperty('submitting');
|
||||||
|
this.set('authenticationError', 'Authentication with Ghost.org denied or failed');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
confirm() {
|
||||||
|
if (this.get('config.ghostOAuth')) {
|
||||||
|
return this._oauthConfirm();
|
||||||
|
} else {
|
||||||
|
return this._passwordConfirm();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -89,15 +89,7 @@ export default Controller.extend(ValidationEngine, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
_passwordSetup() {
|
||||||
preValidate(model) {
|
|
||||||
// Only triggers validation if a value has been entered, preventing empty errors on focusOut
|
|
||||||
if (this.get(model)) {
|
|
||||||
this.validate({property: model});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
let setupProperties = ['blogTitle', 'name', 'email', 'password'];
|
let setupProperties = ['blogTitle', 'name', 'email', 'password'];
|
||||||
let data = this.getProperties(setupProperties);
|
let data = this.getProperties(setupProperties);
|
||||||
let config = this.get('config');
|
let config = this.get('config');
|
||||||
|
@ -145,6 +137,58 @@ export default Controller.extend(ValidationEngine, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: for OAuth ghost is in the "setup completed" step as soon
|
||||||
|
// as a user has been authenticated so we need to use the standard settings
|
||||||
|
// update to set the blog title before redirecting
|
||||||
|
_oauthSetup() {
|
||||||
|
let blogTitle = this.get('blogTitle');
|
||||||
|
let config = this.get('config');
|
||||||
|
|
||||||
|
this.get('hasValidated').addObjects(['blogTitle', 'session']);
|
||||||
|
|
||||||
|
return this.validate().then(() => {
|
||||||
|
this.store.queryRecord('setting', {type: 'blog,theme,private'})
|
||||||
|
.then((settings) => {
|
||||||
|
settings.set('title', blogTitle);
|
||||||
|
|
||||||
|
return settings.save()
|
||||||
|
.then((settings) => {
|
||||||
|
// update the config so that the blog title shown in
|
||||||
|
// the nav bar is also updated
|
||||||
|
config.set('blogTitle', settings.get('title'));
|
||||||
|
|
||||||
|
// this.blogCreated is used by step 3 to check if step 2
|
||||||
|
// has been completed
|
||||||
|
this.set('blogCreated', true);
|
||||||
|
return this.afterAuthentication(settings);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this._handleSaveError(error);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.toggleProperty('submitting');
|
||||||
|
this.set('session.skipAuthSuccessHandler', undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
preValidate(model) {
|
||||||
|
// Only triggers validation if a value has been entered, preventing empty errors on focusOut
|
||||||
|
if (this.get(model)) {
|
||||||
|
this.validate({property: model});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
if (this.get('config.ghostOAuth')) {
|
||||||
|
return this._oauthSetup();
|
||||||
|
} else {
|
||||||
|
return this._passwordSetup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setImage(image) {
|
setImage(image) {
|
||||||
this.set('image', image);
|
this.set('image', image);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import injectController from 'ember-controller/inject';
|
||||||
import {isEmberArray} from 'ember-array/utils';
|
import {isEmberArray} from 'ember-array/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VersionMismatchError,
|
|
||||||
isVersionMismatchError
|
isVersionMismatchError
|
||||||
} from 'ghost-admin/services/ajax';
|
} from 'ghost-admin/services/ajax';
|
||||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||||
|
@ -15,55 +14,23 @@ export default Controller.extend(ValidationEngine, {
|
||||||
loggingIn: false,
|
loggingIn: false,
|
||||||
authProperties: ['identification', 'password'],
|
authProperties: ['identification', 'password'],
|
||||||
|
|
||||||
|
ajax: injectService(),
|
||||||
|
application: injectController(),
|
||||||
|
config: injectService(),
|
||||||
ghostPaths: injectService(),
|
ghostPaths: injectService(),
|
||||||
notifications: injectService(),
|
notifications: injectService(),
|
||||||
session: injectService(),
|
session: injectService(),
|
||||||
application: injectController(),
|
|
||||||
ajax: injectService(),
|
|
||||||
flowErrors: '',
|
flowErrors: '',
|
||||||
|
|
||||||
// ValidationEngine settings
|
// ValidationEngine settings
|
||||||
validationType: 'signin',
|
validationType: 'signin',
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
authenticate() {
|
validateAndAuthenticate() {
|
||||||
let model = this.get('model');
|
let model = this.get('model');
|
||||||
let authStrategy = 'authenticator:oauth2';
|
let authStrategy = 'authenticator:oauth2';
|
||||||
|
|
||||||
// Authentication transitions to posts.index, we can leave spinner running unless there is an error
|
|
||||||
this.get('session').authenticate(authStrategy, model.get('identification'), model.get('password')).catch((error) => {
|
|
||||||
this.toggleProperty('loggingIn');
|
|
||||||
|
|
||||||
if (error && error.errors) {
|
|
||||||
// we don't get back an ember-data/ember-ajax error object
|
|
||||||
// back so we need to pass in a null status in order to
|
|
||||||
// test against the payload
|
|
||||||
if (isVersionMismatchError(null, error)) {
|
|
||||||
let versionMismatchError = new VersionMismatchError(error);
|
|
||||||
return this.get('notifications').showAPIError(versionMismatchError);
|
|
||||||
}
|
|
||||||
|
|
||||||
error.errors.forEach((err) => {
|
|
||||||
err.message = err.message.htmlSafe();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.set('flowErrors', error.errors[0].message.string);
|
|
||||||
|
|
||||||
if (error.errors[0].message.string.match(/user with that email/)) {
|
|
||||||
this.get('model.errors').add('identification', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.errors[0].message.string.match(/password is incorrect/)) {
|
|
||||||
this.get('model.errors').add('password', '');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Connection errors don't return proper status message, only req.body
|
|
||||||
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
validateAndAuthenticate() {
|
|
||||||
this.set('flowErrors', '');
|
this.set('flowErrors', '');
|
||||||
// Manually trigger events for input fields, ensuring legacy compatibility with
|
// Manually trigger events for input fields, ensuring legacy compatibility with
|
||||||
// browsers and password managers that don't send proper events on autofill
|
// browsers and password managers that don't send proper events on autofill
|
||||||
|
@ -73,7 +40,7 @@ export default Controller.extend(ValidationEngine, {
|
||||||
this.get('hasValidated').addObjects(this.authProperties);
|
this.get('hasValidated').addObjects(this.authProperties);
|
||||||
this.validate({property: 'signin'}).then(() => {
|
this.validate({property: 'signin'}).then(() => {
|
||||||
this.toggleProperty('loggingIn');
|
this.toggleProperty('loggingIn');
|
||||||
this.send('authenticate');
|
this.send('authenticate', authStrategy, [model.get('identification'), model.get('password')]);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.set('flowErrors', 'Please fill out the form to sign in.');
|
this.set('flowErrors', 'Please fill out the form to sign in.');
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default Controller.extend({
|
||||||
_scratchTwitter: null,
|
_scratchTwitter: null,
|
||||||
|
|
||||||
ajax: injectService(),
|
ajax: injectService(),
|
||||||
|
config: injectService(),
|
||||||
dropdown: injectService(),
|
dropdown: injectService(),
|
||||||
ghostPaths: injectService(),
|
ghostPaths: injectService(),
|
||||||
notifications: injectService(),
|
notifications: injectService(),
|
||||||
|
@ -50,6 +51,10 @@ export default Controller.extend({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
canChangePassword: computed('config.ghostOAuth', 'isAdminUserOnOwnerProfile', function () {
|
||||||
|
return !this.get('config.ghostOAuth') && !this.get('isAdminUserOnOwnerProfile');
|
||||||
|
}),
|
||||||
|
|
||||||
// duplicated in gh-user-active -- find a better home and consolidate?
|
// duplicated in gh-user-active -- find a better home and consolidate?
|
||||||
userDefault: computed('ghostPaths', function () {
|
userDefault: computed('ghostPaths', function () {
|
||||||
return `${this.get('ghostPaths.subdir')}/ghost/img/user-image.png`;
|
return `${this.get('ghostPaths.subdir')}/ghost/img/user-image.png`;
|
||||||
|
|
|
@ -36,6 +36,21 @@ export default function mockAuthentication(server) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.get('/authentication/invitation/', function (db, request) {
|
||||||
|
let {email} = request.queryParams;
|
||||||
|
let [invite] = db.invites.where({email});
|
||||||
|
let user = db.users.find(invite.created_by);
|
||||||
|
let valid = !!invite;
|
||||||
|
let invitedBy = user && user.name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
invitation: [{
|
||||||
|
valid,
|
||||||
|
invitedBy
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
/* Setup ---------------------------------------------------------------- */
|
/* Setup ---------------------------------------------------------------- */
|
||||||
|
|
||||||
server.post('/authentication/setup', function (db, request) {
|
server.post('/authentication/setup', function (db, request) {
|
||||||
|
@ -70,4 +85,19 @@ export default function mockAuthentication(server) {
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* OAuth ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
server.post('/authentication/ghost', function (db) {
|
||||||
|
if (!db.users.length) {
|
||||||
|
let [role] = db.roles.where({name: 'Owner'});
|
||||||
|
server.create('user', {email: 'oauthtest@example.com', roles: [role]});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
access_token: '5JhTdKI7PpoZv4ROsFoERc6wCHALKFH5jxozwOOAErmUzWrFNARuH1q01TYTKeZkPW7FmV5MJ2fU00pg9sm4jtH3Z1LjCf8D6nNqLYCfFb2YEKyuvG7zHj4jZqSYVodN2YTCkcHv6k8oJ54QXzNTLIDMlCevkOebm5OjxGiJpafMxncm043q9u1QhdU9eee3zouGRMVVp8zkKVoo5zlGMi3zvS2XDpx7xsfk8hKHpUgd7EDDQxmMueifWv7hv6n',
|
||||||
|
expires_in: 3600,
|
||||||
|
refresh_token: 'XP13eDjwV5mxOcrq1jkIY9idhdvN3R1Br5vxYpYIub2P5Hdc8pdWMOGmwFyoUshiEB62JWHTl8H1kACJR18Z8aMXbnk5orG28br2kmVgtVZKqOSoiiWrQoeKTqrRV0t7ua8uY5HdDUaKpnYKyOdpagsSPn3WEj8op4vHctGL3svOWOjZhq6F2XeVPMR7YsbiwBE8fjT3VhTB3KRlBtWZd1rE0Qo2EtSplWyjGKv1liAEiL0ndQoLeeSOCH4rTP7'
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,20 +11,23 @@ export default Route.extend(styleBody, {
|
||||||
ghostPaths: injectService(),
|
ghostPaths: injectService(),
|
||||||
session: injectService(),
|
session: injectService(),
|
||||||
ajax: injectService(),
|
ajax: injectService(),
|
||||||
|
config: injectService(),
|
||||||
|
|
||||||
// use the beforeModel hook to check to see whether or not setup has been
|
// use the beforeModel hook to check to see whether or not setup has been
|
||||||
// previously completed. If it has, stop the transition into the setup page.
|
// previously completed. If it has, stop the transition into the setup page.
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
if (this.get('session.isAuthenticated')) {
|
// with OAuth auth users are authenticated on step 2 so we
|
||||||
|
// can't use the session.isAuthenticated shortcut
|
||||||
|
if (!this.get('config.ghostOAuth') && this.get('session.isAuthenticated')) {
|
||||||
this.transitionTo(Configuration.routeIfAlreadyAuthenticated);
|
this.transitionTo(Configuration.routeIfAlreadyAuthenticated);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let authUrl = this.get('ghostPaths.url').api('authentication', 'setup');
|
let authUrl = this.get('ghostPaths.url').api('authentication', 'setup');
|
||||||
|
|
||||||
// If user is not logged in, check the state of the setup process via the API
|
// check the state of the setup process via the API
|
||||||
return this.get('ajax').request(authUrl)
|
return this.get('ajax').request(authUrl)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
let [setup] = result.setup;
|
let [setup] = result.setup;
|
||||||
|
|
67
app/routes/setup/two.js
Normal file
67
app/routes/setup/two.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import Route from 'ember-route';
|
||||||
|
import injectService from 'ember-service/inject';
|
||||||
|
import {
|
||||||
|
VersionMismatchError,
|
||||||
|
isVersionMismatchError
|
||||||
|
} from 'ghost-admin/services/ajax';
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
|
||||||
|
session: injectService(),
|
||||||
|
notifications: injectService(),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// TODO: reduce duplication with setup/signin/signup routes
|
||||||
|
authenticateWithGhostOrg() {
|
||||||
|
let authStrategy = 'authenticator:oauth2-ghost';
|
||||||
|
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
this.set('controller.flowErrors', '');
|
||||||
|
|
||||||
|
this.get('torii')
|
||||||
|
.open('ghost-oauth2', {type: 'setup'})
|
||||||
|
.then((authentication) => {
|
||||||
|
this.send('authenticate', authStrategy, [authentication]);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
this.set('controller.flowErrors', 'Authentication with Ghost.org denied or failed');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
authenticate(strategy, authentication) {
|
||||||
|
// we don't want to redirect after sign-in during setup
|
||||||
|
this.set('session.skipAuthSuccessHandler', true);
|
||||||
|
|
||||||
|
// Authentication transitions to posts.index, we can leave spinner running unless there is an error
|
||||||
|
this.get('session')
|
||||||
|
.authenticate(strategy, ...authentication)
|
||||||
|
.then(() => {
|
||||||
|
this.get('controller.errors').remove('session');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error && error.errors) {
|
||||||
|
// we don't get back an ember-data/ember-ajax error object
|
||||||
|
// back so we need to pass in a null status in order to
|
||||||
|
// test against the payload
|
||||||
|
if (isVersionMismatchError(null, error)) {
|
||||||
|
let versionMismatchError = new VersionMismatchError(error);
|
||||||
|
return this.get('notifications').showAPIError(versionMismatchError);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.errors.forEach((err) => {
|
||||||
|
err.message = err.message.htmlSafe();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set('controller.flowErrors', error.errors[0].message.string);
|
||||||
|
} else {
|
||||||
|
// Connection errors don't return proper status message, only req.body
|
||||||
|
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -4,6 +4,10 @@ import EmberObject from 'ember-object';
|
||||||
import styleBody from 'ghost-admin/mixins/style-body';
|
import styleBody from 'ghost-admin/mixins/style-body';
|
||||||
import Configuration from 'ember-simple-auth/configuration';
|
import Configuration from 'ember-simple-auth/configuration';
|
||||||
import DS from 'ember-data';
|
import DS from 'ember-data';
|
||||||
|
import {
|
||||||
|
VersionMismatchError,
|
||||||
|
isVersionMismatchError
|
||||||
|
} from 'ghost-admin/services/ajax';
|
||||||
|
|
||||||
const {Errors} = DS;
|
const {Errors} = DS;
|
||||||
|
|
||||||
|
@ -13,6 +17,7 @@ export default Route.extend(styleBody, {
|
||||||
classNames: ['ghost-login'],
|
classNames: ['ghost-login'],
|
||||||
|
|
||||||
session: injectService(),
|
session: injectService(),
|
||||||
|
notifications: injectService(),
|
||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -39,5 +44,60 @@ export default Route.extend(styleBody, {
|
||||||
// clear the properties that hold the credentials when we're no longer on the signin screen
|
// clear the properties that hold the credentials when we're no longer on the signin screen
|
||||||
controller.set('model.identification', '');
|
controller.set('model.identification', '');
|
||||||
controller.set('model.password', '');
|
controller.set('model.password', '');
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
authenticateWithGhostOrg() {
|
||||||
|
let authStrategy = 'authenticator:oauth2-ghost';
|
||||||
|
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
this.set('controller.flowErrors', '');
|
||||||
|
|
||||||
|
this.get('torii')
|
||||||
|
.open('ghost-oauth2', {type: 'signin'})
|
||||||
|
.then((authentication) => {
|
||||||
|
this.send('authenticate', authStrategy, [authentication]);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
this.set('controller.flowErrors', 'Authentication with Ghost.org denied or failed');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
authenticate(strategy, authentication) {
|
||||||
|
// Authentication transitions to posts.index, we can leave spinner running unless there is an error
|
||||||
|
this.get('session')
|
||||||
|
.authenticate(strategy, ...authentication)
|
||||||
|
.catch((error) => {
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
|
||||||
|
if (error && error.errors) {
|
||||||
|
// we don't get back an ember-data/ember-ajax error object
|
||||||
|
// back so we need to pass in a null status in order to
|
||||||
|
// test against the payload
|
||||||
|
if (isVersionMismatchError(null, error)) {
|
||||||
|
let versionMismatchError = new VersionMismatchError(error);
|
||||||
|
return this.get('notifications').showAPIError(versionMismatchError);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.errors.forEach((err) => {
|
||||||
|
err.message = err.message.htmlSafe();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set('controller.flowErrors', error.errors[0].message.string);
|
||||||
|
|
||||||
|
if (error.errors[0].message.string.match(/user with that email/)) {
|
||||||
|
this.get('controller.model.errors').add('identification', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.errors[0].message.string.match(/password is incorrect/)) {
|
||||||
|
this.get('controller.model.errors').add('password', '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Connection errors don't return proper status message, only req.body
|
||||||
|
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,11 @@ import Route from 'ember-route';
|
||||||
import RSVP from 'rsvp';
|
import RSVP from 'rsvp';
|
||||||
import injectService from 'ember-service/inject';
|
import injectService from 'ember-service/inject';
|
||||||
import EmberObject from 'ember-object';
|
import EmberObject from 'ember-object';
|
||||||
|
import {assign} from 'ember-platform';
|
||||||
|
import {
|
||||||
|
VersionMismatchError,
|
||||||
|
isVersionMismatchError
|
||||||
|
} from 'ghost-admin/services/ajax';
|
||||||
|
|
||||||
import DS from 'ember-data';
|
import DS from 'ember-data';
|
||||||
import Configuration from 'ember-simple-auth/configuration';
|
import Configuration from 'ember-simple-auth/configuration';
|
||||||
|
@ -61,6 +66,8 @@ export default Route.extend(styleBody, {
|
||||||
return resolve(this.transitionTo('signin'));
|
return resolve(this.transitionTo('signin'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model.set('invitedBy', response.invitation[0].invitedBy);
|
||||||
|
|
||||||
resolve(model);
|
resolve(model);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
resolve(model);
|
resolve(model);
|
||||||
|
@ -73,5 +80,64 @@ export default Route.extend(styleBody, {
|
||||||
|
|
||||||
// clear the properties that hold the sensitive data from the controller
|
// clear the properties that hold the sensitive data from the controller
|
||||||
this.controllerFor('signup').setProperties({email: '', password: '', token: ''});
|
this.controllerFor('signup').setProperties({email: '', password: '', token: ''});
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
authenticateWithGhostOrg() {
|
||||||
|
let authStrategy = 'authenticator:oauth2-ghost';
|
||||||
|
let inviteToken = this.get('controller.model.token');
|
||||||
|
let email = this.get('controller.model.email');
|
||||||
|
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
this.set('controller.flowErrors', '');
|
||||||
|
|
||||||
|
this.get('torii')
|
||||||
|
.open('ghost-oauth2', {email, type: 'invite'})
|
||||||
|
.then((authentication) => {
|
||||||
|
let _authentication = assign({}, authentication, {inviteToken});
|
||||||
|
this.send('authenticate', authStrategy, [_authentication]);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
this.set('controller.flowErrors', 'Authentication with Ghost.org denied or failed');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: this is duplicated with the signin route - maybe extract into a mixin?
|
||||||
|
authenticate(strategy, authentication) {
|
||||||
|
// Authentication transitions to posts.index, we can leave spinner running unless there is an error
|
||||||
|
this.get('session')
|
||||||
|
.authenticate(strategy, ...authentication)
|
||||||
|
.catch((error) => {
|
||||||
|
this.toggleProperty('controller.loggingIn');
|
||||||
|
|
||||||
|
if (error && error.errors) {
|
||||||
|
// we don't get back an ember-data/ember-ajax error object
|
||||||
|
// back so we need to pass in a null status in order to
|
||||||
|
// test against the payload
|
||||||
|
if (isVersionMismatchError(null, error)) {
|
||||||
|
let versionMismatchError = new VersionMismatchError(error);
|
||||||
|
return this.get('notifications').showAPIError(versionMismatchError);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.errors.forEach((err) => {
|
||||||
|
err.message = err.message.htmlSafe();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set('controller.flowErrors', error.errors[0].message.string);
|
||||||
|
|
||||||
|
if (error.errors[0].message.string.match(/user with that email/)) {
|
||||||
|
this.get('controller.model.errors').add('identification', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.errors[0].message.string.match(/password is incorrect/)) {
|
||||||
|
this.get('controller.model.errors').add('password', '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Connection errors don't return proper status message, only req.body
|
||||||
|
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,8 +3,9 @@ import Ember from 'ember';
|
||||||
import Service from 'ember-service';
|
import Service from 'ember-service';
|
||||||
import computed from 'ember-computed';
|
import computed from 'ember-computed';
|
||||||
import injectService from 'ember-service/inject';
|
import injectService from 'ember-service/inject';
|
||||||
|
import {isBlank} from 'ember-utils';
|
||||||
|
|
||||||
// ember-cli-shims doesn't export _ProxyMixin
|
// ember-cli-shims doesn't export _ProxyMixin ot testing
|
||||||
const {_ProxyMixin} = Ember;
|
const {_ProxyMixin} = Ember;
|
||||||
const {isNumeric} = $;
|
const {isNumeric} = $;
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ export default Service.extend(_ProxyMixin, {
|
||||||
return config;
|
return config;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
availableTimezones: computed(function() {
|
availableTimezones: computed(function () {
|
||||||
let timezonesUrl = this.get('ghostPaths.url').api('configuration', 'timezones');
|
let timezonesUrl = this.get('ghostPaths.url').api('configuration', 'timezones');
|
||||||
|
|
||||||
return this.get('ajax').request(timezonesUrl).then((configTimezones) => {
|
return this.get('ajax').request(timezonesUrl).then((configTimezones) => {
|
||||||
|
@ -57,5 +58,9 @@ export default Service.extend(_ProxyMixin, {
|
||||||
|
|
||||||
return timezonesObj;
|
return timezonesObj;
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
ghostOAuth: computed('ghostAuthId', function () {
|
||||||
|
return !isBlank(this.get('ghostAuthId'));
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,12 +4,18 @@
|
||||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||||
|
|
||||||
<div class="modal-body {{if authenticationError 'error'}}">
|
<div class="modal-body {{if authenticationError 'error'}}">
|
||||||
|
|
||||||
|
{{#if config.ghostOAuth}}
|
||||||
|
{{#gh-spin-button class="login btn btn-blue btn-block" type="submit" action="confirm" tabindex="3" submitting=loggingIn autoWidth="false"}}Sign in with Ghost{{/gh-spin-button}}
|
||||||
|
{{else}}
|
||||||
<form id="login" class="login-form" method="post" novalidate="novalidate" {{action "confirm" on="submit"}}>
|
<form id="login" class="login-form" method="post" novalidate="novalidate" {{action "confirm" on="submit"}}>
|
||||||
{{#gh-validation-status-container class="password-wrap" errors=errors property="password" hasValidated=hasValidated}}
|
{{#gh-validation-status-container class="password-wrap" errors=errors property="password" hasValidated=hasValidated}}
|
||||||
{{gh-input password class="password" type="password" placeholder="Password" name="password" update=(action (mut password))}}
|
{{gh-input password class="password" type="password" placeholder="Password" name="password" update=(action (mut password))}}
|
||||||
{{/gh-validation-status-container}}
|
{{/gh-validation-status-container}}
|
||||||
{{#gh-spin-button class="btn btn-blue" type="submit" submitting=submitting}}Log in{{/gh-spin-button}}
|
{{#gh-spin-button class="btn btn-blue" type="submit" submitting=submitting}}Log in{{/gh-spin-button}}
|
||||||
</form>
|
</form>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if authenticationError}}
|
{{#if authenticationError}}
|
||||||
<p class="response">{{authenticationError}}</p>
|
<p class="response">{{authenticationError}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,8 +1,39 @@
|
||||||
<header>
|
{{#if config.ghostOAuth}}
|
||||||
<h1>Create your account</h1>
|
<header>
|
||||||
</header>
|
<h1>Setup your blog</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
<form id="setup" class="gh-flow-create">
|
<form id="setup" class="gh-flow-create" {{action "setup" on="submit"}}>
|
||||||
|
{{#gh-form-group errors=errors hasValidated=hasValidated property="session"}}
|
||||||
|
{{#gh-spin-button class="login btn btn-blue btn-block" type="button" action="authenticateWithGhostOrg" tabindex="3" submitting=loggingIn autoWidth="false"}}
|
||||||
|
{{#if session.isAuthenticated}}
|
||||||
|
Connected: {{session.user.email}}
|
||||||
|
{{else}}
|
||||||
|
Sign in with Ghost
|
||||||
|
{{/if}}
|
||||||
|
{{/gh-spin-button}}
|
||||||
|
{{gh-error-message errors=errors property="session"}}
|
||||||
|
{{/gh-form-group}}
|
||||||
|
|
||||||
|
{{#gh-form-group errors=errors hasValidated=hasValidated property="blogTitle"}}
|
||||||
|
<label for="blog-title">Blog title</label>
|
||||||
|
<span class="input-icon icon-content">
|
||||||
|
{{gh-input blogTitle tabindex="4" type="text" name="blog-title" placeholder="Eg. The Daily Awesome" autocorrect="off" focusOut=(action "preValidate" "blogTitle") update=(action (mut blogTitle)) onenter=(action "setup")}}
|
||||||
|
</span>
|
||||||
|
{{gh-error-message errors=errors property="blogTitle"}}
|
||||||
|
{{/gh-form-group}}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{#gh-spin-button type="submit" tabindex="5" class="btn btn-green btn-lg btn-block" action=(action 'setup') disabled=submitDisabled submitting=submitting autoWidth="false"}}
|
||||||
|
Last step: Invite your team <i class="icon-chevron"></i>
|
||||||
|
{{/gh-spin-button}}
|
||||||
|
{{else}}
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Create your account</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form id="setup" class="gh-flow-create">
|
||||||
{{!-- Horrible hack to prevent Chrome from incorrectly auto-filling inputs --}}
|
{{!-- Horrible hack to prevent Chrome from incorrectly auto-filling inputs --}}
|
||||||
<input style="display:none;" type="text" name="fakeusernameremembered"/>
|
<input style="display:none;" type="text" name="fakeusernameremembered"/>
|
||||||
<input style="display:none;" type="password" name="fakepasswordremembered"/>
|
<input style="display:none;" type="password" name="fakepasswordremembered"/>
|
||||||
|
@ -39,6 +70,7 @@
|
||||||
{{#gh-spin-button type="submit" tabindex="5" class="btn btn-green btn-lg btn-block" action="setup" submitting=submitting autoWidth="false"}}
|
{{#gh-spin-button type="submit" tabindex="5" class="btn btn-green btn-lg btn-block" action="setup" submitting=submitting autoWidth="false"}}
|
||||||
Last step: Invite your team <i class="icon-chevron"></i>
|
Last step: Invite your team <i class="icon-chevron"></i>
|
||||||
{{/gh-spin-button}}
|
{{/gh-spin-button}}
|
||||||
</form>
|
</form>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<p class="main-error">{{{flowErrors}}}</p>
|
<p class="main-error">{{{flowErrors}}}</p>
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
<div class="gh-flow">
|
<div class="gh-flow">
|
||||||
<div class="gh-flow-content-wrap">
|
<div class="gh-flow-content-wrap">
|
||||||
<section class="gh-flow-content">
|
<section class="gh-flow-content">
|
||||||
|
{{#if config.ghostOAuth}}
|
||||||
|
<header>
|
||||||
|
<h1>{{config.blogTitle}}</h1>
|
||||||
|
</header>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<form id="login" class="gh-signin" method="post" novalidate="novalidate">
|
<form id="login" class="gh-signin" method="post" novalidate="novalidate">
|
||||||
|
{{#if config.ghostOAuth}}
|
||||||
|
{{#gh-spin-button class="login btn btn-blue btn-block" type="submit" action="authenticateWithGhostOrg" tabindex="3" submitting=loggingIn autoWidth="false"}}Sign in with Ghost{{/gh-spin-button}}
|
||||||
|
{{else}}
|
||||||
{{#gh-form-group errors=model.errors hasValidated=hasValidated property="identification"}}
|
{{#gh-form-group errors=model.errors hasValidated=hasValidated property="identification"}}
|
||||||
<span class="input-icon icon-mail">
|
<span class="input-icon icon-mail">
|
||||||
{{gh-trim-focus-input model.identification class="email" type="email" placeholder="Email Address" name="identification" autocapitalize="off" autocorrect="off" tabindex="1" focusOut=(action "validate" "identification") update=(action (mut model.identification))}}
|
{{gh-trim-focus-input model.identification class="email" type="email" placeholder="Email Address" name="identification" autocapitalize="off" autocorrect="off" tabindex="1" focusOut=(action "validate" "identification") update=(action (mut model.identification))}}
|
||||||
|
@ -14,6 +23,7 @@
|
||||||
</span>
|
</span>
|
||||||
{{/gh-form-group}}
|
{{/gh-form-group}}
|
||||||
{{#gh-spin-button class="login btn btn-blue btn-block" type="submit" action="validateAndAuthenticate" tabindex="3" submitting=loggingIn autoWidth="false"}}Sign in{{/gh-spin-button}}
|
{{#gh-spin-button class="login btn btn-blue btn-block" type="submit" action="validateAndAuthenticate" tabindex="3" submitting=loggingIn autoWidth="false"}}Sign in{{/gh-spin-button}}
|
||||||
|
{{/if}}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="main-error">{{{flowErrors}}}</p>
|
<p class="main-error">{{{flowErrors}}}</p>
|
||||||
|
|
|
@ -2,6 +2,21 @@
|
||||||
|
|
||||||
<div class="gh-flow-content-wrap">
|
<div class="gh-flow-content-wrap">
|
||||||
<section class="gh-flow-content">
|
<section class="gh-flow-content">
|
||||||
|
{{#if config.ghostOAuth}}
|
||||||
|
<header>
|
||||||
|
<h1>{{config.blogTitle}}</h1>
|
||||||
|
<p>
|
||||||
|
{{!-- TODO: show invite creator's name/email --}}
|
||||||
|
Accept your invite from <strong>{{model.invitedBy}}</strong>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form id="signup" class="gh-signin" method="post" novalidate="novalidate">
|
||||||
|
{{#gh-spin-button class="login btn btn-blue btn-block" type="submit" action="authenticateWithGhostOrg" tabindex="3" submitting=loggingIn autoWidth="false"}}
|
||||||
|
Sign in with Ghost to accept
|
||||||
|
{{/gh-spin-button}}
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
<header>
|
<header>
|
||||||
<h1>Create your account</h1>
|
<h1>Create your account</h1>
|
||||||
</header>
|
</header>
|
||||||
|
@ -38,6 +53,8 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{#gh-spin-button tabindex="3" type="submit" class="btn btn-green btn-lg btn-block" action="signup" submitting=submitting autoWidth="false"}}Create Account{{/gh-spin-button}}
|
{{#gh-spin-button tabindex="3" type="submit" class="btn btn-green btn-lg btn-block" action="signup" submitting=submitting autoWidth="false"}}Create Account{{/gh-spin-button}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<p class="main-error">{{{flowErrors}}}</p>
|
<p class="main-error">{{{flowErrors}}}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -171,9 +171,10 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form> {{! user details form }}
|
</form> {{! user details form }}
|
||||||
<form class="user-profile" novalidate="novalidate" autocomplete="off" {{action (perform user.saveNewPassword) on="submit"}}>
|
|
||||||
{{!-- If an administrator is viewing Owner's profile then hide inputs for change password --}}
|
{{!-- If an administrator is viewing Owner's profile or we're using Ghost.org OAuth then hide inputs for change password --}}
|
||||||
{{#unless isAdminUserOnOwnerProfile}}
|
{{#if canChangePassword}}
|
||||||
|
<form id="password-reset" class="user-profile" novalidate="novalidate" autocomplete="off" {{action (perform user.saveNewPassword) on="submit"}}>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{{#unless isNotOwnProfile}}
|
{{#unless isNotOwnProfile}}
|
||||||
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="password"}}
|
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="password"}}
|
||||||
|
@ -199,7 +200,7 @@
|
||||||
{{#gh-task-button class="btn btn-red button-change-password" task=user.saveNewPassword}}Change Password{{/gh-task-button}}
|
{{#gh-task-button class="btn btn-red button-change-password" task=user.saveNewPassword}}Change Password{{/gh-task-button}}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{{/unless}}
|
|
||||||
</form> {{! change password form }}
|
</form> {{! change password form }}
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
33
app/torii-providers/ghost-oauth2.js
Normal file
33
app/torii-providers/ghost-oauth2.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import Oauth2 from 'torii/providers/oauth2-code';
|
||||||
|
import injectService from 'ember-service/inject';
|
||||||
|
import computed from 'ember-computed';
|
||||||
|
|
||||||
|
let GhostOauth2 = Oauth2.extend({
|
||||||
|
|
||||||
|
config: injectService(),
|
||||||
|
|
||||||
|
name: 'ghost-oauth2',
|
||||||
|
baseUrl: 'http://devauth.ghost.org:8080/oauth2/authorize',
|
||||||
|
apiKey: computed(function () {
|
||||||
|
return this.get('config.ghostAuthId');
|
||||||
|
}),
|
||||||
|
|
||||||
|
optionalUrlParams: ['type', 'email'],
|
||||||
|
|
||||||
|
responseParams: ['code'],
|
||||||
|
|
||||||
|
// we want to redirect to the ghost admin app by default
|
||||||
|
redirectUri: window.location.href.replace(/(\/ghost)(.*)/, '$1/'),
|
||||||
|
|
||||||
|
open(options) {
|
||||||
|
if (options.type) {
|
||||||
|
this.set('type', options.type);
|
||||||
|
}
|
||||||
|
if (options.email) {
|
||||||
|
this.set('email', options.email);
|
||||||
|
}
|
||||||
|
return this._super(...arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GhostOauth2;
|
|
@ -4,17 +4,23 @@ export default BaseValidator.extend({
|
||||||
properties: ['name', 'email', 'password'],
|
properties: ['name', 'email', 'password'],
|
||||||
|
|
||||||
name(model) {
|
name(model) {
|
||||||
|
let usingOAuth = model.get('config.ghostOAuth');
|
||||||
let name = model.get('name');
|
let name = model.get('name');
|
||||||
|
|
||||||
if (!validator.isLength(name, 1)) {
|
if (!usingOAuth && !validator.isLength(name, 1)) {
|
||||||
model.get('errors').add('name', 'Please enter a name.');
|
model.get('errors').add('name', 'Please enter a name.');
|
||||||
this.invalidate();
|
this.invalidate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
email(model) {
|
email(model) {
|
||||||
|
let usingOAuth = model.get('config.ghostOAuth');
|
||||||
let email = model.get('email');
|
let email = model.get('email');
|
||||||
|
|
||||||
|
if (usingOAuth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (validator.empty(email)) {
|
if (validator.empty(email)) {
|
||||||
model.get('errors').add('email', 'Please enter an email.');
|
model.get('errors').add('email', 'Please enter an email.');
|
||||||
this.invalidate();
|
this.invalidate();
|
||||||
|
@ -25,9 +31,10 @@ export default BaseValidator.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
password(model) {
|
password(model) {
|
||||||
|
let usingOAuth = model.get('config.ghostOAuth');
|
||||||
let password = model.get('password');
|
let password = model.get('password');
|
||||||
|
|
||||||
if (!validator.isLength(password, 8)) {
|
if (!usingOAuth && !validator.isLength(password, 8)) {
|
||||||
model.get('errors').add('password', 'Password must be at least 8 characters long');
|
model.get('errors').add('password', 'Password must be at least 8 characters long');
|
||||||
this.invalidate();
|
this.invalidate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import NewUserValidator from 'ghost-admin/validators/new-user';
|
import NewUserValidator from 'ghost-admin/validators/new-user';
|
||||||
|
|
||||||
export default NewUserValidator.create({
|
export default NewUserValidator.create({
|
||||||
properties: ['name', 'email', 'password', 'blogTitle'],
|
properties: ['name', 'email', 'password', 'blogTitle', 'session'],
|
||||||
|
|
||||||
blogTitle(model) {
|
blogTitle(model) {
|
||||||
let blogTitle = model.get('blogTitle');
|
let blogTitle = model.get('blogTitle');
|
||||||
|
@ -10,5 +10,16 @@ export default NewUserValidator.create({
|
||||||
model.get('errors').add('blogTitle', 'Please enter a blog title.');
|
model.get('errors').add('blogTitle', 'Please enter a blog title.');
|
||||||
this.invalidate();
|
this.invalidate();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
session(model) {
|
||||||
|
let usingOAuth = model.get('config.ghostOAuth');
|
||||||
|
let isAuthenticated = model.get('session.isAuthenticated');
|
||||||
|
|
||||||
|
if (usingOAuth && !isAuthenticated) {
|
||||||
|
model.get('errors').add('session', 'Please connect a Ghost.org account before continuing');
|
||||||
|
model.get('hasValidated').pushObject('session');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,6 +35,10 @@ module.exports = function (environment) {
|
||||||
authenticationRoute: 'signin',
|
authenticationRoute: 'signin',
|
||||||
routeAfterAuthentication: 'posts',
|
routeAfterAuthentication: 'posts',
|
||||||
routeIfAlreadyAuthenticated: 'posts'
|
routeIfAlreadyAuthenticated: 'posts'
|
||||||
|
},
|
||||||
|
|
||||||
|
torii: {
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,7 @@
|
||||||
"moment-timezone": "0.5.5",
|
"moment-timezone": "0.5.5",
|
||||||
"password-generator": "2.0.2",
|
"password-generator": "2.0.2",
|
||||||
"top-gh-contribs": "2.0.4",
|
"top-gh-contribs": "2.0.4",
|
||||||
|
"torii": "0.8.0",
|
||||||
"walk-sync": "0.3.1"
|
"walk-sync": "0.3.1"
|
||||||
},
|
},
|
||||||
"ember-addon": {
|
"ember-addon": {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* jshint expr:true */
|
/* jshint expr:true */
|
||||||
|
import Ember from 'ember';
|
||||||
import {
|
import {
|
||||||
describe,
|
describe,
|
||||||
it,
|
it,
|
||||||
|
@ -10,6 +11,11 @@ import startApp from 'ghost-admin/tests/helpers/start-app';
|
||||||
import destroyApp from 'ghost-admin/tests/helpers/destroy-app';
|
import destroyApp from 'ghost-admin/tests/helpers/destroy-app';
|
||||||
import { invalidateSession, authenticateSession } from 'ghost-admin/tests/helpers/ember-simple-auth';
|
import { invalidateSession, authenticateSession } from 'ghost-admin/tests/helpers/ember-simple-auth';
|
||||||
import Mirage from 'ember-cli-mirage';
|
import Mirage from 'ember-cli-mirage';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import {
|
||||||
|
stubSuccessfulOAuthConnect,
|
||||||
|
stubFailedOAuthConnect
|
||||||
|
} from 'ghost-admin/tests/helpers/oauth';
|
||||||
|
|
||||||
describe('Acceptance: Setup', function () {
|
describe('Acceptance: Setup', function () {
|
||||||
let application;
|
let application;
|
||||||
|
@ -409,4 +415,121 @@ describe('Acceptance: Setup', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('using Ghost OAuth', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// mimic a new install
|
||||||
|
server.get('/authentication/setup/', function () {
|
||||||
|
return {
|
||||||
|
setup: [
|
||||||
|
{status: false}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// simulate active oauth config
|
||||||
|
$('head').append('<meta name="env-ghostAuthId" content="6e0704b3-c653-4c12-8da7-584232b5c629" />');
|
||||||
|
|
||||||
|
// ensure we have roles available
|
||||||
|
server.loadFixtures('roles');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
// ensure we don't leak OAuth config to other tests
|
||||||
|
$('meta[name="env-ghostAuthId"]').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the connect form and validates', function () {
|
||||||
|
invalidateSession(application);
|
||||||
|
|
||||||
|
visit('/setup');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
// it redirects to step one
|
||||||
|
expect(
|
||||||
|
currentURL(),
|
||||||
|
'url after accessing /setup'
|
||||||
|
).to.equal('/setup/one');
|
||||||
|
});
|
||||||
|
|
||||||
|
click('.btn-green');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('button.login').text().trim(),
|
||||||
|
'login button text'
|
||||||
|
).to.equal('Sign in with Ghost');
|
||||||
|
});
|
||||||
|
|
||||||
|
click('.btn-green');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
let sessionFG = find('button.login').closest('.form-group');
|
||||||
|
let titleFG = find('input[name="blog-title"]').closest('.form-group');
|
||||||
|
|
||||||
|
// session is validated
|
||||||
|
expect(
|
||||||
|
sessionFG.hasClass('error'),
|
||||||
|
'session form group has error class'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sessionFG.find('.response').text().trim(),
|
||||||
|
'session validation text'
|
||||||
|
).to.match(/Please connect a Ghost\.org account/i);
|
||||||
|
|
||||||
|
// blog title is validated
|
||||||
|
expect(
|
||||||
|
titleFG.hasClass('error'),
|
||||||
|
'title form group has error class'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
titleFG.find('.response').text().trim(),
|
||||||
|
'title validation text'
|
||||||
|
).to.match(/please enter a blog title/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: test that connecting clears session validation error
|
||||||
|
// TODO: test that typing in blog title clears validation error
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can connect and setup successfully', function () {
|
||||||
|
stubSuccessfulOAuthConnect(application);
|
||||||
|
|
||||||
|
visit('/setup/two');
|
||||||
|
click('button.login');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('button.login').text().trim(),
|
||||||
|
'login button text when connected'
|
||||||
|
).to.equal('Connected: oauthtest@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
fillIn('input[name="blog-title"]', 'Ghostbusters');
|
||||||
|
click('.btn-green');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
currentURL(),
|
||||||
|
'url after submitting'
|
||||||
|
).to.equal('/setup/three');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles failed connect', function () {
|
||||||
|
stubFailedOAuthConnect(application);
|
||||||
|
|
||||||
|
visit('/setup/two');
|
||||||
|
click('button.login');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.main-error').text().trim(),
|
||||||
|
'error text after failed oauth connect'
|
||||||
|
).to.match(/authentication with ghost\.org denied or failed/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,10 @@ import startApp from '../helpers/start-app';
|
||||||
import destroyApp from '../helpers/destroy-app';
|
import destroyApp from '../helpers/destroy-app';
|
||||||
import { invalidateSession, authenticateSession } from 'ghost-admin/tests/helpers/ember-simple-auth';
|
import { invalidateSession, authenticateSession } from 'ghost-admin/tests/helpers/ember-simple-auth';
|
||||||
import Mirage from 'ember-cli-mirage';
|
import Mirage from 'ember-cli-mirage';
|
||||||
|
import {
|
||||||
|
stubSuccessfulOAuthConnect,
|
||||||
|
stubFailedOAuthConnect
|
||||||
|
} from 'ghost-admin/tests/helpers/oauth';
|
||||||
|
|
||||||
describe('Acceptance: Signin', function() {
|
describe('Acceptance: Signin', function() {
|
||||||
let application;
|
let application;
|
||||||
|
@ -129,4 +133,54 @@ describe('Acceptance: Signin', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('using Ghost OAuth', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// simulate active oauth config
|
||||||
|
$('head').append('<meta name="env-ghostAuthId" content="6e0704b3-c653-4c12-8da7-584232b5c629" />');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
// ensure we don't leak OAuth config to other tests
|
||||||
|
$('meta[name="env-ghostAuthId"]').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can sign in successfully', function () {
|
||||||
|
server.loadFixtures('roles');
|
||||||
|
stubSuccessfulOAuthConnect(application);
|
||||||
|
|
||||||
|
visit('/signin');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(currentURL(), 'current url').to.equal('/signin');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('button.login').text().trim(),
|
||||||
|
'login button text'
|
||||||
|
).to.equal('Sign in with Ghost');
|
||||||
|
});
|
||||||
|
|
||||||
|
click('button.login');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(currentURL(), 'url after connect').to.equal('/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a failed connect', function () {
|
||||||
|
stubFailedOAuthConnect(application);
|
||||||
|
|
||||||
|
visit('/signin');
|
||||||
|
click('button.login');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(currentURL(), 'current url').to.equal('/signin');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.main-error').text().trim(),
|
||||||
|
'sign-in error'
|
||||||
|
).to.match(/Authentication with Ghost\.org denied or failed/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,10 @@ import { expect } from 'chai';
|
||||||
import startApp from '../helpers/start-app';
|
import startApp from '../helpers/start-app';
|
||||||
import destroyApp from '../helpers/destroy-app';
|
import destroyApp from '../helpers/destroy-app';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import {
|
||||||
|
stubSuccessfulOAuthConnect,
|
||||||
|
stubFailedOAuthConnect
|
||||||
|
} from 'ghost-admin/tests/helpers/oauth';
|
||||||
|
|
||||||
describe('Acceptance: Signup', function() {
|
describe('Acceptance: Signup', function() {
|
||||||
let application;
|
let application;
|
||||||
|
@ -24,6 +28,29 @@ describe('Acceptance: Signup', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can signup successfully', function() {
|
it('can signup successfully', function() {
|
||||||
|
server.get('/authentication/invitation', function (db, request) {
|
||||||
|
return {
|
||||||
|
invitation: [{valid: true}]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
server.post('/authentication/invitation/', function (db, request) {
|
||||||
|
let params = JSON.parse(request.requestBody);
|
||||||
|
expect(params.invitation[0].name).to.equal('Test User');
|
||||||
|
expect(params.invitation[0].email).to.equal('kevin+test2@ghost.org');
|
||||||
|
expect(params.invitation[0].password).to.equal('ValidPassword');
|
||||||
|
expect(params.invitation[0].token).to.equal('MTQ3MDM0NjAxNzkyOXxrZXZpbit0ZXN0MkBnaG9zdC5vcmd8MmNEblFjM2c3ZlFUajluTks0aUdQU0dmdm9ta0xkWGY2OEZ1V2dTNjZVZz0');
|
||||||
|
|
||||||
|
// ensure that `/users/me/` request returns a user
|
||||||
|
server.create('user', {email: 'kevin@test2@ghost.org'});
|
||||||
|
|
||||||
|
return {
|
||||||
|
invitation: [{
|
||||||
|
message: 'Invitation accepted.'
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// token details:
|
// token details:
|
||||||
// "1470346017929|kevin+test2@ghost.org|2cDnQc3g7fQTj9nNK4iGPSGfvomkLdXf68FuWgS66Ug="
|
// "1470346017929|kevin+test2@ghost.org|2cDnQc3g7fQTj9nNK4iGPSGfvomkLdXf68FuWgS66Ug="
|
||||||
visit('/signup/MTQ3MDM0NjAxNzkyOXxrZXZpbit0ZXN0MkBnaG9zdC5vcmd8MmNEblFjM2c3ZlFUajluTks0aUdQU0dmdm9ta0xkWGY2OEZ1V2dTNjZVZz0');
|
visit('/signup/MTQ3MDM0NjAxNzkyOXxrZXZpbit0ZXN0MkBnaG9zdC5vcmd8MmNEblFjM2c3ZlFUajluTks0aUdQU0dmdm9ta0xkWGY2OEZ1V2dTNjZVZz0');
|
||||||
|
@ -108,29 +135,6 @@ describe('Acceptance: Signup', function() {
|
||||||
// submitting sends correct details and redirects to content screen
|
// submitting sends correct details and redirects to content screen
|
||||||
click('.btn-green');
|
click('.btn-green');
|
||||||
|
|
||||||
server.get('/authentication/invitation', function (db, request) {
|
|
||||||
return {
|
|
||||||
invitation: [{valid: true}]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
server.post('/authentication/invitation/', function (db, request) {
|
|
||||||
let params = JSON.parse(request.requestBody);
|
|
||||||
expect(params.invitation[0].name).to.equal('Test User');
|
|
||||||
expect(params.invitation[0].email).to.equal('kevin+test2@ghost.org');
|
|
||||||
expect(params.invitation[0].password).to.equal('ValidPassword');
|
|
||||||
expect(params.invitation[0].token).to.equal('MTQ3MDM0NjAxNzkyOXxrZXZpbit0ZXN0MkBnaG9zdC5vcmd8MmNEblFjM2c3ZlFUajluTks0aUdQU0dmdm9ta0xkWGY2OEZ1V2dTNjZVZz0');
|
|
||||||
|
|
||||||
// ensure that `/users/me/` request returns a user
|
|
||||||
server.create('user', {email: 'kevin@test2@ghost.org'});
|
|
||||||
|
|
||||||
return {
|
|
||||||
invitation: [{
|
|
||||||
message: 'Invitation accepted.'
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
andThen(function () {
|
andThen(function () {
|
||||||
expect(currentPath()).to.equal('posts.index');
|
expect(currentPath()).to.equal('posts.index');
|
||||||
});
|
});
|
||||||
|
@ -139,4 +143,67 @@ describe('Acceptance: Signup', function() {
|
||||||
it('redirects if already logged in');
|
it('redirects if already logged in');
|
||||||
it('redirects with alert on invalid token');
|
it('redirects with alert on invalid token');
|
||||||
it('redirects with alert on non-existant or expired token');
|
it('redirects with alert on non-existant or expired token');
|
||||||
|
|
||||||
|
describe('using Ghost OAuth', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// simulate active oauth config
|
||||||
|
$('head').append('<meta name="env-ghostAuthId" content="6e0704b3-c653-4c12-8da7-584232b5c629" />');
|
||||||
|
|
||||||
|
let user = server.create('user', {name: 'Test Invite Creator'});
|
||||||
|
|
||||||
|
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||||
|
server.create('invite', {
|
||||||
|
email: 'kevin+test2@ghost.org',
|
||||||
|
created_by: user.id
|
||||||
|
});
|
||||||
|
/* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
// ensure we don't leak OAuth config to other tests
|
||||||
|
$('meta[name="env-ghostAuthId"]').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can sign up sucessfully', function () {
|
||||||
|
stubSuccessfulOAuthConnect(application);
|
||||||
|
|
||||||
|
// token details:
|
||||||
|
// "1470346017929|kevin+test2@ghost.org|2cDnQc3g7fQTj9nNK4iGPSGfvomkLdXf68FuWgS66Ug="
|
||||||
|
visit('/signup/MTQ3MDM0NjAxNzkyOXxrZXZpbit0ZXN0MkBnaG9zdC5vcmd8MmNEblFjM2c3ZlFUajluTks0aUdQU0dmdm9ta0xkWGY2OEZ1V2dTNjZVZz0');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(currentPath()).to.equal('signup');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.gh-flow-content header p').text().trim(),
|
||||||
|
'form header text'
|
||||||
|
).to.equal('Accept your invite from Test Invite Creator');
|
||||||
|
});
|
||||||
|
|
||||||
|
click('button.login');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(currentPath()).to.equal('posts.index');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles failed connect', function () {
|
||||||
|
stubFailedOAuthConnect(application);
|
||||||
|
|
||||||
|
// token details:
|
||||||
|
// "1470346017929|kevin+test2@ghost.org|2cDnQc3g7fQTj9nNK4iGPSGfvomkLdXf68FuWgS66Ug="
|
||||||
|
visit('/signup/MTQ3MDM0NjAxNzkyOXxrZXZpbit0ZXN0MkBnaG9zdC5vcmd8MmNEblFjM2c3ZlFUajluTks0aUdQU0dmdm9ta0xkWGY2OEZ1V2dTNjZVZz0');
|
||||||
|
|
||||||
|
click('button.login');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
expect(currentPath()).to.equal('signup');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.main-error').text().trim(),
|
||||||
|
'flow error text'
|
||||||
|
).to.match(/authentication with ghost\.org denied or failed/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -716,6 +716,44 @@ describe('Acceptance: Team', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('using Ghost OAuth', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// simulate active oauth config
|
||||||
|
$('head').append('<meta name="env-ghostAuthId" content="6e0704b3-c653-4c12-8da7-584232b5c629" />');
|
||||||
|
|
||||||
|
server.loadFixtures();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
// ensure we don't leak OAuth config to other tests
|
||||||
|
$('meta[name="env-ghostAuthId"]').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doesn\'t show the password reset form', function () {
|
||||||
|
visit(`/team/${admin.slug}`);
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
// ensure that the normal form is displayed so we don't get
|
||||||
|
// false positives
|
||||||
|
expect(
|
||||||
|
find('input#user-slug').length,
|
||||||
|
'profile form is displayed'
|
||||||
|
).to.equal(1);
|
||||||
|
|
||||||
|
// check that the password form is hidden
|
||||||
|
expect(
|
||||||
|
find('#password-reset').length,
|
||||||
|
'presence of password reset form'
|
||||||
|
).to.equal(0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('#user-password-new').length,
|
||||||
|
'presence of new password field'
|
||||||
|
).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('own user', function () {
|
describe('own user', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
server.loadFixtures();
|
server.loadFixtures();
|
||||||
|
|
39
tests/helpers/oauth.js
Normal file
39
tests/helpers/oauth.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import {faker} from 'ember-cli-mirage';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
|
|
||||||
|
let generateCode = function generateCode() {
|
||||||
|
return faker.internet.password(32, false, /[a-zA-Z0-9]/);
|
||||||
|
};
|
||||||
|
|
||||||
|
let generateSecret = function generateSecret() {
|
||||||
|
return faker.internet.password(12, false, /[a-f0-9]/);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stubSuccessfulOAuthConnect = function stubSuccessfulOAuthConnect(application) {
|
||||||
|
let provider = application.__container__.lookup('torii-provider:ghost-oauth2');
|
||||||
|
|
||||||
|
provider.open = function () {
|
||||||
|
return RSVP.Promise.resolve({
|
||||||
|
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||||
|
authorizationCode: generateCode(),
|
||||||
|
client_id: 'ghost-admin',
|
||||||
|
client_secret: generateSecret(),
|
||||||
|
provider: 'ghost-oauth2',
|
||||||
|
redirectUrl: 'http://localhost:2368/ghost/'
|
||||||
|
/* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const stubFailedOAuthConnect = function stubFailedOAuthConnect(application) {
|
||||||
|
let provider = application.__container__.lookup('torii-provider:ghost-oauth2');
|
||||||
|
|
||||||
|
provider.open = function () {
|
||||||
|
return RSVP.Promise.reject();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
stubSuccessfulOAuthConnect,
|
||||||
|
stubFailedOAuthConnect
|
||||||
|
};
|
|
@ -17,6 +17,7 @@
|
||||||
<meta name="env-routeKeywords" content="{"tag":"tag","author":"author","page":"page","preview":"p","private":"private"}" />
|
<meta name="env-routeKeywords" content="{"tag":"tag","author":"author","page":"page","preview":"p","private":"private"}" />
|
||||||
<meta name="env-clientId" content="ghost-admin" />
|
<meta name="env-clientId" content="ghost-admin" />
|
||||||
<meta name="env-clientSecret" content="5076dc643873" />
|
<meta name="env-clientSecret" content="5076dc643873" />
|
||||||
|
<!-- <meta name="env-ghostAuthId" content="6e0704b3-c653-4c12-8da7-584232b5c629" /> -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
||||||
<link rel="stylesheet" href="{{rootURL}}assets/ghost.css">
|
<link rel="stylesheet" href="{{rootURL}}assets/ghost.css">
|
||||||
|
|
|
@ -185,8 +185,6 @@ describeComponent(
|
||||||
return $(name).text().trim();
|
return $(name).text().trim();
|
||||||
}).toArray();
|
}).toArray();
|
||||||
|
|
||||||
console.log(packageNames);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
packageNames,
|
packageNames,
|
||||||
'themes are ordered by label, folder names shown for duplicates'
|
'themes are ordered by label, folder names shown for duplicates'
|
||||||
|
|
Loading…
Reference in a new issue