2017-08-22 10:53:26 +03:00
import Controller from '@ember/controller' ;
2017-05-29 21:50:03 +03:00
import boundOneWay from 'ghost-admin/utils/bound-one-way' ;
2021-03-24 21:14:46 +03:00
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard' ;
2017-05-29 21:50:03 +03:00
import isNumber from 'ghost-admin/utils/isNumber' ;
2019-01-22 16:09:38 +03:00
import validator from 'validator' ;
2017-11-10 17:19:20 +03:00
import windowProxy from 'ghost-admin/utils/window-proxy' ;
2017-08-22 10:53:26 +03:00
import { alias , and , not , or , readOnly } from '@ember/object/computed' ;
import { computed } from '@ember/object' ;
import { isArray as isEmberArray } from '@ember/array' ;
import { run } from '@ember/runloop' ;
2017-10-30 12:38:01 +03:00
import { inject as service } from '@ember/service' ;
2019-12-17 23:47:38 +03:00
import { task , taskGroup , timeout } from 'ember-concurrency' ;
2016-08-11 09:58:38 +03:00
2016-04-25 12:54:36 +03:00
export default Controller . extend ( {
2018-01-11 20:43:23 +03:00
ajax : service ( ) ,
config : service ( ) ,
dropdown : service ( ) ,
ghostPaths : service ( ) ,
notifications : service ( ) ,
session : service ( ) ,
slugGenerator : service ( ) ,
2020-10-27 16:07:01 +03:00
personalToken : null ,
personalTokenRegenerated : false ,
2017-10-31 18:27:25 +03:00
leaveSettingsTransition : null ,
dirtyAttributes : false ,
2015-11-18 13:50:48 +03:00
showDeleteUserModal : false ,
2017-03-08 21:21:35 +03:00
showSuspendUserModal : false ,
2015-11-18 13:50:48 +03:00
showTransferOwnerModal : false ,
showUploadCoverModal : false ,
showUplaodImageModal : false ,
2020-10-27 16:07:01 +03:00
showRegenerateTokenModal : false ,
2016-03-03 11:52:27 +03:00
_scratchFacebook : null ,
_scratchTwitter : null ,
2014-07-31 08:25:42 +04:00
2018-01-11 20:43:23 +03:00
saveHandlers : taskGroup ( ) . enqueue ( ) ,
2015-10-28 14:36:45 +03:00
user : alias ( 'model' ) ,
2015-11-18 13:50:48 +03:00
currentUser : alias ( 'session.user' ) ,
2014-07-01 19:58:26 +04:00
2018-01-11 01:57:43 +03:00
email : readOnly ( 'user.email' ) ,
slugValue : boundOneWay ( 'user.slug' ) ,
2014-07-02 07:44:39 +04:00
2017-09-04 22:17:04 +03:00
canChangeEmail : not ( 'isAdminUserOnOwnerProfile' ) ,
canChangePassword : not ( 'isAdminUserOnOwnerProfile' ) ,
2019-03-04 19:45:16 +03:00
canMakeOwner : and ( 'currentUser.isOwner' , 'isNotOwnProfile' , 'user.isAdmin' , 'isNotSuspended' ) ,
2017-09-04 22:17:04 +03:00
isAdminUserOnOwnerProfile : and ( 'currentUser.isAdmin' , 'user.isOwner' ) ,
isNotOwnersProfile : not ( 'user.isOwner' ) ,
2019-03-04 19:45:16 +03:00
isNotSuspended : not ( 'user.isSuspended' ) ,
2019-06-24 18:33:21 +03:00
rolesDropdownIsVisible : and ( 'currentUser.isOwnerOrAdmin' , 'isNotOwnProfile' , 'isNotOwnersProfile' ) ,
2015-11-18 13:50:48 +03:00
userActionsAreVisible : or ( 'deleteUserActionIsVisible' , 'canMakeOwner' ) ,
2018-01-11 20:43:23 +03:00
isNotOwnProfile : not ( 'isOwnProfile' ) ,
2017-03-03 16:24:43 +03:00
isOwnProfile : computed ( 'user.id' , 'currentUser.id' , function ( ) {
return this . get ( 'user.id' ) === this . get ( 'currentUser.id' ) ;
} ) ,
2015-05-26 05:10:50 +03:00
2019-06-24 18:33:21 +03:00
deleteUserActionIsVisible : computed ( 'currentUser.{isOwnerOrAdmin,isEditor}' , 'user.{isOwner,isAuthorOrContributor}' , 'isOwnProfile' , function ( ) {
// users can't delete themselves
if ( this . isOwnProfile ) {
return false ;
}
if (
// owners/admins can delete any non-owner user
( this . currentUser . get ( 'isOwnerOrAdmin' ) && ! this . user . isOwner ) ||
// editors can delete any author or contributor
( this . currentUser . get ( 'isEditor' ) && this . user . isAuthorOrContributor )
) {
2015-06-13 17:34:09 +03:00
return true ;
}
2019-06-24 18:33:21 +03:00
return false ;
2014-07-30 05:57:19 +04:00
} ) ,
2014-03-23 06:31:45 +04:00
2015-10-28 14:36:45 +03:00
coverTitle : computed ( 'user.name' , function ( ) {
return ` ${ this . get ( 'user.name' ) } 's Cover Image ` ;
2014-07-30 05:57:19 +04:00
} ) ,
2014-07-31 08:25:42 +04:00
2015-10-28 14:36:45 +03:00
roles : computed ( function ( ) {
2015-09-03 14:06:50 +03:00
return this . store . query ( 'role' , { permissions : 'assign' } ) ;
2015-07-09 20:10:00 +03:00
} ) ,
2016-08-11 09:58:38 +03:00
actions : {
changeRole ( newRole ) {
2019-03-06 16:53:54 +03:00
this . user . set ( 'role' , newRole ) ;
2017-10-31 18:27:25 +03:00
this . set ( 'dirtyAttributes' , true ) ;
2014-03-23 06:31:45 +04:00
} ,
2015-11-18 13:50:48 +03:00
toggleDeleteUserModal ( ) {
2019-03-06 16:53:54 +03:00
if ( this . deleteUserActionIsVisible ) {
2015-11-18 13:50:48 +03:00
this . toggleProperty ( 'showDeleteUserModal' ) ;
}
} ,
2017-03-08 21:21:35 +03:00
suspendUser ( ) {
2019-03-06 16:53:54 +03:00
this . user . set ( 'status' , 'inactive' ) ;
return this . save . perform ( ) ;
2017-03-08 21:21:35 +03:00
} ,
toggleSuspendUserModal ( ) {
2019-03-06 16:53:54 +03:00
if ( this . deleteUserActionIsVisible ) {
2017-03-08 21:21:35 +03:00
this . toggleProperty ( 'showSuspendUserModal' ) ;
}
} ,
unsuspendUser ( ) {
2019-03-06 16:53:54 +03:00
this . user . set ( 'status' , 'active' ) ;
return this . save . perform ( ) ;
2017-03-08 21:21:35 +03:00
} ,
toggleUnsuspendUserModal ( ) {
2019-03-06 16:53:54 +03:00
if ( this . deleteUserActionIsVisible ) {
2017-03-08 21:21:35 +03:00
this . toggleProperty ( 'showUnsuspendUserModal' ) ;
}
} ,
2016-03-03 11:52:27 +03:00
validateFacebookUrl ( ) {
2019-03-06 16:53:54 +03:00
let newUrl = this . _scratchFacebook ;
2016-03-03 11:52:27 +03:00
let oldUrl = this . get ( 'user.facebook' ) ;
let errMessage = '' ;
2017-11-10 19:38:30 +03:00
// reset errors and validation
this . get ( 'user.errors' ) . remove ( 'facebook' ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'facebook' ) ;
2016-05-17 21:14:14 +03:00
if ( newUrl === '' ) {
2016-03-03 11:52:27 +03:00
// Clear out the Facebook url
this . set ( 'user.facebook' , '' ) ;
return ;
}
2016-05-17 21:14:14 +03:00
// _scratchFacebook will be null unless the user has input something
if ( ! newUrl ) {
newUrl = oldUrl ;
}
2017-11-10 19:38:30 +03:00
try {
// strip any facebook URLs out
newUrl = newUrl . replace ( /(https?:\/\/)?(www\.)?facebook\.com/i , '' ) ;
2016-03-03 11:52:27 +03:00
2017-11-10 19:38:30 +03:00
// don't allow any non-facebook urls
if ( newUrl . match ( /^(http|\/\/)/i ) ) {
throw 'invalid url' ;
2016-05-16 21:16:40 +03:00
}
2016-03-03 11:52:27 +03:00
2017-11-10 19:38:30 +03:00
// strip leading / if we have one then concat to full facebook URL
newUrl = newUrl . replace ( /^\// , '' ) ;
newUrl = ` https://www.facebook.com/ ${ newUrl } ` ;
2016-03-03 11:52:27 +03:00
2017-11-10 19:38:30 +03:00
// don't allow URL if it's not valid
if ( ! validator . isURL ( newUrl ) ) {
throw 'invalid url' ;
2016-03-03 11:52:27 +03:00
}
2016-05-16 21:16:40 +03:00
2017-10-31 18:27:25 +03:00
this . set ( 'user.facebook' , '' ) ;
run . schedule ( 'afterRender' , this , function ( ) {
this . set ( 'user.facebook' , newUrl ) ;
2016-05-16 21:16:40 +03:00
} ) ;
2017-11-10 19:38:30 +03:00
} catch ( e ) {
if ( e === 'invalid url' ) {
errMessage = 'The URL must be in a format like '
+ 'https://www.facebook.com/yourPage' ;
this . get ( 'user.errors' ) . add ( 'facebook' , errMessage ) ;
return ;
}
throw e ;
} finally {
2016-05-16 21:16:40 +03:00
this . get ( 'user.hasValidated' ) . pushObject ( 'facebook' ) ;
2016-03-03 11:52:27 +03:00
}
} ,
validateTwitterUrl ( ) {
2019-03-06 16:53:54 +03:00
let newUrl = this . _scratchTwitter ;
2016-03-03 11:52:27 +03:00
let oldUrl = this . get ( 'user.twitter' ) ;
let errMessage = '' ;
2017-11-10 19:38:30 +03:00
// reset errors and validation
this . get ( 'user.errors' ) . remove ( 'twitter' ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'twitter' ) ;
2016-05-17 21:14:14 +03:00
if ( newUrl === '' ) {
2016-03-03 11:52:27 +03:00
// Clear out the Twitter url
this . set ( 'user.twitter' , '' ) ;
return ;
}
2016-05-17 21:14:14 +03:00
// _scratchTwitter will be null unless the user has input something
if ( ! newUrl ) {
newUrl = oldUrl ;
}
2017-09-11 10:56:11 +03:00
if ( newUrl . match ( /(?:twitter\.com\/)(\S+)/ ) || newUrl . match ( /([a-z\d.]+)/i ) ) {
2016-05-16 21:16:40 +03:00
let username = [ ] ;
2016-03-03 11:52:27 +03:00
2016-05-16 21:16:40 +03:00
if ( newUrl . match ( /(?:twitter\.com\/)(\S+)/ ) ) {
2016-11-14 16:16:51 +03:00
[ , username ] = newUrl . match ( /(?:twitter\.com\/)(\S+)/ ) ;
2016-05-16 21:16:40 +03:00
} else {
[ username ] = newUrl . match ( /([^/]+)\/?$/mi ) ;
}
2016-03-03 11:52:27 +03:00
2016-05-16 21:16:40 +03:00
// check if username starts with http or www and show error if so
2017-09-11 10:56:11 +03:00
if ( username . match ( /^(http|www)|(\/)/ ) || ! username . match ( /^[a-z\d._]{1,15}$/mi ) ) {
errMessage = ! username . match ( /^[a-z\d._]{1,15}$/mi ) ? 'Your Username is not a valid Twitter Username' : 'The URL must be in a format like https://twitter.com/yourUsername' ;
2016-03-03 11:52:27 +03:00
this . get ( 'user.errors' ) . add ( 'twitter' , errMessage ) ;
this . get ( 'user.hasValidated' ) . pushObject ( 'twitter' ) ;
return ;
}
2016-05-16 21:16:40 +03:00
newUrl = ` https://twitter.com/ ${ username } ` ;
this . get ( 'user.hasValidated' ) . pushObject ( 'twitter' ) ;
2017-10-31 18:27:25 +03:00
this . set ( 'user.twitter' , '' ) ;
run . schedule ( 'afterRender' , this , function ( ) {
this . set ( 'user.twitter' , newUrl ) ;
2016-05-16 21:16:40 +03:00
} ) ;
} else {
2016-11-14 16:16:51 +03:00
errMessage = 'The URL must be in a format like '
+ 'https://twitter.com/yourUsername' ;
2016-05-16 21:16:40 +03:00
this . get ( 'user.errors' ) . add ( 'twitter' , errMessage ) ;
this . get ( 'user.hasValidated' ) . pushObject ( 'twitter' ) ;
return ;
2016-03-03 11:52:27 +03:00
}
} ,
2015-11-18 13:50:48 +03:00
transferOwnership ( ) {
2019-03-06 16:53:54 +03:00
let user = this . user ;
2015-11-18 13:50:48 +03:00
let url = this . get ( 'ghostPaths.url' ) . api ( 'users' , 'owner' ) ;
2019-03-06 16:53:54 +03:00
this . dropdown . closeDropdowns ( ) ;
2015-11-18 13:50:48 +03:00
2019-03-06 16:53:54 +03:00
return this . ajax . put ( url , {
2015-11-18 13:50:48 +03:00
data : {
owner : [ {
id : user . get ( 'id' )
} ]
}
} ) . then ( ( response ) => {
// manually update the roles for the users that just changed roles
// because store.pushPayload is not working with embedded relations
2016-07-06 22:47:30 +03:00
if ( response && isEmberArray ( response . users ) ) {
2015-11-18 13:50:48 +03:00
response . users . forEach ( ( userJSON ) => {
2020-06-17 11:35:46 +03:00
let updatedUser = this . store . peekRecord ( 'user' , userJSON . id ) ;
2015-11-18 13:50:48 +03:00
let role = this . store . peekRecord ( 'role' , userJSON . roles [ 0 ] . id ) ;
2020-06-17 11:35:46 +03:00
updatedUser . set ( 'role' , role ) ;
2015-11-18 13:50:48 +03:00
} ) ;
}
2019-03-06 16:53:54 +03:00
this . notifications . showAlert ( ` Ownership successfully transferred to ${ user . get ( 'name' ) } ` , { type : 'success' , key : 'owner.transfer.success' } ) ;
2015-11-18 13:50:48 +03:00
} ) . catch ( ( error ) => {
2019-03-06 16:53:54 +03:00
this . notifications . showAPIError ( error , { key : 'owner.transfer' } ) ;
2015-11-18 13:50:48 +03:00
} ) ;
} ,
2017-10-31 18:27:25 +03:00
toggleLeaveSettingsModal ( transition ) {
2019-03-06 16:53:54 +03:00
let leaveTransition = this . leaveSettingsTransition ;
2017-10-31 18:27:25 +03:00
2019-03-06 16:53:54 +03:00
if ( ! transition && this . showLeaveSettingsModal ) {
2017-10-31 18:27:25 +03:00
this . set ( 'leaveSettingsTransition' , null ) ;
this . set ( 'showLeaveSettingsModal' , false ) ;
return ;
}
if ( ! leaveTransition || transition . targetName === leaveTransition . targetName ) {
this . set ( 'leaveSettingsTransition' , transition ) ;
// if a save is running, wait for it to finish then transition
if ( this . get ( 'saveHandlers.isRunning' ) ) {
return this . get ( 'saveHandlers.last' ) . then ( ( ) => {
transition . retry ( ) ;
} ) ;
}
// we genuinely have unsaved data, show the modal
this . set ( 'showLeaveSettingsModal' , true ) ;
}
} ,
leaveSettings ( ) {
2019-03-06 16:53:54 +03:00
let transition = this . leaveSettingsTransition ;
let user = this . user ;
2017-10-31 18:27:25 +03:00
if ( ! transition ) {
2019-03-06 16:53:54 +03:00
this . notifications . showAlert ( 'Sorry, there was an error in the application. Please let the Ghost team know what happened.' , { type : 'error' } ) ;
2017-10-31 18:27:25 +03:00
return ;
}
2018-01-11 01:57:43 +03:00
// roll back changes on user props
2017-10-31 18:27:25 +03:00
user . rollbackAttributes ( ) ;
// roll back the slugValue property
2019-03-06 16:53:54 +03:00
if ( this . dirtyAttributes ) {
2017-10-31 18:27:25 +03:00
this . set ( 'slugValue' , user . get ( 'slug' ) ) ;
this . set ( 'dirtyAttributes' , false ) ;
}
return transition . retry ( ) ;
} ,
2015-11-18 13:50:48 +03:00
toggleTransferOwnerModal ( ) {
2019-03-06 16:53:54 +03:00
if ( this . canMakeOwner ) {
2015-11-18 13:50:48 +03:00
this . toggleProperty ( 'showTransferOwnerModal' ) ;
}
} ,
toggleUploadCoverModal ( ) {
this . toggleProperty ( 'showUploadCoverModal' ) ;
} ,
toggleUploadImageModal ( ) {
this . toggleProperty ( 'showUploadImageModal' ) ;
2016-06-14 14:46:24 +03:00
} ,
// TODO: remove those mutation actions once we have better
// inline validations that auto-clear errors on input
updatePassword ( password ) {
this . set ( 'user.password' , password ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'password' ) ;
this . get ( 'user.errors' ) . remove ( 'password' ) ;
} ,
updateNewPassword ( password ) {
this . set ( 'user.newPassword' , password ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'newPassword' ) ;
this . get ( 'user.errors' ) . remove ( 'newPassword' ) ;
} ,
updateNe2Password ( password ) {
this . set ( 'user.ne2Password' , password ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'ne2Password' ) ;
this . get ( 'user.errors' ) . remove ( 'ne2Password' ) ;
2020-10-27 16:07:01 +03:00
} ,
confirmRegenerateTokenModal ( ) {
this . set ( 'showRegenerateTokenModal' , true ) ;
} ,
cancelRegenerateTokenModal ( ) {
this . set ( 'showRegenerateTokenModal' , false ) ;
} ,
regenerateToken ( ) {
let url = this . get ( 'ghostPaths.url' ) . api ( 'users' , 'me' , 'token' ) ;
return this . ajax . put ( url , { data : { } } ) . then ( ( { apiKey } ) => {
this . set ( 'personalToken' , apiKey . id + ':' + apiKey . secret ) ;
this . set ( 'personalTokenRegenerated' , true ) ;
} ) . catch ( ( error ) => {
this . notifications . showAPIError ( error , { key : 'token.regenerate' } ) ;
} ) ;
2014-03-23 06:31:45 +04:00
}
2018-01-11 20:43:23 +03:00
} ,
2019-12-17 08:12:45 +03:00
_exportDb ( filename ) {
2019-12-16 16:33:20 +03:00
let exportUrl = this . get ( 'ghostPaths.url' ) . api ( 'db' ) ;
let downloadURL = ` ${ exportUrl } ?filename= ${ filename } ` ;
let iframe = document . getElementById ( 'iframeDownload' ) ;
if ( ! iframe ) {
iframe = document . createElement ( 'iframe' ) ;
iframe . id = 'iframeDownload' ;
iframe . style . display = 'none' ;
document . body . append ( iframe ) ;
}
iframe . setAttribute ( 'src' , downloadURL ) ;
} ,
2019-12-17 08:12:45 +03:00
deleteUser : task ( function * ( ) {
try {
const result = yield this . user . destroyRecord ( ) ;
2019-12-17 23:47:38 +03:00
if ( result . _meta && result . _meta . filename ) {
this . _exportDb ( result . _meta . filename ) ;
// give the iframe some time to trigger the download before
// it's removed from the dom when transitioning
yield timeout ( 300 ) ;
2019-12-17 08:12:45 +03:00
}
2019-12-17 23:47:38 +03:00
this . notifications . closeAlerts ( 'user.delete' ) ;
this . store . unloadAll ( 'post' ) ;
this . transitionToRoute ( 'staff' ) ;
} catch ( error ) {
this . notifications . showAlert ( 'The user could not be deleted. Please try again.' , { type : 'error' , key : 'user.delete.failed' } ) ;
throw error ;
2019-12-17 08:12:45 +03:00
}
} ) ,
2018-01-11 20:43:23 +03:00
updateSlug : task ( function * ( newSlug ) {
let slug = this . get ( 'user.slug' ) ;
newSlug = newSlug || slug ;
newSlug = newSlug . trim ( ) ;
// Ignore unchanged slugs or candidate slugs that are empty
if ( ! newSlug || slug === newSlug ) {
this . set ( 'slugValue' , slug ) ;
return true ;
}
2019-03-06 16:53:54 +03:00
let serverSlug = yield this . slugGenerator . generateSlug ( 'user' , newSlug ) ;
2018-01-11 20:43:23 +03:00
// If after getting the sanitized and unique slug back from the API
// we end up with a slug that matches the existing slug, abort the change
if ( serverSlug === slug ) {
return true ;
}
// Because the server transforms the candidate slug by stripping
// certain characters and appending a number onto the end of slugs
// to enforce uniqueness, there are cases where we can get back a
// candidate slug that is a duplicate of the original except for
// the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2)
// get the last token out of the slug candidate and see if it's a number
let slugTokens = serverSlug . split ( '-' ) ;
let check = Number ( slugTokens . pop ( ) ) ;
// if the candidate slug is the same as the existing slug except
// for the incrementor then the existing slug should be used
if ( isNumber ( check ) && check > 0 ) {
if ( slug === slugTokens . join ( '-' ) && serverSlug !== newSlug ) {
this . set ( 'slugValue' , slug ) ;
return true ;
}
}
this . set ( 'slugValue' , serverSlug ) ;
this . set ( 'dirtyAttributes' , true ) ;
return true ;
} ) . group ( 'saveHandlers' ) ,
save : task ( function * ( ) {
2019-03-06 16:53:54 +03:00
let user = this . user ;
let slugValue = this . slugValue ;
2018-01-11 20:43:23 +03:00
let slugChanged ;
if ( user . get ( 'slug' ) !== slugValue ) {
slugChanged = true ;
user . set ( 'slug' , slugValue ) ;
}
try {
user = yield user . save ( { format : false } ) ;
// If the user's slug has changed, change the URL and replace
// the history so refresh and back button still work
if ( slugChanged ) {
2018-04-30 14:29:43 +03:00
let currentPath = window . location . hash ;
2018-01-11 20:43:23 +03:00
2018-04-30 14:29:43 +03:00
let newPath = currentPath . split ( '/' ) ;
2018-01-11 20:43:23 +03:00
newPath [ newPath . length - 1 ] = user . get ( 'slug' ) ;
newPath = newPath . join ( '/' ) ;
windowProxy . replaceState ( { path : newPath } , '' , newPath ) ;
}
this . set ( 'dirtyAttributes' , false ) ;
2019-03-06 16:53:54 +03:00
this . notifications . closeAlerts ( 'user.update' ) ;
2018-01-11 20:43:23 +03:00
return user ;
} catch ( error ) {
// validation engine returns undefined so we have to check
// before treating the failure as an API error
if ( error ) {
2019-03-06 16:53:54 +03:00
this . notifications . showAPIError ( error , { key : 'user.update' } ) ;
2018-01-11 20:43:23 +03:00
}
}
2021-03-24 21:14:46 +03:00
} ) . group ( 'saveHandlers' ) ,
copyContentKey : task ( function * ( ) {
copyTextToClipboard ( this . personalToken ) ;
yield timeout ( this . isTesting ? 50 : 3000 ) ;
} )
2014-03-23 06:31:45 +04:00
} ) ;