+
{{ $t('login.form.promptNoAccount') }}
{{ $t('login.form.linkRegister') }}
+/**
+ * EmailVerification Component
+ *
+ * @description * Renders a confirmation message after user registers,
+ * prompting user to confirm their address by clicking the link in the email.
+ *
+ * @example
+ *
+ *
+ * @see [Figma Design](https://www.figma.com/design/L8dVREySVXxh3X12TcFDdR/Do-pr%C3%A1ce-na-kole?node-id=4858-103494&t=6I4I349ASWWgGjGu-1)
+ */
+
+// libraries
+import { colors } from 'quasar';
+import { computed, defineComponent, inject, onMounted, watch } from 'vue';
+import { useRouter } from 'vue-router';
+
+// config
+import { routesConf } from '../../router/routes_conf';
+
+// stores
+import { useRegisterStore } from '../../stores/register';
+
+// types
+import type { Logger } from '../types/Logger';
+
+export default defineComponent({
+ name: 'EmailVerification',
+ setup() {
+ const logger = inject('vuejs3-logger') as Logger | null;
+ const registerStore = useRegisterStore();
+ const email = computed(() => registerStore.getEmail);
+ const isEmailVerified = computed(() => registerStore.getIsEmailVerified);
+ // check email verification on page load
+ onMounted(async () => {
+ if (!isEmailVerified.value) {
+ await registerStore.checkEmailVerification();
+ }
+ });
+ const router = useRouter();
+ // once email is verified, redirect to home page
+ watch(isEmailVerified, (newValue) => {
+ if (newValue) {
+ logger?.debug(
+ `Email address <${email.value}> was verified successfully, redirect to <${routesConf['home']['path']}> URL.`,
+ );
+ router.push(routesConf['home']['path']);
+ }
+ });
+
+ // colors
+ const { getPaletteColor, changeAlpha } = colors;
+ const white = getPaletteColor('white');
+ const whiteOpacity20 = changeAlpha(white, 0.2);
+
+ return {
+ email,
+ white,
+ whiteOpacity20,
+ };
+ },
+});
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('register.form.titleEmailVerification') }}
+
+
+
+
+
+
+ {{ $t('register.form.hintWrongEmail') }}
+ {{ $t('register.form.linkRegister') }}.
+
+
+
diff --git a/src/components/register/FormRegister.vue b/src/components/register/FormRegister.vue
index 2d696b36f..b8476d15c 100644
--- a/src/components/register/FormRegister.vue
+++ b/src/components/register/FormRegister.vue
@@ -15,6 +15,7 @@
* @slots
*
* @components
+ * - `FormFieldEmail`: Component to render email input.
* - `LoginRegisterButtons`: Component to render third-party authentication
* buttons.
*
@@ -25,7 +26,7 @@
*/
// libraries
-import { defineComponent, ref, reactive } from 'vue';
+import { defineComponent, ref, reactive, computed } from 'vue';
import { rideToWorkByBikeConfig } from '../../boot/global_vars';
// composables
@@ -35,6 +36,10 @@ import { useValidation } from '../../composables/useValidation';
import FormFieldEmail from '../global/FormFieldEmail.vue';
import LoginRegisterButtons from '../global/LoginRegisterButtons.vue';
+// stores
+import { useChallengeStore } from '../../stores/challenge';
+import { useRegisterStore } from '../../stores/register';
+
export default defineComponent({
name: 'FormRegister',
components: {
@@ -45,18 +50,32 @@ export default defineComponent({
setup() {
const formRegister = reactive({
email: '',
- password: '',
- passwordConfirm: '',
+ password1: '',
+ password2: '',
});
+ const registerStore = useRegisterStore();
+ const challengeStore = useChallengeStore();
+ const isActiveChallenge = computed(
+ () => challengeStore.getIsChallengeActive,
+ );
const isPassword = ref(true);
const isPasswordConfirm = ref(true);
+ const isPrivacyConsent = ref(false);
+ const isNewsletterSubscription = ref(false);
const { isEmail, isFilled, isIdentical, isStrongPassword } =
useValidation();
- const onSubmitRegister = () => {
- // noop
+ const onSubmitRegister = async (): Promise => {
+ // fields are already validated in the QForm
+ await registerStore.register(formRegister.email, formRegister.password1);
+ };
+
+ const onReset = (): void => {
+ formRegister.email = '';
+ formRegister.password1 = '';
+ formRegister.password2 = '';
};
const backgroundColor = rideToWorkByBikeConfig.colorWhiteOpacity;
@@ -66,35 +85,45 @@ export default defineComponent({
backgroundColor,
borderRadius,
formRegister,
+ isActiveChallenge,
isPassword,
isPasswordConfirm,
+ isPrivacyConsent,
+ isNewsletterSubscription,
isEmail,
isFilled,
isIdentical,
isStrongPassword,
onSubmitRegister,
+ onReset,
};
},
});
-
+
-
-
+
+
{{ $t('register.form.titleRegister') }}
+
+ {{ $t('register.form.textNoActiveChallenge') }}
+
-
+
@@ -105,11 +134,13 @@ export default defineComponent({
+
+
@@ -240,11 +332,3 @@ export default defineComponent({
-
-
diff --git a/src/components/types/Config.ts b/src/components/types/Config.ts
index 5b89e6e35..5ff63f2e3 100644
--- a/src/components/types/Config.ts
+++ b/src/components/types/Config.ts
@@ -42,8 +42,10 @@ export interface ConfigGlobal {
apiBase: string;
apiVersion: string;
apiDefaultLang: string;
+ urlApiHasUserVerifiedEmail: string;
urlApiLogin: string;
urlApiRefresh: string;
+ urlApiRegister: string;
urlLoginRegisterBackgroundImage: string;
}
diff --git a/src/components/types/Logger.ts b/src/components/types/Logger.ts
index 09dbaaff8..546a78ab1 100644
--- a/src/components/types/Logger.ts
+++ b/src/components/types/Logger.ts
@@ -2,4 +2,5 @@ export interface Logger {
debug: (message: string) => void;
error: (message: string) => void;
info: (message: string) => void;
+ warn: (message: string) => void;
}
diff --git a/src/composables/useApi.ts b/src/composables/useApi.ts
index 67619d3a4..7d19a5934 100644
--- a/src/composables/useApi.ts
+++ b/src/composables/useApi.ts
@@ -5,9 +5,12 @@ import { api, axios } from '../boot/axios';
import { i18n } from '../boot/i18n';
import { getApiBaseUrlWithLang } from '../utils/get_api_base_url_with_lang';
+// utils
+import { requestDefaultHeader } from '../utils';
+
// config
import { rideToWorkByBikeConfig } from '../boot/global_vars';
-const { apiVersion, apiDefaultLang, apiBase } = rideToWorkByBikeConfig;
+const { apiDefaultLang, apiBase } = rideToWorkByBikeConfig;
// types
import type { AxiosRequestHeaders, Method } from 'axios';
@@ -17,6 +20,13 @@ interface ApiResponse
{
data: T | null;
}
+interface apiData {
+ url: string;
+ method: Method;
+ headers: AxiosRequestHeaders;
+ data?: object;
+}
+
interface errorResponseData {
[non_field_errors: string]: string[];
}
@@ -71,14 +81,12 @@ export const useApi = () => {
payload,
translationKey,
method = 'get',
- headers = {
- Accept: `application/json; version=${apiVersion}`,
- } as AxiosRequestHeaders,
+ headers = requestDefaultHeader,
logger,
showSuccessMessage = true,
}: {
endpoint: string;
- payload: object;
+ payload?: object;
translationKey: string;
method: Method;
headers?: AxiosRequestHeaders;
@@ -98,12 +106,15 @@ export const useApi = () => {
// Inject Axios base API URL with lang (internationalization)
injectAxioBaseApiUrlWithLang(logger);
const startTime = performance.now();
- const response = await api({
+ const data: apiData = {
url: endpoint,
method: method,
- data: payload,
- headers,
- });
+ headers: headers,
+ };
+ if (payload) {
+ data.data = payload;
+ }
+ const response = await api(data);
const endTime = performance.now();
logger?.info(
`Call to function took ${(endTime - startTime) / 1000} seconds.`,
diff --git a/src/i18n/cs.toml b/src/i18n/cs.toml
index c4dd95db7..c37033acf 100755
--- a/src/i18n/cs.toml
+++ b/src/i18n/cs.toml
@@ -11,6 +11,11 @@ transportType = "Způsoby dopravy"
[breadcrumb]
results = "Výsledky"
+[checkEmailVerification]
+apiMessageError = "Ověření emailu se nezdařilo."
+apiMessageErrorWithMessage = "Ověření emailu se nezdařilo. Chyba: {error}"
+apiMessageSuccess = "Email byl úspěšně ověřen."
+
[drawerMenu]
buttonParticipation = "Účast"
buttonCityAdministration = "Správa města"
@@ -526,23 +531,38 @@ messageJwtInvalidExpiration = "Chyba: přihlašovací token nemá platnost."
messageJwtInvalidFormat = "Chyba: neplatný formát přihlašovacího tokenu."
messageJwtInvalidWithMessage = "Nepodařilo se přečíst přihlašovací token. Chyba: {error}"
+[register]
+apiMessageError = "Registrace se nezdařila. Prosím, zkuste to znovu později."
+apiMessageErrorWithMessage = "Registrace se nezdařila. Prosím, zkuste to znovu později. Chyba: {error}"
+apiMessageSuccess = "Registrace byla úspěšná"
+
[register.form]
-titleRegister = "Registrace"
+hintLogin = "Již máte účet?"
+hintPassword = "Musí obsahovat alespoň 6 znaků a alespoň 1 písmeno."
+hintRegisterAsCoordinator = "Chcete se stát firemní*m či školní*m koordinátorem*kou, ale neúčastnit se výzvy?"
+hintWrongEmail = "Máte špatně e-mail?"
labelEmail = "E-mail"
labelPassword = "Heslo"
labelPasswordConfirm = "Potvrzení hesla"
-hintPassword = "Musí obsahovat alespoň 6 znaků a alespoň 1 písmeno."
+labelPrivacyConsent1 = "Souhlasím se"
+labelPrivacyConsentLink = "zpracováním osobních údajů"
+labelPrivacyConsent2 = "za účelem vytvoření účtu a upozorňování na budoucí výzvy."
+linkLogin = "Přihlašte se"
+labelNewsletterSubscription = "Chci dostávat info o doprovodných akcích (cyklojízdách, snídaních...) a akčních nabídkách."
+linkRegister = "Registrujte se znovu"
+linkRegisterAsCoordinator = "Registrujte se jako koordinátor*ka."
messageEmailReqired = "Prosím vyplňte e-mail"
messageEmailInvalid = "Prosím vyplňte platný e-mail"
messagePasswordRequired = "Prosím vyplňte heslo"
messagePasswordStrong = "Heslo musí obsahovat alespoň 6 znaků a alespoň 1 písmeno."
messagePasswordConfirmRequired = "Prosím vyplňte heslo"
messagePasswordConfirmNotMatch = "Hesla se neshodují"
+messagePrivacyConsentRequired = "Musíte souhlasit se zpracováním osobních údajů"
submitRegister = "Registrovat se"
-hintRegisterAsCoordinator = "Chcete se stát firemní*m či školní*m koordinátorem*kou, ale neúčastnit se výzvy?"
-linkRegisterAsCoordinator = "Registrujte se jako koordinátor*ka."
-hintLogin = "Již máte účet?"
-linkLogin = "Přihlašte se"
+textEmailVerification = "Právě jsme vám na e-mail {email} poslali pozvánku do systému Do práce na kole.
Pokud ji nemůžete najít, koukněte i do spamu.
"
+textNoActiveChallenge = "Momentálně žádná z výzev Do práce na kole neprobíhá. Můžete se zaregistrovat, rádi vás informujeme, jakmile vypíšeme další."
+titleEmailVerification = "Pokračujte potvrzením e-mailu"
+titleRegister = "Registrace"
[register.buttons]
buttonGoogle = "Registrovat přes Facebook"
diff --git a/src/i18n/en.toml b/src/i18n/en.toml
index 52919094f..0c851c973 100755
--- a/src/i18n/en.toml
+++ b/src/i18n/en.toml
@@ -11,6 +11,11 @@ transportType = "Means of transport"
[breadcrumb]
results = "Results"
+[checkEmailVerification]
+apiMessageError = "Email verification failed."
+apiMessageErrorWithMessage = "Email verification failed. Error: {error}"
+apiMessageSuccess = "Email was successfully verified."
+
[drawerMenu]
buttonParticipation = "Participation"
buttonCityAdministration = "City administration"
@@ -523,23 +528,38 @@ messageJwtInvalidExpiration = "Error: login token is invalid."
messageJwtInvalidFormat = "Error: invalid login token format."
messageJwtInvalidWithMessage = "Failed to read login token. Error: {error}"
+[register]
+apiMessageError = "Registration failed. Please try again later."
+apiMessageErrorWithMessage = "Registration failed. Please try again later. Error: {error}"
+apiMessageSuccess = "Registration was successful"
+
[register.form]
-titleRegister = "Registration"
-labelEmail = "E-mail"
+hintLogin = "Already have an account?"
+hintPassword = "It must contain at least 6 characters and at least 1 letter."
+hintRegisterAsCoordinator = "Do you want to become a company or school coordinator but not participate in the challenge?"
+hintWrongEmail = "Do you have the wrong email?"
+labelEmail = "Email"
labelPassword = "Password"
labelPasswordConfirm = "Password confirmation"
-hintPassword = "It must contain at least 6 characters and at least 1 letter."
+labelPrivacyConsent1 = "I agree to the processing of personal data according to"
+labelPrivacyConsentLink = "Privacy policy"
+labelPrivacyConsent2 = "for the purpose of creating an account and informing about future challenges."
+linkLogin = "Sign in"
+labelNewsletterSubscription = "I want to receive information about accompanying events (bike rides, breakfasts...) and action offers."
+linkRegister = "Register again"
+linkRegisterAsCoordinator = "Register as a coordinator"
messageEmailReqired = "Please fill in your e-mail"
messageEmailInvalid = "Please fill in a valid email"
messagePasswordRequired = "Please fill in the password"
messagePasswordStrong = "Password must contain at least 6 characters and at least 1 letter."
messagePasswordConfirmRequired = "Please fill in the password"
messagePasswordConfirmNotMatch = "Passwords don't match"
+messagePrivacyConsentRequired = "You must consent to the processing of personal data"
submitRegister = "Register now"
-hintRegisterAsCoordinator = "Do you want to become a company or school coordinator but not participate in the challenge?"
-linkRegisterAsCoordinator = "Register as a coordinator"
-hintLogin = "Already have an account?"
-linkLogin = "Sign in"
+textEmailVerification = "We have just sent an invitation to the Ride to Work by Bike system to your email {email}.
If you can't find it, please check your spam folder.
"
+textNoActiveChallenge = "Currently, no challenge is active. You can still register, we will inform you as soon as we launch the next one."
+titleEmailVerification = "Continue by confirming your email"
+titleRegister = "Registration"
[register.buttons]
buttonGoogle = "Register via Facebook"
diff --git a/src/i18n/sk.toml b/src/i18n/sk.toml
index a172e7e77..8c8186b6e 100755
--- a/src/i18n/sk.toml
+++ b/src/i18n/sk.toml
@@ -11,6 +11,11 @@ transportType = "Spôsoby dopravy"
[breadcrumb]
results = "Výsledky"
+[checkEmailVerification]
+apiMessageError = "Overenie e-mailu zlyhalo."
+apiMessageErrorWithMessage = "Overenie e-mailu zlyhalo. Chyba: {error}"
+apiMessageSuccess = "E-mail bol úspešne overený."
+
[drawerMenu]
buttonParticipation = "Účasť"
buttonCityAdministration = "Správa mesta"
@@ -523,23 +528,38 @@ messageJwtInvalidExpiration = "Chyba: prihlasovací token nemá platnosť."
messageJwtInvalidFormat = "Chyba: neplatný formát prihlasovacieho tokenu."
messageJwtInvalidWithMessage = "Nepodarilo sa prečítať prihlasovací token. Chyba: {error}"
+[register]
+apiMessageError = "Registrácia sa nezdařila. Prosím, skúste to znovu později."
+apiMessageErrorWithMessage = "Registrácia sa nezdařila. Prosím, skúste to znovu později. Chyba: {error}"
+apiMessageSuccess = "Registrácia bola úspešná"
+
[register.form]
-titleRegister = "Registrácia"
+hintLogin = "Už máte účet?"
+hintPassword = "Musí obsahovať aspoň 6 znakov a aspoň 1 písmeno."
+hintRegisterAsCoordinator = "Chcete sa stať firemným alebo školským koordinátorom, ale nezúčastniť sa výzvy?"
+hintWrongEmail = "Máte nesprávny e-mail?"
labelEmail = "E-mail"
labelPassword = "Heslo"
labelPasswordConfirm = "Potvrdenie hesla"
-hintPassword = "Musí obsahovať aspoň 6 znakov a aspoň 1 písmeno."
+labelPrivacyConsent1 = "Súhlasím so"
+labelPrivacyConsentLink = "spracovaním osobných údajov"
+labelPrivacyConsent2 = "za účelom vytvorenia účtu a upozorňovania na budúce výzvy."
+linkLogin = "Prihláste sa"
+labelNewsletterSubscription = "Chcete dostávať informácie o doprovodných akciách (cyklojazdy, raňajky...) a akčných ponukách."
+linkRegister = "Zaregistrujte sa znovu"
+linkRegisterAsCoordinator = "Zaregistrujte sa ako koordinátor"
messageEmailReqired = "Vyplňte prosím svoj e-mail"
messageEmailInvalid = "Vyplňte prosím platný e-mail"
messagePasswordRequired = "Vyplňte prosím heslo"
messagePasswordStrong = "Heslo musí obsahovať aspoň 6 znakov a aspoň 1 písmeno."
messagePasswordConfirmRequired = "Vyplňte prosím heslo"
messagePasswordConfirmNotMatch = "Heslá sa nezhodujú"
+messagePrivacyConsentRequired = "Musíte súhlasiť s spracovaním osobných údajov"
submitRegister = "Zaregistrujte sa"
-hintRegisterAsCoordinator = "Chcete sa stať firemným alebo školským koordinátorom, ale nezúčastniť sa výzvy?"
-linkRegisterAsCoordinator = "Zaregistrujte sa ako koordinátor"
-hintLogin = "Už máte účet?"
-linkLogin = "Prihláste sa"
+textEmailVerification = "Práve sme vám na e-mail {email} poslali pozvánku do systému Do práce na bicykli.
Ak ju nemôžete nájsť, pozrite sa aj do spamu.
"
+textNoActiveChallenge = "Momentálne neprebieha žiadna výzva Do práce na bicykli. Môžete sa zaregistrovať a my vás radi budeme informovať, keď vypíšeme ďalšiu."
+titleEmailVerification = "Pokračujte potvrdením e-mailu"
+titleRegister = "Registrácia"
[register.buttons]
buttonGoogle = "Registrácia cez Facebook"
diff --git a/src/pages/RegisterPage.vue b/src/pages/RegisterPage.vue
index cdcf1e7af..cd40d289d 100644
--- a/src/pages/RegisterPage.vue
+++ b/src/pages/RegisterPage.vue
@@ -44,7 +44,7 @@ export default defineComponent({
-
+
diff --git a/src/pages/VerifyEmailPage.vue b/src/pages/VerifyEmailPage.vue
new file mode 100644
index 000000000..062423b87
--- /dev/null
+++ b/src/pages/VerifyEmailPage.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
diff --git a/src/router/index.ts b/src/router/index.ts
index a6d8a6fe8..b47a18c5d 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -6,6 +6,7 @@ import {
createWebHistory,
} from 'vue-router';
import { useLoginStore } from 'src/stores/login';
+import { useRegisterStore } from 'src/stores/register';
import routes from './routes';
import { routesConf } from './routes_conf';
@@ -35,14 +36,44 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE),
});
- // turn off auth check if in Cypress tests
- if (!window.Cypress) {
+ // turn off auth check if in Cypress tests (except for register tests)
+ if (!window.Cypress || window.Cypress.spec.name === 'register.spec.cy.js') {
Router.beforeEach(async (to, from, next) => {
const loginStore = useLoginStore();
- const isAuthenticated = await loginStore.validateAccessToken();
- // if not authenticated and not on login page, redirect to login page
+ const registerStore = useRegisterStore();
+ const isAuthenticated: boolean = await loginStore.validateAccessToken();
+ const isEmailVerified: boolean = registerStore.getIsEmailVerified;
+
+ // if authenticated and not verified email, redirect to confirm email page
if (
+ isAuthenticated &&
+ !isEmailVerified &&
+ // only these pages are accessible when authenticated and not verified email
+ !to.matched.some(
+ (record) => record.path === routesConf['verify_email']['path'],
+ )
+ ) {
+ next({ path: routesConf['verify_email']['path'] });
+ }
+ // if authenticated and on login page or register page or confirm email page, redirect to home page
+ else if (
+ isAuthenticated &&
+ isEmailVerified &&
+ // these pages are not accessible when authenticated and verified
+ to.matched.some(
+ (record) =>
+ record.path === routesConf['login']['path'] ||
+ record.path === routesConf['register']['path'] ||
+ record.path === routesConf['verify_email']['path'],
+ )
+ ) {
+ next({ path: routesConf['home']['path'] });
+ }
+ // if not authenticated and not on login or register or confirm email page, redirect to login page
+ else if (
!isAuthenticated &&
+ isEmailVerified &&
+ // only these pages are accessible when not authenticated
!to.matched.some(
(record) =>
record.path === routesConf['login']['path'] ||
@@ -50,15 +81,24 @@ export default route(function (/* { store, ssrContext } */) {
)
) {
next({ path: routesConf['login']['path'] });
- } else {
- next();
}
- // if authenticated and on login page, redirect to home page
- if (
- isAuthenticated &&
- to.matched.some((record) => record.path === routesConf['login']['path'])
+ // if is not awaiting confirmation, and user navigates to confirm email page, redirect based on login status
+ else if (
+ !isAuthenticated &&
+ !isEmailVerified &&
+ // only these pages are accessible when not authenticated and awaiting confirmation
+ !to.matched.some(
+ (record) =>
+ record.path === routesConf['login']['path'] ||
+ record.path === routesConf['register']['path'] ||
+ record.path === routesConf['verify_email']['path'],
+ )
) {
- next({ path: routesConf['home']['path'] });
+ next({ path: routesConf['login']['path'] });
+ }
+ // pass
+ else {
+ next();
}
});
}
diff --git a/src/router/routes.ts b/src/router/routes.ts
index 03f9c8ac8..95eb1b03d 100644
--- a/src/router/routes.ts
+++ b/src/router/routes.ts
@@ -26,6 +26,18 @@ const routes: RouteRecordRaw[] = [
},
],
},
+ // verify email
+ {
+ path: routesConf['verify_email']['path'],
+ component: () => import('layouts/LoginRegisterLayout.vue'),
+ children: [
+ {
+ path: '',
+ name: routesConf['verify_email']['children']['name'],
+ component: () => import('pages/VerifyEmailPage.vue'),
+ },
+ ],
+ },
// login
{
path: routesConf['login']['path'],
diff --git a/src/router/routes_conf.ts b/src/router/routes_conf.ts
index 562871c9f..6154447a6 100644
--- a/src/router/routes_conf.ts
+++ b/src/router/routes_conf.ts
@@ -23,6 +23,13 @@ const routesConf: RoutesConf = {
name: 'company-coordinator',
},
},
+ verify_email: {
+ path: '/verify-email',
+ children: {
+ fullPath: '/verify-email',
+ name: 'verify-email',
+ },
+ },
home: {
path: '/',
children: {
diff --git a/src/stores/challenge.ts b/src/stores/challenge.ts
new file mode 100644
index 000000000..2bda7f4e1
--- /dev/null
+++ b/src/stores/challenge.ts
@@ -0,0 +1,27 @@
+// libraries
+import { defineStore } from 'pinia';
+
+// types
+import type { Logger } from '../components/types/Logger';
+
+export const useChallengeStore = defineStore('challenge', {
+ state: () => ({
+ // property set in pinia.js boot file
+ $log: null as Logger | null,
+ isChallengeActive: false,
+ }),
+
+ getters: {
+ getIsChallengeActive: (state): boolean => state.isChallengeActive,
+ },
+
+ actions: {
+ setIsChallengeActive(isActive: boolean): void {
+ this.isChallengeActive = isActive;
+ },
+ },
+
+ persist: {
+ pick: ['isChallengeActive'],
+ },
+});
diff --git a/src/stores/login.ts b/src/stores/login.ts
index bf0adbf1c..1b1b5c26d 100644
--- a/src/stores/login.ts
+++ b/src/stores/login.ts
@@ -7,8 +7,8 @@ import { Router } from 'vue-router';
import { i18n } from '../boot/i18n';
import { useApi } from '../composables/useApi';
import { useJwt } from '../composables/useJwt';
-
import { timestampToDatetimeString } from 'src/utils';
+import { setAccessRefreshTokens } from '../utils/set_access_refresh_tokens';
// config
import { rideToWorkByBikeConfig } from '../boot/global_vars';
@@ -139,40 +139,18 @@ export const useLoginStore = defineStore('login', {
`Login store saved user data <${JSON.stringify(this.getUser, null, 2)}>.`,
);
}
- // set tokens
if (data && data.access && data.refresh) {
- this.$log?.info('Save access and refresh token into store.');
- this.setAccessToken(data.access);
- this.setRefreshToken(data.refresh);
- this.$log?.debug(
- `Login store saved access token <${this.getAccessToken}>.`,
- );
- this.$log?.debug(
- `Login store saved refresh token <${this.getRefreshToken}>.`,
- );
-
- // set JWT expiration
- const { readJwtExpiration } = useJwt();
- const expiration = readJwtExpiration(data.access);
- this.$log?.debug(
- `Current time <${timestampToDatetimeString(Date.now() / 1000)}>.`,
- );
- this.$log?.debug(
- `Access token expiration time <${expiration ? timestampToDatetimeString(expiration) : null}>.`,
- );
- if (expiration) {
- this.setJwtExpiration(expiration);
- this.$log?.debug(
- `Login store saved access token expiration time <${this.getJwtExpiration ? timestampToDatetimeString(this.getJwtExpiration) : null}>.`,
- );
- }
-
- // token refresh (if no page reload before expiration)
- this.scheduleTokenRefresh();
+ // set tokens
+ setAccessRefreshTokens({
+ access: data.access,
+ refresh: data.refresh,
+ loginStore: this,
+ $log: this.$log as Logger,
+ });
if (this.$router) {
- this.$log?.info(
- `Login was succcesfull, redirect to <${routesConf['home']['path']}> URL.`,
+ this.$log?.debug(
+ `Login was successfull, redirect to <${routesConf['home']['path']}> URL.`,
);
this.$router.push(routesConf['home']['path']);
}
diff --git a/src/stores/register.ts b/src/stores/register.ts
new file mode 100644
index 000000000..bb9d39a6a
--- /dev/null
+++ b/src/stores/register.ts
@@ -0,0 +1,166 @@
+// libraries
+import { defineStore } from 'pinia';
+import { Router } from 'vue-router';
+
+// composables
+import { useApi } from 'src/composables/useApi';
+import { setAccessRefreshTokens } from '../utils/set_access_refresh_tokens';
+
+// stores
+import { useLoginStore } from './login';
+
+// utils
+import { requestDefaultHeader, requestTokenHeader } from 'src/utils';
+
+// config
+import { rideToWorkByBikeConfig } from 'src/boot/global_vars';
+import { routesConf } from '../router/routes_conf';
+
+// types
+import type { Logger } from '../components/types/Logger';
+
+declare module 'pinia' {
+ export interface PiniaCustomProperties {
+ $router: Router;
+ }
+}
+
+interface RegisterResponse {
+ access: string;
+ refresh: string;
+ user: {
+ email: string;
+ };
+}
+
+interface HasVerifiedEmailResponse {
+ has_user_verified_email_address: boolean;
+}
+
+export const useRegisterStore = defineStore('register', {
+ state: () => ({
+ // property set in pinia.js boot file
+ $log: null as Logger | null,
+ email: '',
+ isEmailVerified: false,
+ }),
+
+ getters: {
+ getEmail: (state): string => state.email,
+ getIsEmailVerified: (state): boolean => state.isEmailVerified,
+ },
+
+ actions: {
+ setEmail(email: string): void {
+ this.email = email;
+ },
+ setIsEmailVerified(awaiting: boolean): void {
+ this.isEmailVerified = awaiting;
+ },
+ /**
+ * Register user
+ * Sends the registration request to the API.
+ * If successful:
+ * - sets auth tokens from the response
+ * - stores user email in register store
+ * - sets isEmailVerified flag to false
+ * - redirects to email verification page
+ * If not successful, returns response data.
+ * @param {string} email - Email address
+ * @param {string} password - Password
+ * @return {Promise
} - Register response or null
+ */
+ async register(
+ email: string,
+ password: string,
+ ): Promise {
+ const { apiFetch } = useApi();
+ this.$log?.debug(`Register email <${email}>.`);
+ this.$log?.debug(`Register password <${password}>.`);
+ // register
+ this.$log?.info('Post API registration details.');
+ const { data } = await apiFetch({
+ endpoint: rideToWorkByBikeConfig.urlApiRegister,
+ method: 'post',
+ payload: {
+ email: email,
+ password1: password,
+ password2: password,
+ },
+ translationKey: 'register',
+ logger: this.$log,
+ });
+
+ if (data?.user?.email) {
+ // set email in store
+ this.$log?.info('Registration successful. Saving email to store.');
+ this.setEmail(data.user.email);
+ this.$log?.debug(`Register store saved email <${this.getEmail}>.`);
+ // set isEmailVerified in store
+ this.$log?.info('Setting isEmailVerified flag.');
+ this.setIsEmailVerified(false);
+ this.$log?.debug(
+ `Register store set isEmailVerified to <${this.getIsEmailVerified}>.`,
+ );
+
+ // redirect to confirm email page
+ if (this.$router) {
+ this.$log?.debug(
+ `Registration was succcesfull, redirect to <${routesConf['verify_email']['path']}> URL.`,
+ );
+ this.$router.push(routesConf['verify_email']['path']);
+ }
+ }
+
+ // set tokens
+ if (data && data.access && data.refresh) {
+ const loginStore = useLoginStore();
+ setAccessRefreshTokens({
+ access: data.access,
+ refresh: data.refresh,
+ loginStore,
+ $log: this.$log as Logger,
+ });
+ }
+
+ return data;
+ },
+ /**
+ * Check email verification
+ * Sends the email verification check request to the API.
+ * If successful, sets isEmailVerified flag to the value from the response.
+ * @returns {Promise}
+ */
+ async checkEmailVerification(): Promise {
+ const { apiFetch } = useApi();
+ this.$log?.debug(`Checking email verification for <${this.email}>.`);
+ const loginStore = useLoginStore();
+ // Append access token into HTTP header
+ requestTokenHeader.Authorization += loginStore.getAccessToken;
+ // check email verification
+ const { data } = await apiFetch({
+ endpoint: rideToWorkByBikeConfig.urlApiHasUserVerifiedEmail,
+ method: 'get',
+ translationKey: 'checkEmailVerification',
+ showSuccessMessage: false,
+ headers: Object.assign(requestDefaultHeader, requestTokenHeader),
+ logger: this.$log,
+ });
+
+ // type check data
+ if (data && typeof data?.has_user_verified_email_address === 'boolean') {
+ this.$log?.info('Email verification check successful.');
+ this.setIsEmailVerified(data.has_user_verified_email_address);
+ this.$log?.debug(
+ `Email verified status set to <${this.isEmailVerified}>.`,
+ );
+ } else {
+ this.$log?.warn('Email verification check failed or returned no data.');
+ }
+ },
+ },
+
+ persist: {
+ pick: ['email', 'isEmailVerified'],
+ },
+});
diff --git a/src/utils/get_app_conf.js b/src/utils/get_app_conf.js
index bdafefe52..c7d025a0a 100644
--- a/src/utils/get_app_conf.js
+++ b/src/utils/get_app_conf.js
@@ -92,10 +92,15 @@ const getAppConfig = (process) => {
config['apiVersion'] = process.env.API_VERSION;
} else if (process.env.API_DEFAULT_LANG) {
config['apiDefaultLang'] = process.env.API_DEFAULT_LANG;
+ } else if (process.env.URL_API_HAS_USER_VERIFIED_EMAIL) {
+ config['urlApiHasUserVerifiedEmail'] =
+ process.env.URL_API_HAS_USER_VERIFIED_EMAIL;
} else if (process.env.URL_API_LOGIN) {
config['urlApiLogin'] = process.env.URL_API_LOGIN;
} else if (process.env.URL_API_REFRESH) {
config['urlApiRefresh'] = process.env.URL_API_REFRESH;
+ } else if (process.env.URL_API_REGISTER) {
+ config['urlApiRegister'] = process.env.URL_API_REGISTER;
} else if (process.env.URL_LOGIN_REGISTER_BACKGROUND_IMAGE) {
config['urlLoginRegisterBackgroundImage'] =
process.env.URL_LOGIN_REGISTER_BACKGROUND_IMAGE;
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 3412116ac..ed1a9279d 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,6 +1,11 @@
-const getString = (text: string): string => {
- return text;
-};
+// types
+import type { AxiosRequestHeaders } from 'axios';
+import { ConfigGlobal } from '../components/types';
+
+// config
+const rideToWorkByBikeConfig: ConfigGlobal = JSON.parse(
+ process.env.RIDE_TO_WORK_BY_BIKE_CONFIG,
+);
/*
* Convert date time timestamp number to formated
@@ -39,4 +44,12 @@ const timestampToDatetimeString = (timestamp: number): string => {
return formatedDateTime;
};
-export { getString, timestampToDatetimeString };
+const requestDefaultHeader = {
+ Accept: `application/json; version=${rideToWorkByBikeConfig.apiVersion}`,
+} as AxiosRequestHeaders;
+
+const requestTokenHeader = {
+ Authorization: 'Bearer ',
+} as AxiosRequestHeaders;
+
+export { requestDefaultHeader, requestTokenHeader, timestampToDatetimeString };
diff --git a/src/utils/set_access_refresh_tokens.ts b/src/utils/set_access_refresh_tokens.ts
new file mode 100644
index 000000000..f93d4ab3a
--- /dev/null
+++ b/src/utils/set_access_refresh_tokens.ts
@@ -0,0 +1,48 @@
+// composables
+import { useLoginStore } from '../stores/login';
+import { useJwt } from '../composables/useJwt';
+import { timestampToDatetimeString } from '../utils';
+
+// types
+import type { Logger } from '../components/types/Logger';
+
+interface SetAccessRefreshTokensParams {
+ access: string;
+ refresh: string;
+ loginStore: ReturnType;
+ $log: Logger;
+}
+
+export const setAccessRefreshTokens = ({
+ access,
+ refresh,
+ loginStore,
+ $log,
+}: SetAccessRefreshTokensParams) => {
+ $log?.info('Save access and refresh token into store.');
+ loginStore.setAccessToken(access);
+ loginStore.setRefreshToken(refresh);
+ $log?.debug(`Login store saved access token <${loginStore.getAccessToken}>.`);
+ $log?.debug(
+ `Login store saved refresh token <${loginStore.getRefreshToken}>.`,
+ );
+
+ // set JWT expiration
+ const { readJwtExpiration } = useJwt();
+ const expiration = readJwtExpiration(access);
+ $log?.debug(
+ `Current time <${timestampToDatetimeString(Date.now() / 1000)}>.`,
+ );
+ $log?.debug(
+ `Access token expiration time <${expiration ? timestampToDatetimeString(expiration) : null}>.`,
+ );
+ if (expiration) {
+ loginStore.setJwtExpiration(expiration);
+ $log?.debug(
+ `Login store saved access token expiration time <${loginStore.getJwtExpiration ? timestampToDatetimeString(loginStore.getJwtExpiration) : null}>.`,
+ );
+ }
+
+ // token refresh (if no page reload before expiration)
+ loginStore.scheduleTokenRefresh();
+};
diff --git a/test/cypress/e2e/login.spec.cy.js b/test/cypress/e2e/login.spec.cy.js
index 4b2bdd77f..a8d815b87 100644
--- a/test/cypress/e2e/login.spec.cy.js
+++ b/test/cypress/e2e/login.spec.cy.js
@@ -1,22 +1,13 @@
import {
testLanguageSwitcher,
testBackgroundImage,
+ timeUntilExpiration,
+ systemTime,
} from '../support/commonTests';
import { routesConf } from '../../../src/router/routes_conf';
import { httpSuccessfullStatus } from '../support/commonTests';
import { getApiBaseUrlWithLang } from '../../../src/utils/get_api_base_url_with_lang';
-// variables
-// access token expiration time: Tuesday 24. September 2024 22:36:03
-const fixtureTokenExpiration = new Date('2024-09-24T22:36:03');
-const fixtureTokenExpirationTime = fixtureTokenExpiration.getTime() / 1000;
-// refresh token expiration time: Tuesday 24. September 2024 22:37:41
-// const fixtureTokenRefreshExpiration = new Date('2024-09-24T22:37:41');
-// const fixtureTokenRefreshExpirationTime = fixtureTokenRefreshExpiration.getTime() / 1000;
-const timeUntilRefresh = 60;
-const timeUntilExpiration = timeUntilRefresh * 2;
-const systemTime = fixtureTokenExpirationTime - timeUntilExpiration; // 2 min before JWT expires
-
describe('Login page', () => {
context('desktop', () => {
beforeEach(() => {
diff --git a/test/cypress/e2e/register.spec.cy.js b/test/cypress/e2e/register.spec.cy.js
index f91d36d6a..083b03839 100644
--- a/test/cypress/e2e/register.spec.cy.js
+++ b/test/cypress/e2e/register.spec.cy.js
@@ -1,16 +1,36 @@
import {
testLanguageSwitcher,
testBackgroundImage,
+ httpSuccessfullStatus,
+ httpInternalServerErrorStatus,
+ systemTime,
} from '../support/commonTests';
import { routesConf } from '../../../src/router/routes_conf';
+import { getApiBaseUrlWithLang } from '../../../src/utils/get_api_base_url_with_lang';
-describe('Login page', () => {
+// selectors
+const selectorFormRegisterPrivacyConsent = 'form-register-privacy-consent';
+const selectorFormRegisterEmail = 'form-register-email';
+const selectorFormRegisterPasswordInput = 'form-register-password-input';
+const selectorFormRegisterPasswordConfirmInput =
+ 'form-register-password-confirm-input';
+const selectorFormRegisterSubmit = 'form-register-submit';
+
+// variables
+const testEmail = 'test@example.com';
+const testPassword = 'validPassword123';
+
+const registerRequestBody = {
+ email: testEmail,
+ password1: testPassword,
+ password2: testPassword,
+};
+
+describe('Register page', () => {
context('desktop', () => {
beforeEach(() => {
cy.visit('#' + routesConf['register']['path']);
cy.viewport('macbook-16');
-
- // load config an i18n objects as aliases
cy.task('getAppConfig', process).then((config) => {
// alias config
cy.wrap(config).as('config');
@@ -28,7 +48,6 @@ describe('Login page', () => {
cy.dataCy('login-register-header').should('be.visible');
});
- // switching between languages can only be tested in E2E context
testLanguageSwitcher();
it('renders register form', () => {
@@ -50,5 +69,250 @@ describe('Login page', () => {
});
});
});
+
+ it('renders coordinator registration link', () => {
+ cy.dataCy('form-register-coordinator-link')
+ .should('be.visible')
+ .invoke('attr', 'href')
+ .should('contain', routesConf['register_coordinator']['path']);
+ });
+
+ it('renders login link', () => {
+ cy.dataCy('form-register-login-link')
+ .should('be.visible')
+ .invoke('attr', 'href')
+ .should('contain', routesConf['login']['path']);
+ });
+
+ it('shows error message on registration failure', () => {
+ cy.get('@i18n').then((i18n) => {
+ cy.get('@config').then((config) => {
+ // variables
+ const { apiBase, apiDefaultLang, urlApiRegister } = config;
+ const apiBaseUrl = getApiBaseUrlWithLang(
+ null,
+ apiBase,
+ apiDefaultLang,
+ i18n,
+ );
+ const apiRegisterUrl = `${apiBaseUrl}${urlApiRegister}`;
+ // intercept register request
+ cy.intercept('POST', apiRegisterUrl, {
+ statusCode: httpInternalServerErrorStatus,
+ body: { message: 'Registration failed' },
+ }).as('registerRequest');
+ // fill form
+ cy.dataCy(selectorFormRegisterEmail).find('input').type(testEmail);
+ cy.dataCy(selectorFormRegisterPasswordInput).type(testPassword);
+ cy.dataCy(selectorFormRegisterPasswordConfirmInput).type(
+ testPassword,
+ );
+ // accept privacy policy
+ cy.dataCy(selectorFormRegisterPrivacyConsent)
+ .should('be.visible')
+ .click('topLeft');
+ cy.dataCy(selectorFormRegisterSubmit).click();
+ // wait for request to complete
+ cy.wait('@registerRequest');
+ // check error message
+ cy.get('@i18n').then((i18n) => {
+ cy.contains(
+ i18n.global.t('register.apiMessageErrorWithMessage'),
+ ).should('be.visible');
+ });
+ });
+ });
+ });
+
+ // ! router redirection rules are enabled for this file in /router/index.ts
+ it('allows user to register with valid credentials and requires email verification to access other pages', () => {
+ cy.get('@i18n').then((i18n) => {
+ cy.get('@config').then((config) => {
+ // variables
+ const {
+ apiBase,
+ apiDefaultLang,
+ urlApiHasUserVerifiedEmail,
+ urlApiRegister,
+ } = config;
+ const apiBaseUrl = getApiBaseUrlWithLang(
+ null,
+ apiBase,
+ apiDefaultLang,
+ i18n,
+ );
+ const apiRegisterUrl = `${apiBaseUrl}${urlApiRegister}`;
+ const apiEmailVerificationUrl = `${apiBaseUrl}${urlApiHasUserVerifiedEmail}`;
+ cy.fixture('registerResponse.json').then((registerResponse) => {
+ // intercept register request
+ cy.intercept('POST', apiRegisterUrl, {
+ statusCode: httpSuccessfullStatus,
+ body: registerResponse,
+ }).as('registerRequest');
+ // intercept email verification request
+ cy.intercept('GET', apiEmailVerificationUrl, {
+ statusCode: httpSuccessfullStatus,
+ body: { has_user_verified_email_address: false },
+ }).as('emailVerificationRequest');
+
+ cy.clock().then((clock) => {
+ clock.setSystemTime(systemTime);
+ // fill form
+ cy.dataCy(selectorFormRegisterEmail)
+ .find('input')
+ .type(testEmail);
+ cy.dataCy(selectorFormRegisterPasswordInput).type(testPassword);
+ cy.dataCy(selectorFormRegisterPasswordConfirmInput).type(
+ testPassword,
+ );
+ // accept privacy policy
+ cy.dataCy(selectorFormRegisterPrivacyConsent)
+ .should('be.visible')
+ .click('topLeft');
+ cy.dataCy(selectorFormRegisterSubmit).click();
+ // wait for request to complete
+ cy.wait('@registerRequest').then((interception) => {
+ expect(interception.request.body).to.deep.equal(
+ registerRequestBody,
+ );
+ expect(interception.response.body).to.deep.equal(
+ registerResponse,
+ );
+ });
+ // redirect to verify email page
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ cy.wait('@emailVerificationRequest').then((interception) => {
+ expect(interception.response.body).to.deep.equal({
+ has_user_verified_email_address: false,
+ });
+ });
+
+ // test navigating to app pages (logged in and email not verified)
+ cy.visit(
+ '#' + routesConf['routes_calendar']['children']['fullPath'],
+ );
+ cy.url().should(
+ 'not.contain',
+ routesConf['routes_calendar']['children']['fullPath'],
+ );
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ // results page
+ cy.visit('#' + routesConf['results']['path']);
+ cy.url().should('not.contain', routesConf['results']['path']);
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ // community page
+ cy.visit('#' + routesConf['community']['path']);
+ cy.url().should('not.contain', routesConf['community']['path']);
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ // prizes page
+ cy.visit('#' + routesConf['prizes']['path']);
+ cy.url().should('not.contain', routesConf['prizes']['path']);
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ // profile page
+ cy.visit('#' + routesConf['profile_details']['path']);
+ cy.url().should(
+ 'not.contain',
+ routesConf['profile_details']['path'],
+ );
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ // test navigating to login and register page (this is NOT allowed when email not verified and logged in)
+ cy.visit('#' + routesConf['login']['path']);
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ cy.visit('#' + routesConf['register']['path']);
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ // test navigating to verify email page (this is allowed when email not verified)
+ cy.visit('#' + routesConf['verify_email']['path']);
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ });
+ });
+ });
+ });
+ });
+
+ it('redirects to login page after registering and verifying email', () => {
+ cy.get('@i18n').then((i18n) => {
+ cy.get('@config').then((config) => {
+ // variables
+ const {
+ apiBase,
+ apiDefaultLang,
+ urlApiHasUserVerifiedEmail,
+ urlApiRegister,
+ } = config;
+ const apiBaseUrl = getApiBaseUrlWithLang(
+ null,
+ apiBase,
+ apiDefaultLang,
+ i18n,
+ );
+ const apiRegisterUrl = `${apiBaseUrl}${urlApiRegister}`;
+ const apiEmailVerificationUrl = `${apiBaseUrl}${urlApiHasUserVerifiedEmail}`;
+ cy.fixture('registerResponse.json').then((registerResponse) => {
+ // intercept register request
+ cy.intercept('POST', apiRegisterUrl, {
+ statusCode: httpSuccessfullStatus,
+ body: registerResponse,
+ }).as('registerRequest');
+ // intercept email verification request
+ cy.intercept('GET', apiEmailVerificationUrl, {
+ statusCode: httpSuccessfullStatus,
+ body: { has_user_verified_email_address: false },
+ }).as('emailVerificationRequest');
+ // fill form
+ cy.dataCy(selectorFormRegisterEmail).find('input').type(testEmail);
+ cy.dataCy(selectorFormRegisterPasswordInput).type(testPassword);
+ cy.dataCy(selectorFormRegisterPasswordConfirmInput).type(
+ testPassword,
+ );
+ // accept privacy policy
+ cy.dataCy(selectorFormRegisterPrivacyConsent)
+ .should('be.visible')
+ .click('topLeft');
+ cy.dataCy(selectorFormRegisterSubmit).click();
+ // wait for request to complete
+ cy.wait('@registerRequest').then((interception) => {
+ expect(interception.request.body).to.deep.equal(
+ registerRequestBody,
+ );
+ expect(interception.response.body).to.deep.equal(
+ registerResponse,
+ );
+ });
+ });
+ // check success message
+ cy.get('@i18n').then((i18n) => {
+ cy.contains(i18n.global.t('register.apiMessageSuccess')).should(
+ 'be.visible',
+ );
+ });
+ cy.clock().then((clock) => {
+ clock.setSystemTime(systemTime);
+ // redirect to verify email page
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ // wait for email verification request to complete
+ cy.wait('@emailVerificationRequest').then((interception) => {
+ expect(interception.response.body).to.deep.equal({
+ has_user_verified_email_address: false,
+ });
+ });
+ cy.url().should('contain', routesConf['verify_email']['path']);
+ // update email verification request
+ // intercept email verification request
+ cy.intercept('GET', apiEmailVerificationUrl, {
+ statusCode: httpSuccessfullStatus,
+ body: { has_user_verified_email_address: true },
+ }).as('emailVerificationRequest');
+ cy.reload();
+ cy.wait('@emailVerificationRequest').then((interception) => {
+ expect(interception.response.body).to.deep.equal({
+ has_user_verified_email_address: true,
+ });
+ });
+ // redirected to home page
+ cy.url().should('contain', routesConf['home']['path']);
+ });
+ });
+ });
+ });
});
});
diff --git a/test/cypress/e2e/unit_tests_app_code.spec.cy.js b/test/cypress/e2e/unit_tests_app_code.spec.cy.js
index 551d82a08..9bf4395c5 100644
--- a/test/cypress/e2e/unit_tests_app_code.spec.cy.js
+++ b/test/cypress/e2e/unit_tests_app_code.spec.cy.js
@@ -1,18 +1,3 @@
-import { getString } from 'src/utils';
-
-describe('Unit Test Application Code', function () {
- before(() => {
- // check if the import worked correctly
- expect(getString, 'getString').to.be.a('function');
- });
- context('src/utils/utils.js', () => {
- it('get string', function () {
- const str = 'Test text';
- expect(getString(str)).to.eq(str);
- });
- });
-});
-
describe('Component Boilerplate function', function () {
const componentName = 'TestComponent';
const componentOutputDir = 'global';
@@ -41,6 +26,7 @@ describe('Component Boilerplate function', function () {
'fileExists',
`${componentsDir}/${componentOutputDir}/${componentName}.vue`,
).then((exists) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(exists).to.be.true;
});
});
@@ -51,6 +37,7 @@ describe('Component Boilerplate function', function () {
'fileExists',
`${componentsDir}/__tests__/${componentName}.cy.js`,
).then((exists) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(exists).to.be.true;
});
});
diff --git a/test/cypress/fixtures/registerResponse.json b/test/cypress/fixtures/registerResponse.json
new file mode 100644
index 000000000..ba3a3768d
--- /dev/null
+++ b/test/cypress/fixtures/registerResponse.json
@@ -0,0 +1,7 @@
+{
+ "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzI3MjEwMTYzLCJqdGkiOiJiMzY5M2I1ZTU3OWE0MDZhOWUyNWE0ZTQ3YzFmMjQ4NiIsInVzZXJfaWQiOjE4OTc2MX0.jAfrS_1R2FnoNcZmYUEoOqPq7evNLP7KzPAOFmuHu88",
+ "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcyNzI5NjI2MywianRpIjoiNzYzNGIxYzBiYTdiNDQ0Zjk0ZTZmNTA5M2E1MDM3MDYiLCJ1c2VyX2lkIjoxODk3NjF9.6J_L4wVjPN3bKAOU-UcvxhrIBirqLVrgi5AZefsqrt0",
+ "user": {
+ "email": "foo@bar.org"
+ }
+}
diff --git a/test/cypress/support/commonTests.ts b/test/cypress/support/commonTests.ts
index f61d8d8b9..84025109a 100644
--- a/test/cypress/support/commonTests.ts
+++ b/test/cypress/support/commonTests.ts
@@ -181,3 +181,10 @@ export const httpInternalServerErrorStatus = 500;
export const httpTooManyRequestsStatus = 429;
export const httpTooManyRequestsStatusMessage = `HTTP status code ${httpTooManyRequestsStatus} Too Many Requests ("rate limiting").`;
export const failOnStatusCode = false;
+
+// access token expiration time: Tuesday 24. September 2024 22:36:03
+const fixtureTokenExpiration = new Date('2024-09-24T22:36:03Z');
+const fixtureTokenExpirationTime = fixtureTokenExpiration.getTime() / 1000;
+const timeUntilRefresh = 60;
+export const timeUntilExpiration = timeUntilRefresh * 2;
+export const systemTime = fixtureTokenExpirationTime - timeUntilExpiration; // 2 min before JWT expires