Skip to content

Commit c1a30c7

Browse files
author
BacLuc
committed
tmp
Signed-off-by: BacLuc <lucius.bachmann@clubpage.ch>
1 parent 83e0d24 commit c1a30c7

6 files changed

Lines changed: 449 additions & 11 deletions

File tree

e2e/tests/9-behavior-tests/reLoginDialog.e2e.spec.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ test.describe('re-login dialog', () => {
1414
// Make the camps endpoint return 401 to trigger the dialog
1515
await page.route(/\/api\/camps/, (route) => route.fulfill({ status: 401, body: '' }))
1616
await page.reload()
17-
await expect(page.getByText('Session expired')).toBeVisible()
17+
await expect(page.getByText('Sitzung abgelaufen')).toBeVisible()
1818
})
1919

2020
test('dialog appears when session expires mid-session', async ({ page }) => {
21-
await expect(page.getByText('Your session has expired')).toBeVisible()
21+
await expect(page.getByText('Sitzung abgelaufen')).toBeVisible()
2222
await expect(page).toHaveURL(/\/camps/)
2323
})
2424

@@ -27,26 +27,28 @@ test.describe('re-login dialog', () => {
2727
}) => {
2828
await page.unroute(/\/token\/refresh$/)
2929

30-
await page.getByLabel('Email').fill('test@example.com')
31-
await page.getByLabel('Password').fill('test')
30+
await page.locator('[type="email"]').fill('test@example.com')
31+
await page.locator('[type="password"]').fill('test')
3232
await page.getByRole('button', { name: /ecamp/i }).click()
3333

34-
await expect(page.getByText('Session expired')).toBeHidden({ timeout: 15000 })
34+
await expect(page.getByText('Sitzung abgelaufen')).toBeHidden({ timeout: 15000 })
3535
await expect(page).toHaveURL(/\/camps/)
3636
})
3737

3838
test('wrong credentials show an error without closing the dialog', async ({ page }) => {
39-
await page.getByLabel('Email').fill('test@example.com')
40-
await page.getByLabel('Password').fill('wrong-password')
39+
await page.locator('[type="email"]').fill('test@example.com')
40+
await page.locator('[type="password"]').fill('wrong-password')
4141
await page.getByRole('button', { name: /ecamp/i }).click()
4242

43-
await expect(page.getByRole('alert')).toBeVisible()
44-
await expect(page.getByText('Session expired')).toBeVisible()
43+
await expect(page.getByText('Invalid credentials.')).toBeVisible()
44+
await expect(page.getByText('Sitzung abgelaufen')).toBeVisible()
4545
await expect(page).toHaveURL(/\/camps/)
4646
})
4747

