Add OAuth Tokens management to settings

This commit is contained in:
Maxim Filippov 2019-02-12 21:53:59 +03:00
parent 058238c3c6
commit 2c7406d9a8
25 changed files with 208 additions and 1 deletions

View file

@ -1,6 +1,10 @@
<<<<<<< HEAD
import { compose } from 'vue-compose' import { compose } from 'vue-compose'
import unescape from 'lodash/unescape' import unescape from 'lodash/unescape'
import get from 'lodash/get' import get from 'lodash/get'
=======
import { unescape, truncate } from 'lodash'
>>>>>>> Add OAuth Tokens management to settings
import TabSwitcher from '../tab_switcher/tab_switcher.js' import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue' import ImageCropper from '../image_cropper/image_cropper.vue'
@ -62,6 +66,9 @@ const UserSettings = {
activeTab: 'profile' activeTab: 'profile'
} }
}, },
created () {
this.$store.dispatch('fetchTokens')
},
components: { components: {
StyleSwitcher, StyleSwitcher,
TabSwitcher, TabSwitcher,
@ -87,8 +94,20 @@ const UserSettings = {
direct: { selected: this.newDefaultScope === 'direct' } direct: { selected: this.newDefaultScope === 'direct' }
} }
}, },
<<<<<<< HEAD
currentSaveStateNotice () { currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice return this.$store.state.interface.settings.currentSaveStateNotice
=======
oauthTokens () {
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
return {
id: oauthToken.id,
token: truncate(oauthToken.token, { length: 15 }),
refreshToken: truncate(oauthToken.refresh_token, { length: 15 }),
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
}
})
>>>>>>> Add OAuth Tokens management to settings
} }
}, },
methods: { methods: {
@ -308,6 +327,11 @@ const UserSettings = {
logout () { logout () {
this.$store.dispatch('logout') this.$store.dispatch('logout')
this.$router.replace('/') this.$router.replace('/')
},
revokeToken (id) {
if(confirm('Are you sure?')) {
this.$store.dispatch('revokeToken', id)
}
} }
} }
} }

View file

@ -121,6 +121,30 @@
<p v-if="changePasswordError">{{changePasswordError}}</p> <p v-if="changePasswordError">{{changePasswordError}}</p>
</div> </div>
<div class="setting-item">
<h2>{{$t('settings.oauth_tokens')}}</h2>
<table class="oauth-tokens">
<thead>
<tr>
<th>Token</th>
<th>Refresh Token</th>
<th>Valid Until</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
<td>{{oauthToken.token}}</td>
<td>{{oauthToken.refreshToken}}</td>
<td>{{oauthToken.validUntil}}</td>
<td class="actions">
<button class="btn btn-default" @click="revokeToken(oauthToken.id)">Revoke</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.delete_account')}}</h2> <h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p> <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
@ -213,5 +237,17 @@
border-radius: $fallback--avatarRadius; border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius); border-radius: var(--avatarRadius, $fallback--avatarRadius);
} }
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
} }
</style> </style>

View file

@ -134,6 +134,11 @@
"notification_visibility_mentions": "الإشارات", "notification_visibility_mentions": "الإشارات",
"notification_visibility_repeats": "", "notification_visibility_repeats": "",
"nsfw_clickthrough": "", "nsfw_clickthrough": "",
"oauth_tokens": "رموز OAuth",
"token": "رمز",
"refresh_token": "رمز التحديث",
"valid_until": "صالح حتى",
"revoke_token": "سحب",
"panelRadius": "", "panelRadius": "",
"pause_on_unfocused": "", "pause_on_unfocused": "",
"presets": "النماذج", "presets": "النماذج",

View file

@ -132,6 +132,11 @@
"notification_visibility_repeats": "Republica una entrada meva", "notification_visibility_repeats": "Republica una entrada meva",
"no_rich_text_description": "Neteja el formatat de text de totes les entrades", "no_rich_text_description": "Neteja el formatat de text de totes les entrades",
"nsfw_clickthrough": "Amaga el contingut NSFW darrer d'una imatge clicable", "nsfw_clickthrough": "Amaga el contingut NSFW darrer d'una imatge clicable",
"oauth_tokens": "Llistats OAuth",
"token": "Token",
"refresh_token": "Actualitza el token",
"valid_until": "Vàlid fins",
"revoke_token": "Revocar",
"panelRadius": "Panells", "panelRadius": "Panells",
"pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus", "pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus",
"presets": "Temes", "presets": "Temes",

