Client - a user can request and download data export archive
This commit is contained in:
parent
073c677b92
commit
67d7fc13b5
@ -62,13 +62,39 @@
|
||||
<button class="danger" @click.prevent="updateDisplayModal(true)">
|
||||
{{ $t('buttons.DELETE_MY_ACCOUNT') }}
|
||||
</button>
|
||||
<button class="confirm" v-if="canRequestExport()" @click.prevent="requestExport">
|
||||
{{ $t('buttons.REQUEST_DATA_EXPORT') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="data-export">
|
||||
<span class="info-box">
|
||||
<i class="fa fa-info-circle" aria-hidden="true" />
|
||||
{{ $t('user.EXPORT_REQUEST.ONLY_ONE_EXPORT_PER_DAY') }}
|
||||
</span>
|
||||
<div v-if="exportRequest" class="data-export-archive">
|
||||
{{$t('user.EXPORT_REQUEST.DATA_EXPORT')}}
|
||||
({{ exportRequestDate }}):
|
||||
<span
|
||||
v-if="exportRequest.status=== 'successful'"
|
||||
class="archive-link"
|
||||
@click.prevent="downloadArchive(exportRequest.file_name)"
|
||||
>
|
||||
<i class="fa fa-download" aria-hidden="true" />
|
||||
{{ $t("user.EXPORT_REQUEST.DOWNLOAD_ARCHIVE") }}
|
||||
({{ getReadableFileSize(exportRequest.file_size) }})
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t(`user.EXPORT_REQUEST.STATUS.${exportRequest.status}`)}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isBefore, subDays } from 'date-fns'
|
||||
import {
|
||||
ComputedRef,
|
||||
Ref,
|
||||
@ -81,14 +107,17 @@
|
||||
onUnmounted,
|
||||
} from 'vue'
|
||||
|
||||
import authApi from "@/api/authApi";
|
||||
import PasswordInput from '@/components/Common/PasswordInput.vue'
|
||||
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
|
||||
import { TAppConfig } from '@/types/application'
|
||||
import { IUserProfile, IUserAccountPayload } from '@/types/user'
|
||||
import {IAuthUserProfile, IUserAccountPayload, IExportRequest} from '@/types/user'
|
||||
import { useStore } from '@/use/useStore'
|
||||
import { formatDate } from '@/utils/dates'
|
||||
import { getReadableFileSize } from '@/utils/files'
|
||||
|
||||
interface Props {
|
||||
user: IUserProfile
|
||||
user: IAuthUserProfile
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const { user } = toRefs(props)
|
||||
@ -114,9 +143,16 @@
|
||||
)
|
||||
const formErrors = ref(false)
|
||||
const displayModal: Ref<boolean> = ref(false)
|
||||
const exportRequest: ComputedRef<IExportRequest | null> = computed(
|
||||
() => store.getters[AUTH_USER_STORE.GETTERS.EXPORT_REQUEST]
|
||||
)
|
||||
const exportRequestDate: ComputedRef<string | null> = computed(
|
||||
() => getExportRequestDate()
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.user) {
|
||||
store.dispatch(AUTH_USER_STORE.ACTIONS.GET_REQUEST_DATA_EXPORT)
|
||||
updateUserForm(props.user)
|
||||
}
|
||||
})
|
||||
@ -124,7 +160,7 @@
|
||||
function invalidateForm() {
|
||||
formErrors.value = true
|
||||
}
|
||||
function updateUserForm(user: IUserProfile) {
|
||||
function updateUserForm(user: IAuthUserProfile) {
|
||||
userForm.email = user.email
|
||||
}
|
||||
function updatePassword(password: string) {
|
||||
@ -133,6 +169,21 @@
|
||||
function updateNewPassword(new_password: string) {
|
||||
userForm.new_password = new_password
|
||||
}
|
||||
function getExportRequestDate() {
|
||||
return exportRequest.value ? formatDate(
|
||||
exportRequest.value.created_at,
|
||||
user.value.timezone,
|
||||
user.value.date_format,
|
||||
true,
|
||||
null, true
|
||||
) : null
|
||||
}
|
||||
|
||||
function canRequestExport() {
|
||||
return exportRequestDate.value
|
||||
? isBefore(new Date(exportRequestDate.value), subDays(new Date(), 1))
|
||||
: true
|
||||
}
|
||||
function updateProfile() {
|
||||
const payload: IUserAccountPayload = {
|
||||
email: userForm.email,
|
||||
@ -150,6 +201,25 @@
|
||||
function deleteAccount(username: string) {
|
||||
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
|
||||
}
|
||||
function requestExport() {
|
||||
store.dispatch(AUTH_USER_STORE.ACTIONS.REQUEST_DATA_EXPORT)
|
||||
}
|
||||
async function downloadArchive(filename: string) {
|
||||
await authApi
|
||||
.get(`/auth/profile/export/${filename}`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
.then((response) => {
|
||||
const archiveFileUrl = window.URL.createObjectURL(
|
||||
new Blob([response.data], { type: 'application/zip' })
|
||||
)
|
||||
const archive_link = document.createElement('a')
|
||||
archive_link.href = archiveFileUrl
|
||||
archive_link.setAttribute('download', filename)
|
||||
document.body.appendChild(archive_link)
|
||||
archive_link.click()
|
||||
})
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
store.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_IS_SUCCESS, false)
|
||||
@ -203,4 +273,17 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.data-export {
|
||||
padding: $default-padding 0;
|
||||
.data-export-archive {
|
||||
padding-top: $default-padding*2;
|
||||
font-size: .9em;
|
||||
|
||||
.archive-link {
|
||||
color: var(--app-a-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -3,6 +3,7 @@
|
||||
"Network Error": "Network Error.",
|
||||
"UNKNOWN": "Error. Please try again or contact the administrator.",
|
||||
"at least one file in zip archive exceeds size limit, please check the archive": "At least one file in zip archive exceeds size limit, please check the archive.",
|
||||
"completed request already exists": "A completed export request already exists.",
|
||||
"email: valid email must be provided": "Email: valid email must be provided.",
|
||||
"error during gpx file parsing": "Error during gpx file parsing.",
|
||||
"error during gpx processing": "Error during gpx processing.",
|
||||
@ -19,6 +20,7 @@
|
||||
"new email must be different than curent email": "The new email must be different than curent email",
|
||||
"no file part": "No file provided.",
|
||||
"no selected file": "No selected file.",
|
||||
"ongoing request exists": "A data export request already exists.",
|
||||
"password: password and password confirmation do not match": "Password: password and password confirmation don't match.",
|
||||
"provide a valid auth token": "Provide a valid auth token.",
|
||||
"signature expired, please log in again": "Signature expired. Please log in again.",
|
||||
|
@ -12,6 +12,7 @@
|
||||
"LOGIN": "Log in",
|
||||
"NO": "No",
|
||||
"REGISTER": "Register",
|
||||
"REQUEST_DATA_EXPORT": "Request data export",
|
||||
"RESET": "Reset",
|
||||
"SUBMIT": "Submit",
|
||||
"YES": "Yes"
|
||||
|
@ -8,6 +8,15 @@
|
||||
"EMAIL": "Email",
|
||||
"EMAIL_INFO": "Enter a valid email address.",
|
||||
"ENTER_PASSWORD": "Enter a password",
|
||||
"EXPORT_REQUEST": {
|
||||
"DATA_EXPORT": "Data export",
|
||||
"DOWNLOAD_ARCHIVE": "Download archive",
|
||||
"ONLY_ONE_EXPORT_PER_DAY": "You can request an archive by 24 hours",
|
||||
"STATUS": {
|
||||
"errored": "errored (please request another export)",
|
||||
"in_progress": "in progres..."
|
||||
}
|
||||
},
|
||||
"FILTER_ON_USERNAME": "Filter on username",
|
||||
"HIDE_PASSWORD": "hide password",
|
||||
"I_WANT_TO_DELETE_MY_ACCOUNT": "I want to delete my account",
|
||||
|
@ -3,6 +3,7 @@
|
||||
"Network Error": "Erreur réseau.",
|
||||
"UNKNOWN": "Erreur. Veuillez réessayer ou contacter l'administrateur.",
|
||||
"at least one file in zip archive exceeds size limit, please check the archive": "Au moins un fichier de l'archive zip dépasse la taille maximale, veuillez vérifier l'archive.",
|
||||
"completed request already exists": "Une demande d'export terminée existe déjà.",
|
||||
"email: valid email must be provided": "Courriel : une adresse électronique valide doit être fournie.",
|
||||
"error during gpx file parsing": "Erreur lors de l'analyse du fichier.",
|
||||
"error during gpx processing": "Erreur lors du traitement du fichier gpx.",
|
||||
@ -19,6 +20,7 @@
|
||||
"new email must be different than curent email": "La nouvelle addresse électronique doit être differente de l'adresse actuelle",
|
||||
"no file part": "Pas de fichier fourni.",
|
||||
"no selected file": "Pas de fichier sélectionné.",
|
||||
"ongoing request exists": "Une demande d'export de données est en cours",
|
||||
"password: password and password confirmation do not match": "Mot de passe : les mots de passe saisis sont différents.",
|
||||
"provide a valid auth token": "Merci de fournir un jeton de connexion valide.",
|
||||
"signature expired, please log in again": "Signature expirée. Merci de vous reconnecter.",
|
||||
|
@ -12,6 +12,7 @@
|
||||
"LOGIN": "Se connecter",
|
||||
"NO": "Non",
|
||||
"REGISTER": "S'inscrire",
|
||||
"REQUEST_DATA_EXPORT": "Demander un export de données",
|
||||
"RESET": "Réinit.",
|
||||
"SUBMIT": "Valider",
|
||||
"YES": "Oui"
|
||||
|
@ -8,6 +8,15 @@
|
||||
"EMAIL": "Courriel",
|
||||
"EMAIL_INFO": "Saisissez une adresse électronique valide.",
|
||||
"ENTER_PASSWORD": "Saisissez un mot de passe",
|
||||
"EXPORT_REQUEST": {
|
||||
"DATA_EXPORT": "Export des données",
|
||||
"DOWNLOAD_ARCHIVE": "Télécharger l'archive",
|
||||
"ONLY_ONE_EXPORT_PER_DAY": "Vous pouvez demander un export par 24h",
|
||||
"STATUS": {
|
||||
"errored": "en erreur (veuillez demander une nouvelle archive)",
|
||||
"in_progress": "en cours..."
|
||||
}
|
||||
},
|
||||
"FILTER_ON_USERNAME": "Filtrer sur le nom d'utilisateur",
|
||||
"HIDE_PASSWORD": "masquer le mot de passe",
|
||||
"I_WANT_TO_DELETE_MY_ACCOUNT": "Je souhaite supprimer mon compte",
|
||||
|
@ -447,4 +447,40 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
|
||||
})
|
||||
.catch((error) => handleError(context, error))
|
||||
},
|
||||
[AUTH_USER_STORE.ACTIONS.REQUEST_DATA_EXPORT](
|
||||
context: ActionContext<IAuthUserState, IRootState>
|
||||
): void {
|
||||
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
authApi
|
||||
.post('auth/profile/export/request')
|
||||
.then((res) => {
|
||||
if (res.data.status === 'success') {
|
||||
context.commit(
|
||||
AUTH_USER_STORE.MUTATIONS.SET_EXPORT_REQUEST,
|
||||
res.data.request
|
||||
)
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
})
|
||||
.catch((error) => handleError(context, error))
|
||||
},
|
||||
[AUTH_USER_STORE.ACTIONS.GET_REQUEST_DATA_EXPORT](
|
||||
context: ActionContext<IAuthUserState, IRootState>
|
||||
): void {
|
||||
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
authApi
|
||||
.get('auth/profile/export')
|
||||
.then((res) => {
|
||||
if (res.data.status === 'success') {
|
||||
context.commit(
|
||||
AUTH_USER_STORE.MUTATIONS.SET_EXPORT_REQUEST,
|
||||
res.data.request
|
||||
)
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
})
|
||||
.catch((error) => handleError(context, error))
|
||||
},
|
||||
}
|
||||
|
@ -5,13 +5,15 @@ export enum AuthUserActions {
|
||||
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||
DELETE_ACCOUNT = 'DELETE_ACCOUNT',
|
||||
DELETE_PICTURE = 'DELETE_PICTURE',
|
||||
GET_REQUEST_DATA_EXPORT = 'GET_REQUEST_DATA_EXPORT',
|
||||
GET_USER_PROFILE = 'GET_USER_PROFILE',
|
||||
LOGIN_OR_REGISTER = 'LOGIN_OR_REGISTER',
|
||||
LOGOUT = 'LOGOUT',
|
||||
SEND_PASSWORD_RESET_REQUEST = 'SEND_PASSWORD_RESET_REQUEST',
|
||||
REQUEST_DATA_EXPORT = 'REQUEST_DATA_EXPORT',
|
||||
RESEND_ACCOUNT_CONFIRMATION_EMAIL = 'RESEND_ACCOUNT_CONFIRMATION_EMAIL',
|
||||
RESET_USER_PASSWORD = 'RESET_USER_PASSWORD',
|
||||
RESET_USER_SPORT_PREFERENCES = 'RESET_USER_SPORT_PREFERENCES',
|
||||
SEND_PASSWORD_RESET_REQUEST = 'SEND_PASSWORD_RESET_REQUEST',
|
||||
UPDATE_USER_ACCOUNT = 'UPDATE_USER_ACCOUNT',
|
||||
UPDATE_USER_PICTURE = 'UPDATE_USER_PICTURE',
|
||||
UPDATE_USER_PROFILE = 'UPDATE_USER_PROFILE',
|
||||
@ -27,6 +29,7 @@ export enum AuthUserGetters {
|
||||
IS_SUCCESS = 'IS_SUCCESS',
|
||||
IS_REGISTRATION_SUCCESS = 'IS_REGISTRATION_SUCCESS',
|
||||
USER_LOADING = 'USER_LOADING',
|
||||
EXPORT_REQUEST = 'EXPORT_REQUEST',
|
||||
}
|
||||
|
||||
export enum AuthUserMutations {
|
||||
@ -36,4 +39,5 @@ export enum AuthUserMutations {
|
||||
UPDATE_IS_SUCCESS = 'UPDATE_USER_IS_SUCCESS',
|
||||
UPDATE_IS_REGISTRATION_SUCCESS = 'UPDATE_IS_REGISTRATION_SUCCESS',
|
||||
UPDATE_USER_LOADING = 'UPDATE_USER_LOADING',
|
||||
SET_EXPORT_REQUEST = 'SET_EXPORT_REQUEST',
|
||||
}
|
||||
|
@ -15,6 +15,9 @@ export const getters: GetterTree<IAuthUserState, IRootState> &
|
||||
[AUTH_USER_STORE.GETTERS.AUTH_USER_PROFILE]: (state: IAuthUserState) => {
|
||||
return state.authUserProfile
|
||||
},
|
||||
[AUTH_USER_STORE.GETTERS.EXPORT_REQUEST]: (state: IAuthUserState) => {
|
||||
return state.exportRequest
|
||||
},
|
||||
[AUTH_USER_STORE.GETTERS.IS_AUTHENTICATED]: (state: IAuthUserState) => {
|
||||
return state.authToken !== null
|
||||
},
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
IAuthUserState,
|
||||
TAuthUserMutations,
|
||||
} from '@/store/modules/authUser/types'
|
||||
import { IAuthUserProfile } from '@/types/user'
|
||||
import { IAuthUserProfile, IExportRequest } from '@/types/user'
|
||||
|
||||
export const mutations: MutationTree<IAuthUserState> & TAuthUserMutations = {
|
||||
[AUTH_USER_STORE.MUTATIONS.CLEAR_AUTH_USER_TOKEN](state: IAuthUserState) {
|
||||
@ -42,4 +42,10 @@ export const mutations: MutationTree<IAuthUserState> & TAuthUserMutations = {
|
||||
) {
|
||||
state.loading = loading
|
||||
},
|
||||
[AUTH_USER_STORE.MUTATIONS.SET_EXPORT_REQUEST](
|
||||
state: IAuthUserState,
|
||||
exportRequest: IExportRequest
|
||||
) {
|
||||
state.exportRequest = exportRequest
|
||||
},
|
||||
}
|
||||
|
@ -7,4 +7,5 @@ export const authUserState: IAuthUserState = {
|
||||
isSuccess: false,
|
||||
isRegistrationSuccess: false,
|
||||
loading: false,
|
||||
exportRequest: null,
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
IUserSportPreferencesPayload,
|
||||
IUserAccountPayload,
|
||||
IUserAccountUpdatePayload,
|
||||
IExportRequest,
|
||||
} from '@/types/user'
|
||||
|
||||
export interface IAuthUserState {
|
||||
@ -27,6 +28,7 @@ export interface IAuthUserState {
|
||||
isRegistrationSuccess: boolean
|
||||
isSuccess: boolean
|
||||
loading: boolean
|
||||
exportRequest: IExportRequest | null
|
||||
}
|
||||
|
||||
export interface IAuthUserActions {
|
||||
@ -115,6 +117,14 @@ export interface IAuthUserActions {
|
||||
context: ActionContext<IAuthUserState, IRootState>,
|
||||
acceptedPolicy: boolean
|
||||
): void
|
||||
|
||||
[AUTH_USER_STORE.ACTIONS.REQUEST_DATA_EXPORT](
|
||||
context: ActionContext<IAuthUserState, IRootState>
|
||||
): void
|
||||
|
||||
[AUTH_USER_STORE.ACTIONS.GET_REQUEST_DATA_EXPORT](
|
||||
context: ActionContext<IAuthUserState, IRootState>
|
||||
): void
|
||||
}
|
||||
|
||||
export interface IAuthUserGetters {
|
||||
@ -124,6 +134,10 @@ export interface IAuthUserGetters {
|
||||
state: IAuthUserState
|
||||
): IAuthUserProfile
|
||||
|
||||
[AUTH_USER_STORE.GETTERS.EXPORT_REQUEST](
|
||||
state: IAuthUserState
|
||||
): IExportRequest | null
|
||||
|
||||
[AUTH_USER_STORE.GETTERS.IS_ADMIN](state: IAuthUserState): boolean
|
||||
|
||||
[AUTH_USER_STORE.GETTERS.IS_AUTHENTICATED](state: IAuthUserState): boolean
|
||||
@ -139,6 +153,10 @@ export interface IAuthUserGetters {
|
||||
|
||||
export type TAuthUserMutations<S = IAuthUserState> = {
|
||||
[AUTH_USER_STORE.MUTATIONS.CLEAR_AUTH_USER_TOKEN](state: S): void
|
||||
[AUTH_USER_STORE.MUTATIONS.SET_EXPORT_REQUEST](
|
||||
state: S,
|
||||
exportRequest: IExportRequest | null
|
||||
): void
|
||||
[AUTH_USER_STORE.MUTATIONS.UPDATE_AUTH_TOKEN](
|
||||
state: S,
|
||||
authToken: string
|
||||
|
@ -108,3 +108,10 @@ export interface ILoginOrRegisterData {
|
||||
formData: ILoginRegisterFormData
|
||||
redirectUrl?: string | null | LocationQueryValue[]
|
||||
}
|
||||
|
||||
export interface IExportRequest {
|
||||
created_at: string
|
||||
status: string
|
||||
file_name: string
|
||||
file_size: number
|
||||
}
|
||||
|
@ -111,14 +111,16 @@ export const formatDate = (
|
||||
timezone: string,
|
||||
dateFormat: string,
|
||||
withTime = true,
|
||||
language: string | null = null
|
||||
language: string | null = null,
|
||||
withSeconds = false
|
||||
): string => {
|
||||
if (!language) {
|
||||
language = locale.value
|
||||
}
|
||||
const timeFormat = withTime ? (withSeconds ? ' HH:mm:ss' : ' HH:mm') : ''
|
||||
return format(
|
||||
getDateWithTZ(dateString, timezone),
|
||||
`${getDateFormat(dateFormat, language)}${withTime ? ' HH:mm' : ''}`,
|
||||
`${getDateFormat(dateFormat, language)}${timeFormat}`,
|
||||
{ locale: localeFromLanguage[language] }
|
||||
)
|
||||
}
|
||||
|
@ -293,6 +293,22 @@ describe('formatDate (w/ default value)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatDate with_seconds', () => {
|
||||
it('format date for "Europe/Paris" timezone and "dd/MM/yyyy" format and seconds', () => {
|
||||
assert.deepEqual(
|
||||
formatDate(
|
||||
'Tue, 01 Nov 2022 00:00:00 GMT',
|
||||
'Europe/Paris',
|
||||
'yyyy-MM-dd',
|
||||
true,
|
||||
null,
|
||||
true
|
||||
),
|
||||
'2022-11-01 01:00:00'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDateFormat', () => {
|
||||
const testsParams = [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user