4848
test('logout button in dialog navigates to login page', async ({ page }) => {
49-
await page.getByRole('button', { name: 'Logout' }).click()
49+
const logoutButton = page.getByRole('button', { name: 'Ausloggen' })
50+
await logoutButton.scrollIntoViewIfNeeded()
51+
await logoutButton.click()
5052

5153
await expect(page).toHaveURL(/\/login/)
5254
})
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
<template>
2+
<div>
3+
<v-alert
4+
v-if="!isProdSuffix"
5+
class="mt-2 mb-4 text-justify text-warning"
6+
variant="tonal"
7+
border="start"
8+
density="compact"
9+
style="hyphens: auto"
10+
color="warning"
11+
>
12+
<div>
13+
<i18n-t :keypath="infoTextKey" scope="global">
14+
<template #br><br /></template>
15+
</i18n-t>
16+
<v-btn
17+
variant="text"
18+
elevation="0"
19+
height="32px"
20+
class="v-btn--has-bg float-end dev-login-button text-deep-orange-darken-4"
21+
append-icon="mdi-auto-fix"
22+
@click="
23+
(event) => {
24+
email = event.shiftKey ? 'admin@example.com' : 'test@example.com'
25+
password = 'test'
26+
login()
27+
}
28+
"
29+
>
30+
Login
31+
</v-btn>
32+
</div>
33+
</v-alert>
34+
35+
<v-alert
36+
v-if="error"
37+
class="mt-2 mb-4"
38+
variant="tonal"
39+
border="start"
40+
type="error"
41+
icon="mdi-alert"
42+
>
43+
<span class="d-block">{{ error }}</span>
44+
<span class="d-block mt-1"
45+
>{{ $t('views.auth.login.passwordForgotten') }}
46+
<router-link :to="{ name: 'resetPasswordRequest' }">
47+
{{ $t('views.auth.login.resetPassword') }}
48+
</router-link>
49+
</span>
50+
<span class="d-block mt-1"
51+
>{{ $t('views.auth.login.notActivated') }}
52+
<router-link :to="{ name: 'resendActivation' }">
53+
{{ $t('views.auth.login.resendActivation') }}
54+
</router-link>
55+
</span>
56+
</v-alert>
57+
58+
<v-form @submit.prevent="login">
59+
<e-text-field
60+
id="inputEmail"
61+
v-model="email"
62+
vee-rules="email|required"
63+
autofocus
64+
:label="$t('views.auth.login.email')"
65+
name="email"
66+
path="email"
67+
append-inner-icon="mdi-account-outline"
68+
:density="$vuetify.display.xs ? 'compact' : 'default'"
69+
type="email"
70+
autocomplete="username"
71+
/>
72+
<e-text-field
73+
id="inputPassword"
74+
v-model="password"
75+
:label="$t('views.auth.login.password')"
76+
vee-rules="required"
77+
name="password"
78+
path="email"
79+
append-inner-icon="mdi-lock-outline"
80+
:density="$vuetify.display.xs ? 'compact' : 'default'"
81+
type="password"
82+
autocomplete="current-password"
83+
/>
84+
<small class="ml-2">
85+
<router-link
86+
:to="{ name: 'resetPasswordRequest' }"
87+
tabindex="100"
88+
style="color: gray"
89+
>
90+
{{ $t('views.auth.login.passwordForgotten') }}
91+
</router-link>
92+
</small>
93+
<v-btn
94+
type="submit"
95+
:color="email && password ? 'blue-darken-2' : 'blue-lighten-4'"
96+
block
97+
:disabled="!(email && password) || authenticationInProgress"
98+
:size="$vuetify.display.smAndUp && 'large'"
99+
height="50"
100+
variant="outlined"
101+
class="my-4 pa-2 ec-login-button"
102+
>
103+
<v-progress-circular v-if="authenticationInProgress" indeterminate size="24" />
104+
<v-icon v-else size="large">$ecamp</v-icon>
105+
<v-spacer />
106+
<span>{{ $t('views.auth.login.provider.ecamp') }}</span>
107+
<v-spacer />
108+
<icon-spacer />
109+
</v-btn>
110+
</v-form>
111+
112+
<horizontal-rule :label="$t('views.auth.login.or')" />
113+
114+
<div class="openid-buttons">
115+
<v-btn
116+
dark
117+
color="#91697f"
118+
:size="$vuetify.display.smAndUp && 'x-large'"
119+
variant="text"
120+
@click="$auth.loginPbsMiData()"
121+
>
122+
<v-icon class="my-1" color="#521d3a">$pbs</v-icon>
123+
<span class="text-grey-darken-2 text-body-2 font-weight-medium">{{
124+
$t('views.auth.login.provider.midata')
125+
}}</span>
126+
</v-btn>
127+
<v-btn
128+
dark
129+
color="green"
130+
:size="$vuetify.display.smAndUp && 'x-large'"
131+
variant="text"
132+
@click="$auth.loginCeviDB()"
133+
>
134+
<v-icon class="my-1">$cevi</v-icon>
135+
<span class="text-grey-darken-2 text-body-2 font-weight-medium">{{
136+
$t('views.auth.login.provider.cevidb')
137+
}}</span>
138+
</v-btn>
139+
<v-btn
140+
dark
141+
color="blue"
142+
:size="$vuetify.display.smAndUp && 'x-large'"
143+
variant="text"
144+
class="jubla-btn"
145+
@click="$auth.loginJublaDB()"
146+
>
147+
<v-icon size="24">$jubla</v-icon>
148+
<span class="text-grey-darken-2 text-body-2 font-weight-medium">{{
149+
$t('views.auth.login.provider.jubladb')
150+
}}</span>
151+
</v-btn>
152+
<v-btn
153+
dark
154+
:size="$vuetify.display.smAndUp && 'x-large'"
155+
color="blue-grey-lighten-3"
156+
variant="text"
157+
@click="$auth.loginGoogle()"
158+
>
159+
<v-icon class="my-1">$google</v-icon>
160+
<span class="text-grey-darken-2 text-body-2 font-weight-medium">{{
161+
$t('views.auth.login.provider.google')
162+
}}</span>
163+
</v-btn>
164+
<small class="w-100">
165+
<i18n-t
166+
keypath="views.auth.login.acceptTermsOfServiceOnOAuthLogin"
167+
tag="p"
168+
class="text-grey-darken-2 text-center w-100 mt-2"
169+
scope="global"
170+
style="hyphens: auto"
171+
>
172+
<template #termsOfServiceLink>
173+
<a :href="termsOfServiceLink" target="_blank" style="color: gray">{{
174+
$t('views.auth.login.termsOfServiceLink')
175+
}}</a>
176+
</template>
177+
</i18n-t>
178+
</small>
179+
</div>
180+
</div>
181+
</template>
182+
183+
<script>
184+
import HorizontalRule from '@/components/layout/HorizontalRule.vue'
185+
import IconSpacer from '@/components/layout/IconSpacer.vue'
186+
import { serverErrorToString } from '@/helpers/serverError'
187+
import { parseTemplate } from 'url-template'
188+
import { getEnv } from '@/environment.js'
189+
190+
const LOGIN_INFO_TEXT_KEY = getEnv().LOGIN_INFO_TEXT_KEY
191+
192+
export default {
193+
name: 'LoginForm',
194+
components: { HorizontalRule, IconSpacer },
195+
emits: ['success'],
196+
data() {
197+
return {
198+
email: '',
199+
password: '',
200+
error: null,
201+
authenticationInProgress: false,
202+
}
203+
},
204+
computed: {
205+
infoTextSuffix() {
206+
return LOGIN_INFO_TEXT_KEY
207+
},
208+
isProdSuffix() {
209+
return this.infoTextSuffix === 'prod'
210+
},
211+
infoTextKey() {
212+
return `views.auth.login.infoText.${this.infoTextSuffix ?? 'prod'}`
213+
},
214+
termsOfServiceLink() {
215+
return (
216+
parseTemplate(getEnv().TERMS_OF_SERVICE_LINK_TEMPLATE || '').expand({
217+
lang: this.$store.state.lang.language.substring(0, 2),
218+
}) || false
219+
)
220+
},
221+
},
222+
methods: {
223+
async login() {
224+
this.authenticationInProgress = true
225+
this.error = null
226+
this.$auth
227+
.login(this.email, this.password)
228+
.then(() => {
229+
this.password = ''
230+
this.$emit('success')
231+
})
232+
.catch((e) => {
233+
this.error = serverErrorToString(e)
234+
})
235+
.finally(() => {
236+
this.authenticationInProgress = false
237+
})
238+
},
239+
},
240+
}
241+
</script>
242+
243+
<style lang="scss" scoped>
244+
.openid-buttons {
245+
display: flex;
246+
flex-wrap: wrap;
247+
gap: 0.5rem;
248+
249+
:deep(.v-btn) {
250+
height: auto;
251+
min-width: auto;
252+
padding: 4px 0;
253+
flex-grow: 1;
254+
}
255+
256+
:deep(.v-btn__content) {
257+
flex-direction: column;
258+
}
259+
}
260+
261+
.dev-login-button {
262+
background-color: #f7e4cc !important;
263+
margin-top: 12px;
264+
margin-bottom: -12px;
265+
translate: 0 -12px;
266+
}
267+
268+
/* eslint-disable-next-line vue-scoped-css/no-unused-selector */
269+
.ec-login-button.v-btn--disabled {
270+
color: rgba(0, 0, 0, 0.26) !important;
271+
opacity: 1;
272+
}
273+
274+
.ec-login-button :deep(.v-btn__overlay) {
275+
opacity: 0.08;
276+
}
277+
278+
.ec-login-button :deep(.v-btn__content) {
279+
width: 100%;
280+
}
281+
282+
.jubla-btn :deep(.v-btn__content) {
283+
height: 100%;
284+
justify-content: space-between;
285+
}
286+
</style>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<template>
2+
<v-dialog v-model="isAuthRequired" persistent max-width="500">
3+
<v-card>
4+
<v-card-title>{{ $t('global.reLoginDialog.title') }}</v-card-title>
5+
<v-card-text>
6+
<p class="mb-4">{{ $t('global.reLoginDialog.description') }}</p>
7+
<login-form @success="onLoginSuccess" />
8+
</v-card-text>
9+
<v-card-actions>
10+
<v-spacer />
11+
<v-btn variant="text" @click="logOut">{{ $t('global.button.logout') }}</v-btn>
12+
</v-card-actions>
13+
</v-card>
14+
</v-dialog>
15+
</template>
16+
17+
<script>
18+
import LoginForm from '@/components/auth/LoginForm.vue'
19+
import { resolveReAuth, rejectReAuth } from '@/plugins/auth.js'
20+
import { mapGetters } from 'vuex'
21+
22+
export default {
23+
name: 'ReLoginDialog',
24+
components: { LoginForm },
25+
computed: {
26+
...mapGetters(['isAuthRequired']),
27+
},
28+
methods: {
29+
onLoginSuccess() {
30+
resolveReAuth()
31+
},
32+
async logOut() {
33+
rejectReAuth()
34+
await this.$auth.logout()
35+
},
36+
},
37+
}
38+
</script>

frontend/src/plugins/auth.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function rejectReAuth() {
4444

4545
export function isRefreshLikelyPossible() {
4646
const refreshTokenExpiry = getRefreshTokenExpiresAt()
47-
if (refreshTokenExpiry === 0) return true
47+
if (refreshTokenExpiry === 0) return false
4848
return Date.now() < refreshTokenExpiry
4949
}
5050

@@ -112,6 +112,9 @@ export async function initRefresh() {
112112
}
113113
let refreshedSuccessfully = false
114114
if (!isLoggedIn()) {
115+
if (getRefreshTokenExpiresAt() === 0) {
116+
return
117+
}
115118
try {
116119
await refreshAuthTokenSingleton()
117120
} catch {

0 commit comments

Comments
 (0)