View file

@ -159,6 +159,11 @@
"hide_follows_description": "Zeige nicht, wem ich folge", "hide_follows_description": "Zeige nicht, wem ich folge",
"hide_followers_description": "Zeige nicht, wer mir folgt", "hide_followers_description": "Zeige nicht, wer mir folgt",
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind", "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
"oauth_tokens": "OAuth-Token",
"token": "Zeichen",
"refresh_token": "Token aktualisieren",
"valid_until": "Gültig bis",
"revoke_token": "Widerrufen",
"panelRadius": "Panel", "panelRadius": "Panel",
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist", "pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
"presets": "Voreinstellungen", "presets": "Voreinstellungen",

View file

@ -189,6 +189,11 @@
"show_admin_badge": "Show Admin badge in my profile", "show_admin_badge": "Show Admin badge in my profile",
"show_moderator_badge": "Show Moderator badge in my profile", "show_moderator_badge": "Show Moderator badge in my profile",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Refresh Token",
"valid_until": "Valid Until",
"revoke_token": "Revoke",
"panelRadius": "Panels", "panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused", "pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets", "presets": "Presets",

View file

@ -171,6 +171,11 @@
"show_admin_badge": "Mostrar la placa de administrador en mi perfil", "show_admin_badge": "Mostrar la placa de administrador en mi perfil",
"show_moderator_badge": "Mostrar la placa de moderador en mi perfil", "show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
"oauth_tokens": "Tokens de OAuth",
"token": "Token",
"refresh_token": "Actualizar el token",
"valid_until": "Válido hasta",
"revoke_token": "Revocar",
"panelRadius": "Paneles", "panelRadius": "Paneles",
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.", "pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
"presets": "Por defecto", "presets": "Por defecto",

View file

@ -166,6 +166,11 @@
"no_rich_text_description": "Älä näytä tekstin muotoilua.", "no_rich_text_description": "Älä näytä tekstin muotoilua.",
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani", "hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse", "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
"oauth_tokens": "OAuth-merkit",
"token": "Token",
"refresh_token": "Päivitä token",
"valid_until": "Voimassa asti",
"revoke_token": "Peruuttaa",
"panelRadius": "Ruudut", "panelRadius": "Ruudut",
"pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta", "pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
"presets": "Valmiit teemat", "presets": "Valmiit teemat",

View file

@ -137,6 +137,11 @@
"notification_visibility_mentions": "Mentionnés", "notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages", "notification_visibility_repeats": "Partages",
"nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible", "nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible",
"oauth_tokens": "Jetons OAuth",
"token": "Jeton",
"refresh_token": "Refresh Token",
"valid_until": "Valable jusque",
"revoke_token": "Révoquer",
"panelRadius": "Fenêtres", "panelRadius": "Fenêtres",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré", "pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
"presets": "Thèmes prédéfinis", "presets": "Thèmes prédéfinis",

View file

@ -134,6 +134,11 @@
"notification_visibility_repeats": "Atphostáil", "notification_visibility_repeats": "Atphostáil",
"no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post", "no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post",
"nsfw_clickthrough": "Cumasaigh an ceangaltán NSFW cliceáil ar an gcnaipe", "nsfw_clickthrough": "Cumasaigh an ceangaltán NSFW cliceáil ar an gcnaipe",
"oauth_tokens": "Tocanna OAuth",
"token": "Token",
"refresh_token": "Athnuachan Comórtas",
"valid_until": "Bailí Go dtí",
"revoke_token": "Athghairm",
"panelRadius": "Painéil", "panelRadius": "Painéil",
"pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte", "pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte",
"presets": "Réamhshocruithe", "presets": "Réamhshocruithe",

View file

@ -129,6 +129,11 @@
"notification_visibility_mentions": "אזכורים", "notification_visibility_mentions": "אזכורים",
"notification_visibility_repeats": "חזרות", "notification_visibility_repeats": "חזרות",
"nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר", "nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר",
"oauth_tokens": "אסימוני OAuth",
"token": "אסימון",
"refresh_token": "רענון האסימון",
"valid_until": "בתוקף עד",
"revoke_token": "בטל",
"panelRadius": "פאנלים", "panelRadius": "פאנלים",
"pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס", "pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס",
"presets": "ערכים קבועים מראש", "presets": "ערכים קבועים מראש",

View file

