admin-auth.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. <template lang='pug'>
  2. v-container(fluid, grid-list-lg)
  3. v-layout(row, wrap)
  4. v-flex(xs12)
  5. .admin-header
  6. img.animated.fadeInUp(src='/_assets/svg/icon-unlock.svg', alt='Authentication', style='width: 80px;')
  7. .admin-header-title
  8. .headline.primary--text.animated.fadeInLeft {{ $t('admin:auth.title') }}
  9. .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:auth.subtitle') }}
  10. v-spacer
  11. v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/auth', target='_blank')
  12. v-icon mdi-help-circle
  13. v-btn.animated.fadeInDown.wait-p2s.mx-3(icon, outlined, color='grey', @click='refresh')
  14. v-icon mdi-refresh
  15. v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
  16. v-icon(left) mdi-check
  17. span {{$t('common:actions.apply')}}
  18. v-flex(lg3, xs12)
  19. v-card.animated.fadeInUp
  20. v-toolbar(flat, color='teal', dark, dense)
  21. .subtitle-1 {{$t('admin:auth.activeStrategies')}}
  22. v-list(two-line, dense).py-0
  23. draggable(
  24. v-model='activeStrategies'
  25. handle='.is-handle'
  26. direction='vertical'
  27. :store='order'
  28. )
  29. transition-group
  30. v-list-item(
  31. v-for='(str, idx) in activeStrategies'
  32. :key='str.key'
  33. @click='selectedStrategy = str.key'
  34. :class='selectedStrategy === str.key ? ($vuetify.theme.dark ? `grey darken-5` : `teal lighten-5`) : ``'
  35. )
  36. v-list-item-avatar.is-handle(size='24')
  37. v-icon(:color='selectedStrategy === str.key ? `teal` : `grey`') mdi-drag-horizontal
  38. v-list-item-content
  39. v-list-item-title.body-2(:class='selectedStrategy === str.key ? `teal--text` : ``') {{ str.displayName }}
  40. v-list-item-subtitle: .caption(:class='selectedStrategy === str.key ? `teal--text ` : ``') {{ str.strategy.title }}
  41. v-list-item-avatar(v-if='selectedStrategy === str.key', size='24')
  42. v-icon.animated.fadeInLeft(color='teal', large) mdi-chevron-right
  43. v-card-chin
  44. v-menu(offset-y, bottom, min-width='250px', max-width='550px', max-height='50vh', style='flex: 1 1;', center)
  45. template(v-slot:activator='{ on }')
  46. v-btn(v-on='on', color='primary', depressed, block)
  47. v-icon(left) mdi-plus
  48. span {{$t('admin:auth.addStrategy')}}
  49. v-list(dense)
  50. template(v-for='(str, idx) of strategies')
  51. v-list-item(
  52. :key='str.key'
  53. :disabled='str.isDisabled'
  54. @click='addStrategy(str)'
  55. )
  56. v-list-item-avatar(height='24', width='48', tile)
  57. v-img(:src='str.logo', width='48px', height='24px', contain, :style='str.isDisabled ? `opacity: .25;` : ``')
  58. v-list-item-content
  59. v-list-item-title {{str.title}}
  60. v-list-item-subtitle: .caption(:style='str.isDisabled ? `opacity: .4;` : ``') {{str.description}}
  61. v-divider(v-if='idx < strategies.length - 1')
  62. v-flex(xs12, lg9)
  63. v-card.animated.fadeInUp.wait-p2s
  64. v-toolbar(color='primary', dense, flat, dark)
  65. .subtitle-1 {{strategy.displayName}} #[em ({{strategy.strategy.title}})]
  66. v-spacer
  67. v-btn(small, outlined, dark, color='white', :disabled='strategy.key === `local`', @click='deleteStrategy()')
  68. v-icon(left) mdi-close
  69. span {{$t('common:actions.delete')}}
  70. v-card-info(color='blue')
  71. div
  72. span {{strategy.strategy.description}}
  73. .caption: a(:href='strategy.strategy.website') {{strategy.strategy.website}}
  74. v-spacer
  75. .admin-providerlogo
  76. img(:src='strategy.strategy.logo', :alt='strategy.strategy.title')
  77. v-card-text
  78. .row
  79. .col-8
  80. v-text-field(
  81. outlined
  82. :label='$t(`admin:auth.displayName`)'
  83. v-model='strategy.displayName'
  84. prepend-icon='mdi-format-title'
  85. :hint='$t(`admin:auth.displayNameHint`)'
  86. persistent-hint
  87. )
  88. .col-4
  89. v-switch.mt-1(
  90. :label='$t(`admin:auth.strategyIsEnabled`)'
  91. v-model='strategy.isEnabled'
  92. color='primary'
  93. prepend-icon='mdi-power'
  94. :hint='$t(`admin:auth.strategyIsEnabledHint`)'
  95. persistent-hint
  96. inset
  97. :disabled='strategy.key === `local`'
  98. )
  99. template(v-if='strategy.config && Object.keys(strategy.config).length > 0')
  100. v-divider
  101. .overline.my-5 {{$t('admin:auth.strategyConfiguration')}}
  102. .pr-3
  103. template(v-for='cfg in strategy.config')
  104. v-select.mb-3(
  105. v-if='cfg.value.type === "string" && cfg.value.enum'
  106. outlined
  107. :items='cfg.value.enum'
  108. :key='cfg.key'
  109. :label='cfg.value.title'
  110. v-model='cfg.value.value'
  111. prepend-icon='mdi-cog-box'
  112. :hint='cfg.value.hint ? cfg.value.hint : ""'
  113. persistent-hint
  114. :class='cfg.value.hint ? "mb-2" : ""'
  115. :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
  116. )
  117. v-switch.mb-6(
  118. v-else-if='cfg.value.type === "boolean"'
  119. :key='cfg.key'
  120. :label='cfg.value.title'
  121. v-model='cfg.value.value'
  122. color='primary'
  123. prepend-icon='mdi-cog-box'
  124. :hint='cfg.value.hint ? cfg.value.hint : ""'
  125. persistent-hint
  126. inset
  127. )
  128. v-textarea.mb-3(
  129. v-else-if='cfg.value.type === "string" && cfg.value.multiline'
  130. outlined
  131. :key='cfg.key'
  132. :label='cfg.value.title'
  133. v-model='cfg.value.value'
  134. prepend-icon='mdi-cog-box'
  135. :hint='cfg.value.hint ? cfg.value.hint : ""'
  136. persistent-hint
  137. :class='cfg.value.hint ? "mb-2" : ""'
  138. )
  139. v-text-field.mb-3(
  140. v-else
  141. outlined
  142. :key='cfg.key'
  143. :label='cfg.value.title'
  144. v-model='cfg.value.value'
  145. prepend-icon='mdi-cog-box'
  146. :hint='cfg.value.hint ? cfg.value.hint : ""'
  147. persistent-hint
  148. :class='cfg.value.hint ? "mb-2" : ""'
  149. :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
  150. )
  151. v-divider
  152. .overline.my-5 {{$t('admin:auth.registration')}}
  153. .pr-3
  154. v-switch.ml-3(
  155. v-model='strategy.selfRegistration'
  156. :label='$t(`admin:auth.selfRegistration`)'
  157. color='primary'
  158. :hint='$t(`admin:auth.selfRegistrationHint`)'
  159. persistent-hint
  160. inset
  161. )
  162. v-combobox.ml-3.mt-5(
  163. :label='$t(`admin:auth.domainsWhitelist`)'
  164. v-model='strategy.domainWhitelist'
  165. prepend-icon='mdi-email-check-outline'
  166. outlined
  167. :disabled='!strategy.selfRegistration'
  168. :hint='$t(`admin:auth.domainsWhitelistHint`)'
  169. persistent-hint
  170. small-chips
  171. deletable-chips
  172. clearable
  173. multiple
  174. chips
  175. )
  176. v-autocomplete.mt-3.ml-3(
  177. outlined
  178. :disabled='!strategy.selfRegistration'
  179. :items='groups'
  180. item-text='name'
  181. item-value='id'
  182. :label='$t(`admin:auth.autoEnrollGroups`)'
  183. v-model='strategy.autoEnrollGroups'
  184. prepend-icon='mdi-account-group'
  185. :hint='$t(`admin:auth.autoEnrollGroupsHint`)'
  186. small-chips
  187. persistent-hint
  188. deletable-chips
  189. clearable
  190. multiple
  191. chips
  192. )
  193. v-card.mt-4.wiki-form.animated.fadeInUp.wait-p4s(v-if='selectedStrategy !== `local`')
  194. v-toolbar(color='primary', dense, flat, dark)
  195. .subtitle-1 {{$t('admin:auth.configReference')}}
  196. v-card-text
  197. .body-2 {{$t('admin:auth.configReferenceSubtitle')}}
  198. v-alert.mt-3.radius-7(v-if='host.length < 8', color='red', outlined, :value='true', icon='mdi-alert')
  199. i18next(path='admin:auth.siteUrlNotSetup', tag='span')
  200. strong(place='siteUrl') {{$t('admin:general.siteUrl')}}
  201. strong(place='general') {{$t('admin:general.title')}}
  202. .pa-3.mt-3.radius-7.grey(v-else, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`')
  203. .body-2: strong {{$t('admin:auth.allowedWebOrigins')}}
  204. .body-2 {{host}}
  205. v-divider.my-3
  206. .body-2: strong {{$t('admin:auth.callbackUrl')}}
  207. .body-2 {{host}}/login/{{strategy.key}}/callback
  208. v-divider.my-3
  209. .body-2: strong {{$t('admin:auth.loginUrl')}}
  210. .body-2 {{host}}/login
  211. v-divider.my-3
  212. .body-2: strong {{$t('admin:auth.logoutUrl')}}
  213. .body-2 {{host}}
  214. v-divider.my-3
  215. .body-2: strong {{$t('admin:auth.tokenEndpointAuthMethod')}}
  216. .body-2 HTTP-POST
  217. </template>
  218. <script>
  219. import _ from 'lodash'
  220. import gql from 'graphql-tag'
  221. import { v4 as uuid } from 'uuid'
  222. import groupsQuery from 'gql/admin/auth/auth-query-groups.gql'
  223. import hostQuery from 'gql/admin/auth/auth-query-host.gql'
  224. import draggable from 'vuedraggable'
  225. export default {
  226. components: {
  227. draggable
  228. },
  229. filters: {
  230. startCase(val) { return _.startCase(val) }
  231. },
  232. data() {
  233. return {
  234. groups: [],
  235. strategies: [],
  236. activeStrategies: [],
  237. selectedStrategy: '',
  238. host: '',
  239. strategy: {
  240. strategy: {}
  241. }
  242. }
  243. },
  244. computed: {
  245. order: {
  246. get () {
  247. return this.strategies
  248. },
  249. set (val) {
  250. }
  251. }
  252. },
  253. watch: {
  254. selectedStrategy(newValue, oldValue) {
  255. this.strategy = _.find(this.activeStrategies, ['key', newValue]) || {}
  256. },
  257. activeStrategies(newValue, oldValue) {
  258. this.selectedStrategy = 'local'
  259. }
  260. },
  261. methods: {
  262. async refresh() {
  263. await this.$apollo.queries.strategies.refetch()
  264. await this.$apollo.queries.activeStrategies.refetch()
  265. this.$store.commit('showNotification', {
  266. message: this.$t('admin:auth.refreshSuccess'),
  267. style: 'success',
  268. icon: 'cached'
  269. })
  270. },
  271. addStrategy (str) {
  272. const newStr = {
  273. key: uuid(),
  274. strategy: str,
  275. config: str.props.map(c => ({
  276. key: c.key,
  277. value: {
  278. ...c,
  279. value: c.default
  280. }
  281. })),
  282. order: this.activeStrategies.length,
  283. isEnabled: true,
  284. displayName: str.title,
  285. selfRegistration: false,
  286. domainWhitelist: [],
  287. autoEnrollGroups: []
  288. }
  289. this.activeStrategies = [...this.activeStrategies, newStr]
  290. this.$nextTick(() => {
  291. this.selectedStrategy = newStr.key
  292. })
  293. },
  294. deleteStrategy () {
  295. this.activeStrategies = _.reject(this.activeStrategies, ['key', this.strategy.key])
  296. },
  297. async save() {
  298. this.$store.commit(`loadingStart`, 'admin-auth-savestrategies')
  299. try {
  300. const resp = await this.$apollo.mutate({
  301. mutation: gql`
  302. mutation($strategies: [AuthenticationStrategyInput]!) {
  303. authentication {
  304. updateStrategies(strategies: $strategies) {
  305. responseResult {
  306. succeeded
  307. errorCode
  308. slug
  309. message
  310. }
  311. }
  312. }
  313. }
  314. `,
  315. variables: {
  316. strategies: this.activeStrategies.map(str => ({
  317. key: str.key,
  318. strategyKey: str.strategy.key,
  319. displayName: str.displayName,
  320. order: str.order,
  321. isEnabled: str.isEnabled,
  322. config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })})),
  323. selfRegistration: str.selfRegistration,
  324. domainWhitelist: str.domainWhitelist,
  325. autoEnrollGroups: str.autoEnrollGroups
  326. }))
  327. }
  328. })
  329. if (_.get(resp, 'data.authentication.updateStrategies.responseResult.succeeded', false)) {
  330. this.$store.commit('showNotification', {
  331. message: this.$t('admin:auth.saveSuccess'),
  332. style: 'success',
  333. icon: 'check'
  334. })
  335. } else {
  336. throw new Error(_.get(resp, 'data.authentication.updateStrategies.responseResult.message', this.$t('common:error.unexpected')))
  337. }
  338. } catch (err) {
  339. this.$store.commit('pushGraphError', err)
  340. }
  341. this.$store.commit(`loadingStop`, 'admin-auth-savestrategies')
  342. }
  343. },
  344. apollo: {
  345. strategies: {
  346. query: gql`
  347. query {
  348. authentication {
  349. strategies {
  350. key
  351. title
  352. description
  353. isAvailable
  354. useForm
  355. logo
  356. website
  357. props {
  358. key
  359. value
  360. }
  361. }
  362. }
  363. }
  364. `,
  365. fetchPolicy: 'network-only',
  366. update: (data) => _.get(data, 'authentication.strategies', []).map(str => ({
  367. ...str,
  368. isDisabled: !str.isAvailable || str.key === `local`,
  369. props: _.sortBy(str.props.map(cfg => ({
  370. key: cfg.key,
  371. ...JSON.parse(cfg.value)
  372. })), [t => t.order])
  373. })),
  374. watchLoading (isLoading) {
  375. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-strategies-refresh')
  376. }
  377. },
  378. activeStrategies: {
  379. query: gql`
  380. query {
  381. authentication {
  382. activeStrategies {
  383. key
  384. strategy {
  385. key
  386. title
  387. description
  388. useForm
  389. logo
  390. website
  391. }
  392. config {
  393. key
  394. value
  395. }
  396. order
  397. isEnabled
  398. displayName
  399. selfRegistration
  400. domainWhitelist
  401. autoEnrollGroups
  402. }
  403. }
  404. }
  405. `,
  406. fetchPolicy: 'network-only',
  407. update: (data) => _.get(data, 'authentication.activeStrategies', []).map(str => ({
  408. ...str,
  409. config: _.sortBy(str.config.map(cfg => ({
  410. ...cfg,
  411. value: JSON.parse(cfg.value)
  412. })), [t => t.value.order])
  413. })),
  414. watchLoading (isLoading) {
  415. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-activestrategies-refresh')
  416. }
  417. },
  418. groups: {
  419. query: groupsQuery,
  420. fetchPolicy: 'network-only',
  421. update: (data) => data.groups.list,
  422. watchLoading (isLoading) {
  423. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-groups-refresh')
  424. }
  425. },
  426. host: {
  427. query: hostQuery,
  428. fetchPolicy: 'network-only',
  429. update: (data) => _.cloneDeep(data.site.config.host),
  430. watchLoading (isLoading) {
  431. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-host-refresh')
  432. }
  433. }
  434. }
  435. }
  436. </script>