Client - add/delete an OAuth app (WIP)

This commit is contained in:
Sam 2022-05-28 17:14:52 +02:00
parent 25decef696
commit 7e45923b25
15 changed files with 479 additions and 11 deletions

View File

@ -0,0 +1,170 @@
<template>
<div id="new-oauth2-app">
<p id="new-oauth2-title">{{ $t('oauth2.ADD_A_NEW_APP') }}</p>
<div id="apps-form">
<form @submit.prevent="createApp">
<div class="form-items">
<div class="form-item">
<label for="app-name">{{ $t('oauth2.APP.NAME') }}*</label>
<input
id="app-name"
type="text"
required
v-model="appForm.client_name"
/>
</div>
<div class="form-item">
<label for="app-description">{{
$t('oauth2.APP.DESCRIPTION')
}}</label>
<CustomTextArea
name="app-description"
:charLimit="200"
:input="appForm.description"
@updateValue="updateDescription"
/>
</div>
<div class="form-item">
<label for="app-url">{{ $t('oauth2.APP.URL') }}*</label>
<input
id="app-url"
type="text"
required
v-model="appForm.client_uri"
/>
</div>
<div class="form-item">
<label for="app-redirect-uri"
>{{ $t('oauth2.APP.REDIRECT_URL') }}*</label
>
<input
id="app-redirect-uri"
type="text"
required
v-model="appForm.redirect_uri"
/>
</div>
<div class="form-item-scope">
<div class="form-item-scope-label">
{{ $t('oauth2.APP.SCOPE.LABEL') }}*
</div>
<div class="form-item-scope-checkboxes">
<label>
<input
type="checkbox"
:checked="appForm.read"
@change="appForm.read = !appForm.read"
/>
{{ $t('oauth2.APP.SCOPE.READ') }}
</label>
<label>
<input
type="checkbox"
:checked="appForm.write"
@change="appForm.write = !appForm.write"
/>
{{ $t('oauth2.APP.SCOPE.WRITE') }}
</label>
</div>
</div>
</div>
<div class="form-buttons">
<button
class="confirm"
type="submit"
:disabled="!appForm.read && !appForm.write"
>
{{ $t('buttons.SUBMIT') }}
</button>
<button
class="cancel"
@click.prevent="() => $router.push('/profile/apps')"
>
{{ $t('buttons.CANCEL') }}
</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { OAUTH2_STORE } from '@/store/constants'
import { IOAuth2ClientPayload } from '@/types/oauth'
import { useStore } from '@/use/useStore'
const store = useStore()
const appForm = reactive({
client_name: '',
client_uri: '',
client_description: '',
redirect_uri: '',
read: true,
write: false,
})
function createApp() {
const payload: IOAuth2ClientPayload = {
client_name: appForm.client_name,
client_description: appForm.client_description,
client_uri: appForm.client_uri,
redirect_uris: [appForm.redirect_uri],
scope: `${appForm.read ? 'read' : ''} ${appForm.write ? 'write' : ''}`,
}
store.dispatch(OAUTH2_STORE.ACTIONS.CREATE_CLIENT, payload)
}
function updateDescription(value: string) {
appForm.client_description = value
}
</script>
<style scoped lang="scss">
@import '~@/scss/vars.scss';
#new-oauth2-app {
#new-oauth2-title {
font-size: 1.05em;
font-weight: bold;
padding: 0 $default-padding;
}
#apps-form {
.form-items {
display: flex;
flex-direction: column;
input {
height: 20px;
}
.form-item-scope {
padding: $default-padding;
.form-item-scope-label {
font-weight: bold;
}
.form-item-scope-checkboxes {
display: flex;
gap: $default-padding;
}
}
.form-item {
display: flex;
flex-direction: column;
padding: $default-padding;
}
}
.form-buttons {
display: flex;
justify-content: flex-end;
button {
margin: $default-padding * 0.5;
}
}
}
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<div id="oauth2-app" class="description-list">
<Modal
v-if="displayModal"
:title="$t('common.CONFIRMATION')"
:message="$t('oauth2.APP_DELETION_CONFIRMATION')"
@confirmAction="deleteClient(client.id)"
@cancelAction="updateDisplayModal(false)"
/>
<div v-if="client && client.client_id">
<dl>
<dt>{{ $t('oauth2.APP.CLIENT_ID') }}:</dt>
<dd>{{ client.client_id }}</dd>
<dt>{{ capitalize($t('oauth2.APP.ISSUE_AT')) }}:</dt>
<dd>
{{
format(
getDateWithTZ(client.issued_at, authUser.timezone),
'dd/MM/yyyy HH:mm'
)
}}
</dd>
<dt>{{ $t('oauth2.APP.NAME') }}:</dt>
<dd>{{ client.name }}</dd>
<dt>{{ $t('oauth2.APP.DESCRIPTION') }}:</dt>
<dd :class="{ 'no-description': !client.client_description }">
{{
client.client_description
? client.client_description
: $t('oauth2.NO_DESCRIPTION')
}}
</dd>
<dt>{{ $t('oauth2.APP.URL') }}:</dt>
<dd>{{ client.website }}</dd>
<dt>{{ $t('oauth2.APP.REDIRECT_URL') }}:</dt>
<dd>
{{ client.redirect_uris.length > 0 ? client.redirect_uris[0] : '' }}
</dd>
<dt>{{ $t('oauth2.APP.SCOPE.LABEL') }}:</dt>
<dd>{{ client.scope }}</dd>
</dl>
<div class="app-buttons">
<button class="danger" @click="updateDisplayModal(true)">
{{ $t('oauth2.DELETE_APP') }}
</button>
<button @click="$router.push('/profile/apps')">
{{ $t('buttons.BACK') }}
</button>
</div>
</div>
<div v-else>
<p class="no-app">{{ $t('oauth2.NO_APP') }}</p>
<button @click="$router.push('/profile/apps')">
{{ $t('buttons.BACK') }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { format } from 'date-fns'
import {
ComputedRef,
Ref,
capitalize,
computed,
onBeforeMount,
toRefs,
ref,
onUnmounted,
} from 'vue'
import { useRoute } from 'vue-router'
import { OAUTH2_STORE, ROOT_STORE } from '@/store/constants'
import { IOAuth2Client } from '@/types/oauth'
import { IAuthUserProfile } from '@/types/user'
import { useStore } from '@/use/useStore'
import { getDateWithTZ } from '@/utils/dates'
interface Props {
authUser: IAuthUserProfile
}
const props = defineProps<Props>()
const route = useRoute()
const store = useStore()
const { authUser } = toRefs(props)
const client: ComputedRef<IOAuth2Client> = computed(
() => store.getters[OAUTH2_STORE.GETTERS.CLIENT]
)
let displayModal: Ref<boolean> = ref(false)
onBeforeMount(() => {
loadClient()
})
function loadClient() {
if (route.params.clientId && typeof route.params.clientId === 'string') {
store.dispatch(OAUTH2_STORE.ACTIONS.GET_CLIENT, route.params.clientId)
}
}
function deleteClient(clientId: number) {
store.dispatch(OAUTH2_STORE.ACTIONS.DELETE_CLIENT, clientId)
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
onUnmounted(() => {
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
store.commit(OAUTH2_STORE.MUTATIONS.EMPTY_CLIENT)
})
</script>
<style scoped lang="scss">
@import '~@/scss/vars.scss';
#oauth2-app {
.app-buttons {
display: flex;
flex-wrap: wrap;
gap: $default-padding;
}
.no-description {
font-style: italic;
}
.no-app {
font-style: italic;
padding: $default-padding 0;
}
}
</style>

View File

@ -3,7 +3,9 @@
<p class="apps-list">{{ $t('oauth2.APPS_LIST') }}</p> <p class="apps-list">{{ $t('oauth2.APPS_LIST') }}</p>
<ul v-if="clients.length > 0"> <ul v-if="clients.length > 0">
<li v-for="client in clients" :key="client.client_id"> <li v-for="client in clients" :key="client.client_id">
<router-link :to="{ name: 'UserApp', params: { clientId: client.id } }">
{{ client.name }} {{ client.name }}
</router-link>
<span class="app-issued-at"> <span class="app-issued-at">
{{ $t('oauth2.APP.ISSUE_AT') }} {{ $t('oauth2.APP.ISSUE_AT') }}
{{ {{
@ -17,8 +19,13 @@
</ul> </ul>
<div class="no-apps" v-else>{{ $t('oauth2.NO_APPS') }}</div> <div class="no-apps" v-else>{{ $t('oauth2.NO_APPS') }}</div>
<Pagination :pagination="pagination" path="/profile/apps" :query="query" /> <Pagination :pagination="pagination" path="/profile/apps" :query="query" />
<div class="app-list-buttons">
<button @click="$router.push('/profile/apps/new')">
{{ $t('oauth2.NEW_APP') }}
</button>
<button @click="$router.push('/')">{{ $t('common.HOME') }}</button> <button @click="$router.push('/')">{{ $t('common.HOME') }}</button>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -92,11 +99,16 @@
.app-issued-at { .app-issued-at {
font-size: 0.85em; font-size: 0.85em;
font-style: italic; font-style: italic;
padding-left: $default-padding;
} }
.apps-list { .apps-list {
font-size: 1.05em; font-size: 1.05em;
font-weight: bold; font-weight: bold;
} }
.app-list-buttons {
display: flex;
gap: $default-padding;
}
.no-apps { .no-apps {
font-style: italic; font-style: italic;
} }

View File

@ -5,14 +5,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { toRefs } from 'vue' import { onUnmounted, toRefs } from 'vue'
import { OAUTH2_STORE, ROOT_STORE } from '@/store/constants'
import { IAuthUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
import { useStore } from '@/use/useStore'
interface Props { interface Props {
user: IAuthUserProfile user: IAuthUserProfile
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const store = useStore()
const { user } = toRefs(props) const { user } = toRefs(props)
onUnmounted(() => {
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
store.commit(OAUTH2_STORE.MUTATIONS.SET_CLIENTS, [])
})
</script> </script>

View File

@ -7,7 +7,7 @@
type="radio" type="radio"
:id="tab" :id="tab"
:name="tab" :name="tab"
:checked="selectedTab === tab" :checked="selectedTab.split('/')[0] === tab"
:disabled="disabled" :disabled="disabled"
@input="$router.push(getPath(tab))" @input="$router.push(getPath(tab))"
/> />

View File

@ -1,7 +1,23 @@
{ {
"ADD_A_NEW_APP": "Add a new OAuth2 application",
"APP": { "APP": {
"ISSUE_AT": "issued at" "CLIENT_ID": "Client ID",
"DESCRIPTION": "Application description",
"ISSUE_AT": "issue at",
"NAME": "Application name",
"REDIRECT_URL": "Redirect URI",
"SCOPE": {
"LABEL": "Scope",
"READ": "read",
"WRITE": "write"
}, },
"URL": "Application URI"
},
"APP_DELETION_CONFIRMATION": "Are you sure you want to delete this app?",
"APPS_LIST": "OAuth2 applications", "APPS_LIST": "OAuth2 applications",
"NO_APPS": "No applications" "DELETE_APP": "Delete application",
"NEW_APP": "New OAuth App",
"NO_DESCRIPTION": "no description",
"NO_APP": "Application not found!",
"NO_APPS": "no applications"
} }

View File

@ -1,7 +1,23 @@
{ {
"ADD_A_NEW_APP": "Ajouter une nouvelle application OAuth2",
"APP": { "APP": {
"ISSUE_AT": "créée le" "CLIENT_ID": "Identifiant client",
"DESCRIPTION": "Application description",
"ISSUE_AT": "créée le",
"NAME": "Nom de l'application",
"REDIRECT_URL": "URL de redirection",
"SCOPE": {
"LABEL": "Scope",
"READ": "lecture",
"WRITE": "écriture"
}, },
"URL": "URL de l'application"
},
"APP_DELETION_CONFIRMATION": "Êtes-vous sûr de vouloir supprimer cette application ?",
"APPS_LIST": "Applications OAuth2", "APPS_LIST": "Applications OAuth2",
"DELETE_APP": "Supprimer l'application",
"NEW_APP": "Ajouter une App OAuth",
"NO_DESCRIPTION": "pas de description",
"NO_APP": "Application introuvable !",
"NO_APPS": "pas de applications" "NO_APPS": "pas de applications"
} }

View File

@ -12,7 +12,9 @@ import UserAccountEdition from '@/components/User/ProfileEdition/UserAccountEdit
import UserInfosEdition from '@/components/User/ProfileEdition/UserInfosEdition.vue' import UserInfosEdition from '@/components/User/ProfileEdition/UserInfosEdition.vue'
import UserPictureEdition from '@/components/User/ProfileEdition/UserPictureEdition.vue' import UserPictureEdition from '@/components/User/ProfileEdition/UserPictureEdition.vue'
import UserPreferencesEdition from '@/components/User/ProfileEdition/UserPreferencesEdition.vue' import UserPreferencesEdition from '@/components/User/ProfileEdition/UserPreferencesEdition.vue'
import AddUserApp from '@/components/User/UserApps/AddUserApp.vue'
import UserApps from '@/components/User/UserApps/index.vue' import UserApps from '@/components/User/UserApps/index.vue'
import UserApp from '@/components/User/UserApps/UserApp.vue'
import UserAppsList from '@/components/User/UserApps/UserAppsList.vue' import UserAppsList from '@/components/User/UserApps/UserAppsList.vue'
import UserSportPreferences from '@/components/User/UserSportPreferences.vue' import UserSportPreferences from '@/components/User/UserSportPreferences.vue'
import store from '@/store' import store from '@/store'
@ -156,6 +158,16 @@ const routes: Array<RouteRecordRaw> = [
name: 'UserAppsList', name: 'UserAppsList',
component: UserAppsList, component: UserAppsList,
}, },
{
path: ':clientId',
name: 'UserApp',
component: UserApp,
},
{
path: 'new',
name: 'AddUserApp',
component: AddUserApp,
},
], ],
}, },
], ],

View File

@ -1,13 +1,69 @@
import { ActionContext, ActionTree } from 'vuex' import { ActionContext, ActionTree } from 'vuex'
import authApi from '@/api/authApi' import authApi from '@/api/authApi'
import router from '@/router'
import { OAUTH2_STORE, ROOT_STORE } from '@/store/constants' import { OAUTH2_STORE, ROOT_STORE } from '@/store/constants'
import { IOAuth2Actions, IOAuth2State } from '@/store/modules/oauth2/types' import { IOAuth2Actions, IOAuth2State } from '@/store/modules/oauth2/types'
import { IRootState } from '@/store/modules/root/types' import { IRootState } from '@/store/modules/root/types'
import { IOauth2ClientsPayload } from '@/types/oauth' import { IOauth2ClientsPayload, IOAuth2ClientPayload } from '@/types/oauth'
import { handleError } from '@/utils' import { handleError } from '@/utils'
export const actions: ActionTree<IOAuth2State, IRootState> & IOAuth2Actions = { export const actions: ActionTree<IOAuth2State, IRootState> & IOAuth2Actions = {
[OAUTH2_STORE.ACTIONS.CREATE_CLIENT](
context: ActionContext<IOAuth2State, IRootState>,
payload: IOAuth2ClientPayload
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
authApi
.post('oauth/apps', payload)
.then((res) => {
if (res.data.status === 'created') {
context
.dispatch(OAUTH2_STORE.ACTIONS.GET_CLIENTS)
.then(() => router.push('/profile/apps'))
} else {
handleError(context, null)
}
})
.catch((error) => handleError(context, error))
},
[OAUTH2_STORE.ACTIONS.DELETE_CLIENT](
context: ActionContext<IOAuth2State, IRootState>,
id: number
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
authApi
.delete(`oauth/apps/${id}`)
.then((res) => {
if (res.status === 204) {
context
.dispatch(OAUTH2_STORE.ACTIONS.GET_CLIENTS)
.then(() => router.push('/profile/apps'))
} else {
handleError(context, null)
}
})
.catch((error) => handleError(context, error))
},
[OAUTH2_STORE.ACTIONS.GET_CLIENT](
context: ActionContext<IOAuth2State, IRootState>,
id: string
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
authApi
.get(`oauth/apps/${id}`)
.then((res) => {
if (res.data.status === 'success') {
context.commit(
OAUTH2_STORE.MUTATIONS.SET_CLIENT,
res.data.data.client
)
} else {
handleError(context, null)
}
})
.catch((error) => handleError(context, error))
},
[OAUTH2_STORE.ACTIONS.GET_CLIENTS]( [OAUTH2_STORE.ACTIONS.GET_CLIENTS](
context: ActionContext<IOAuth2State, IRootState>, context: ActionContext<IOAuth2State, IRootState>,
payload: IOauth2ClientsPayload payload: IOauth2ClientsPayload

View File

@ -1,13 +1,19 @@
export enum OAuth2Actions { export enum OAuth2Actions {
CREATE_CLIENT = 'CREATE_CLIENT',
DELETE_CLIENT = 'DELETE_CLIENT',
GET_CLIENTS = 'GET_CLIENTS', GET_CLIENTS = 'GET_CLIENTS',
GET_CLIENT = 'GET_CLIENT',
} }
export enum OAuth2Getters { export enum OAuth2Getters {
CLIENT = 'CLIENT',
CLIENTS = 'CLIENTS', CLIENTS = 'CLIENTS',
CLIENTS_PAGINATION = 'CLIENTS_PAGINATION', CLIENTS_PAGINATION = 'CLIENTS_PAGINATION',
} }
export enum OAuth2Mutations { export enum OAuth2Mutations {
EMPTY_CLIENT = 'EMPTY_CLIENT',
SET_CLIENT = 'SET_CLIENT',
SET_CLIENTS = 'SET_CLIENTS', SET_CLIENTS = 'SET_CLIENTS',
SET_CLIENTS_PAGINATION = 'SET_CLIENTS_PAGINATION', SET_CLIENTS_PAGINATION = 'SET_CLIENTS_PAGINATION',
} }

View File

@ -5,6 +5,7 @@ import { IOAuth2Getters, IOAuth2State } from '@/store/modules/oauth2/types'
import { IRootState } from '@/store/modules/root/types' import { IRootState } from '@/store/modules/root/types'
export const getters: GetterTree<IOAuth2State, IRootState> & IOAuth2Getters = { export const getters: GetterTree<IOAuth2State, IRootState> & IOAuth2Getters = {
[OAUTH2_STORE.GETTERS.CLIENT]: (state: IOAuth2State) => state.client,
[OAUTH2_STORE.GETTERS.CLIENTS]: (state: IOAuth2State) => state.clients, [OAUTH2_STORE.GETTERS.CLIENTS]: (state: IOAuth2State) => state.clients,
[OAUTH2_STORE.GETTERS.CLIENTS_PAGINATION]: (state: IOAuth2State) => [OAUTH2_STORE.GETTERS.CLIENTS_PAGINATION]: (state: IOAuth2State) =>
state.pagination, state.pagination,

View File

@ -6,6 +6,15 @@ import { IPagination } from '@/types/api'
import { IOAuth2Client } from '@/types/oauth' import { IOAuth2Client } from '@/types/oauth'
export const mutations: MutationTree<IOAuth2State> & TOAuth2Mutations = { export const mutations: MutationTree<IOAuth2State> & TOAuth2Mutations = {
[OAUTH2_STORE.MUTATIONS.SET_CLIENT](
state: IOAuth2State,
client: IOAuth2Client
) {
state.client = client
},
[OAUTH2_STORE.MUTATIONS.EMPTY_CLIENT](state: IOAuth2State) {
state.client = <IOAuth2Client>{}
},
[OAUTH2_STORE.MUTATIONS.SET_CLIENTS]( [OAUTH2_STORE.MUTATIONS.SET_CLIENTS](
state: IOAuth2State, state: IOAuth2State,
clients: IOAuth2Client[] clients: IOAuth2Client[]

View File

@ -1,7 +1,9 @@
import { IOAuth2State } from '@/store/modules/oauth2/types' import { IOAuth2State } from '@/store/modules/oauth2/types'
import { IPagination } from '@/types/api' import { IPagination } from '@/types/api'
import { IOAuth2Client } from '@/types/oauth'
export const oAuth2State: IOAuth2State = { export const oAuth2State: IOAuth2State = {
client: <IOAuth2Client>{},
clients: [], clients: [],
pagination: <IPagination>{}, pagination: <IPagination>{},
} }

View File

@ -8,14 +8,31 @@ import {
import { OAUTH2_STORE } from '@/store/constants' import { OAUTH2_STORE } from '@/store/constants'
import { IRootState } from '@/store/modules/root/types' import { IRootState } from '@/store/modules/root/types'
import { IPagination } from '@/types/api' import { IPagination } from '@/types/api'
import { IOAuth2Client, IOauth2ClientsPayload } from '@/types/oauth' import {
IOAuth2Client,
IOAuth2ClientPayload,
IOauth2ClientsPayload,
} from '@/types/oauth'
export interface IOAuth2State { export interface IOAuth2State {
client: IOAuth2Client
clients: IOAuth2Client[] clients: IOAuth2Client[]
pagination: IPagination pagination: IPagination
} }
export interface IOAuth2Actions { export interface IOAuth2Actions {
[OAUTH2_STORE.ACTIONS.CREATE_CLIENT](
context: ActionContext<IOAuth2State, IRootState>,
payload: IOAuth2ClientPayload
): void
[OAUTH2_STORE.ACTIONS.DELETE_CLIENT](
context: ActionContext<IOAuth2State, IRootState>,
id: number
): void
[OAUTH2_STORE.ACTIONS.GET_CLIENT](
context: ActionContext<IOAuth2State, IRootState>,
id: string
): void
[OAUTH2_STORE.ACTIONS.GET_CLIENTS]( [OAUTH2_STORE.ACTIONS.GET_CLIENTS](
context: ActionContext<IOAuth2State, IRootState>, context: ActionContext<IOAuth2State, IRootState>,
payload: IOauth2ClientsPayload payload: IOauth2ClientsPayload
@ -23,11 +40,14 @@ export interface IOAuth2Actions {
} }
export interface IOAuth2Getters { export interface IOAuth2Getters {
[OAUTH2_STORE.GETTERS.CLIENT](state: IOAuth2State): IOAuth2Client
[OAUTH2_STORE.GETTERS.CLIENTS](state: IOAuth2State): IOAuth2Client[] [OAUTH2_STORE.GETTERS.CLIENTS](state: IOAuth2State): IOAuth2Client[]
[OAUTH2_STORE.GETTERS.CLIENTS_PAGINATION](state: IOAuth2State): IPagination [OAUTH2_STORE.GETTERS.CLIENTS_PAGINATION](state: IOAuth2State): IPagination
} }
export type TOAuth2Mutations<S = IOAuth2State> = { export type TOAuth2Mutations<S = IOAuth2State> = {
[OAUTH2_STORE.MUTATIONS.EMPTY_CLIENT](state: S): void
[OAUTH2_STORE.MUTATIONS.SET_CLIENT](state: S, client: IOAuth2Client): void
[OAUTH2_STORE.MUTATIONS.SET_CLIENTS](state: S, clients: IOAuth2Client[]): void [OAUTH2_STORE.MUTATIONS.SET_CLIENTS](state: S, clients: IOAuth2Client[]): void
[OAUTH2_STORE.MUTATIONS.SET_CLIENTS_PAGINATION]( [OAUTH2_STORE.MUTATIONS.SET_CLIENTS_PAGINATION](
state: S, state: S,

View File

@ -10,6 +10,14 @@ export interface IOAuth2Client {
website: string website: string
} }
export interface IOAuth2ClientPayload {
client_name: string
client_uri: string
client_description: string | null
redirect_uris: string[]
scope: string
}
export interface IOauth2ClientsPayload { export interface IOauth2ClientsPayload {
page?: number page?: number
} }