@ -93,6 +93,11 @@
"notification_visibility_mentions": "Menzioni", "notification_visibility_mentions": "Menzioni",
"notification_visibility_repeats": "Condivisioni", "notification_visibility_repeats": "Condivisioni",
"no_rich_text_description": "Togli la formattazione del testo da tutti i post", "no_rich_text_description": "Togli la formattazione del testo da tutti i post",
"oauth_tokens": "Token OAuth",
"token": "Token",
"refresh_token": "Aggiorna token",
"valid_until": "Valido fino a",
"revoke_token": "Revocare",
"panelRadius": "Pannelli", "panelRadius": "Pannelli",
"pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano", "pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano",
"presets": "Valori predefiniti", "presets": "Valori predefiniti",

View file

@ -171,6 +171,11 @@
"show_admin_badge": "アドミンのしるしをみる", "show_admin_badge": "アドミンのしるしをみる",
"show_moderator_badge": "モデレーターのしるしをみる", "show_moderator_badge": "モデレーターのしるしをみる",
"nsfw_clickthrough": "NSFWなファイルをかくす", "nsfw_clickthrough": "NSFWなファイルをかくす",
"oauth_tokens": "OAuthトークン",
"token": "トークン",
"refresh_token": "トークンを更新",
"valid_until": "まで有効",
"revoke_token": "取り消す",
"panelRadius": "パネル", "panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
"presets": "プリセット", "presets": "プリセット",

View file

@ -159,6 +159,11 @@
"hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음", "hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
"hide_followers_description": "나를 따르는 사람을 보여주지 마라.", "hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
"oauth_tokens": "OAuth 토큰",
"token": "토큰",
"refresh_token": "토큰 새로 고침",
"valid_until": "까지 유효하다",
"revoke_token": "취소",
"panelRadius": "패널", "panelRadius": "패널",
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기", "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
"presets": "프리셋", "presets": "프리셋",

View file

@ -132,6 +132,11 @@
"notification_visibility_repeats": "Gjentakelser", "notification_visibility_repeats": "Gjentakelser",
"no_rich_text_description": "Fjern all formatering fra statuser", "no_rich_text_description": "Fjern all formatering fra statuser",
"nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende", "nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende",
"oauth_tokens": "OAuth Tokens",
"token": "Pollett",
"refresh_token": "Refresh Token",
"valid_until": "Gyldig til",
"revoke_token": "Tilbakekall",
"panelRadius": "Panel", "panelRadius": "Panel",
"pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus", "pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus",
"presets": "Forhåndsdefinerte tema", "presets": "Forhåndsdefinerte tema",

View file

@ -159,6 +159,11 @@
"no_rich_text_description": "Strip rich text formattering van alle posts", "no_rich_text_description": "Strip rich text formattering van alle posts",
"hide_network_description": "Toon niet wie mij volgt en wie ik volg.", "hide_network_description": "Toon niet wie mij volgt en wie ik volg.",
"nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in", "nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in",
"oauth_tokens": "OAuth-tokens",
"token": "Token",
"refresh_token": "Token vernieuwen",
"valid_until": "Geldig tot",
"revoke_token": "Intrekken",
"panelRadius": "Panelen", "panelRadius": "Panelen",
"pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is", "pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is",
"presets": "Presets", "presets": "Presets",

View file

@ -142,6 +142,7 @@
"notification_visibility_mentions": "Mencions", "notification_visibility_mentions": "Mencions",
"notification_visibility_repeats": "Repeticions", "notification_visibility_repeats": "Repeticions",
"no_rich_text_description": "Netejar lo format tèxte de totas las publicacions", "no_rich_text_description": "Netejar lo format tèxte de totas las publicacions",
"oauth_tokens": "Llistats OAuth",
"pause_on_unfocused": "Pausar la difusion quand longlet es pas seleccionat", "pause_on_unfocused": "Pausar la difusion quand longlet es pas seleccionat",
"profile_tab": "Perfil", "profile_tab": "Perfil",
"replies_in_timeline": "Responsas del flux", "replies_in_timeline": "Responsas del flux",

View file

@ -86,6 +86,11 @@
"name_bio": "Imię i bio", "name_bio": "Imię i bio",
"new_password": "Nowe hasło", "new_password": "Nowe hasło",
"nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)", "nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
"oauth_tokens": "Tokeny OAuth",
"token": "Token",
"refresh_token": "Odśwież token",
"valid_until": "Ważne do",
"revoke_token": "Odwołać",
"panelRadius": "Panele", "panelRadius": "Panele",
"presets": "Gotowe motywy", "presets": "Gotowe motywy",
"profile_background": "Tło profilu", "profile_background": "Tło profilu",

View file

@ -132,6 +132,11 @@
"show_admin_badge": "Показывать значок администратора в моем профиле", "show_admin_badge": "Показывать значок администратора в моем профиле",
"show_moderator_badge": "Показывать значок модератора в моем профиле", "show_moderator_badge": "Показывать значок модератора в моем профиле",
"nsfw_clickthrough": "Включить скрытие NSFW вложений", "nsfw_clickthrough": "Включить скрытие NSFW вложений",
"oauth_tokens": "OAuth токены",
"token": "Токен",
"refresh_token": "Рефреш токен",
"valid_until": "Годен до",
"revoke_token": "Удалить",
"panelRadius": "Панели", "panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
"presets": "Пресеты", "presets": "Пресеты",

View file

@ -134,6 +134,11 @@
"notification_visibility_repeats": "转发", "notification_visibility_repeats": "转发",
"no_rich_text_description": "不显示富文本格式", "no_rich_text_description": "不显示富文本格式",
"nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开", "nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开",
"oauth_tokens": "OAuth令牌",
"token": "代币",
"refresh_token": "刷新令牌",
"valid_until": "有效期至",
"revoke_token": "撤消",
"panelRadius": "面板", "panelRadius": "面板",
"pause_on_unfocused": "在离开页面时暂停时间线推送", "pause_on_unfocused": "在离开页面时暂停时间线推送",
"presets": "预置", "presets": "预置",

View file

@ -11,6 +11,7 @@ import configModule from './modules/config.js'
import chatModule from './modules/chat.js' import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js' import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js' import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -64,7 +65,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
config: configModule, config: configModule,
chat: chatModule, chat: chatModule,
oauth: oauthModule, oauth: oauthModule,
mediaViewer: mediaViewerModule mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule
}, },
plugins: [persistedState, pushNotifications], plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.

View file

@ -0,0 +1,26 @@
const oauthTokens = {
state: {
tokens: []
},
actions: {
fetchTokens ({rootState, commit}) {
rootState.api.backendInteractor.fetchOAuthTokens().then((tokens) => {
commit('swapTokens', tokens)
})
},
revokeToken ({rootState, commit, state}, id) {
rootState.api.backendInteractor.revokeOAuthToken(id).then((response) => {
if (response.status === 201) {
commit('swapTokens', state.tokens.filter(token => token.id !== id))
}
})
}
},
mutations: {
swapTokens (state, tokens) {
state.tokens = tokens
}
}
}
export default oauthTokens

View file

@ -531,6 +531,23 @@ const fetchBlocks = ({page, credentials}) => {
}) })
} }
const fetchOAuthTokens = ({credentials}) => {
const url = '/api/oauth_tokens.json'
return fetch(url, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const revokeOAuthToken = ({id, credentials}) => {
const url = `/api/oauth_tokens/${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'DELETE'
})
}
const suggestions = ({credentials}) => { const suggestions = ({credentials}) => {
return fetch(SUGGESTIONS_URL, { return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials) headers: authHeaders(credentials)
@ -573,6 +590,8 @@ const apiService = {
setUserMute, setUserMute,
fetchMutes, fetchMutes,
fetchBlocks, fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -65,6 +65,8 @@ const backendInteractorService = (credentials) => {
const fetchMutes = () => apiService.fetchMutes({credentials}) const fetchMutes = () => apiService.fetchMutes({credentials})
const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params}) const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials}) const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
const getCaptcha = () => apiService.getCaptcha() const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register(params) const register = (params) => apiService.register(params)
@ -96,6 +98,8 @@ const backendInteractorService = (credentials) => {
setUserMute, setUserMute,
fetchMutes, fetchMutes,
fetchBlocks, fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -32,6 +32,16 @@ describe('The users module', () => {
expect(user.muted).to.eql(false) expect(user.muted).to.eql(false)
}) })
it('sets oauth tokens', () => {
const state = cloneDeep(defaultState)
const tokens = [{ id: 1, token: 'bar' }]
mutations.addOAuthTokens(state, tokens)
expect(state.oauthTokens).to.have.length(1)
expect(state.oauthTokens).to.eql(tokens)
})
}) })
describe('getUserByName', () => { describe('getUserByName', () => {