Split authentication & newsletter subscription
Deploy Spot / deploy (push) Successful in 32s

This commit is contained in:
2026-08-10 16:41:20 +02:00
parent 3e3c3ee821
commit 462502cf9f
19 changed files with 628 additions and 481 deletions
+5 -5
View File
@@ -115,7 +115,7 @@ export default {
</script>
<template>
<div id="admin">
<a name="back" class="button" :href="getPrevAnchor()"><SpotIcon :icon="'back'" :text="l('action.back')" /></a>
<a name="back" class="button" :href="getPrevAnchor()"><SpotIcon :icon="'back'" :text="l('action.back')" /></a>
<h1>{{ l('project.plural') }}</h1>
<div>
<table>
@@ -195,27 +195,27 @@ export default {
</tbody>
</table>
</div>
<h1>{{ l('user.active') }}</h1>
<h1>{{ l('user.subscribed') }}</h1>
<div>
<table>
<thead>
<tr>
<th>{{ l('user.id') }}</th>
<th>{{ l('user.name') }}</th>
<th>{{ l('user.email') }}</th>
<th>{{ l('user.language') }}</th>
<th>{{ l('time.zone') }}</th>
<th>{{ l('user.clearance') }}</th>
<th>{{ l('action.delete') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="user in elems.user">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td class="left">{{ user.name }}</td>
<td class="left">{{ user.email }}</td>
<td>{{ user.language }}</td>
<td>{{ user.timezone }}</td>
<td><AdminInput :type="'number'" :name="'clearance'" :elem="user" /></td>
<td><SpotButton :icon="'close'" iconSize="lg" @click="deleteElem(user)" /></td>
</tr>
</tbody>
</table>
+113
View File
@@ -0,0 +1,113 @@
<script>
import SpotButton from '@components/spotButton';
import SpotIcon from '@components/spotIcon';
export default {
components: {
SpotButton,
SpotIcon
},
data() {
return {
feedbacks: [],
loginLoading: false,
newsletterLoading: false,
passwordMode: false,
settingPassword: false,
password: '',
passwordConfirmation: ''
};
},
computed: {
buttonClasses() {
return [
'manage',
this.action,
this.loginLoading?'loading':''
].filter(n => n).join(' ');
},
loggedIn() {
return this.user.id_user > 0;
},
action() {
return this.loggedIn?'logout':'login';
},
status() {
return this.loggedIn?(this.user.subscribed?'subscribed':(this.user.name?'logged_in_named':'logged_in')):'logged_out';
}
},
inject: ['api', 'lang', 'user'],
methods: {
manageSubscription() {
if(this.newsletterLoading) return;
this.newsletterLoading = true;
this.api.request(this.user.subscribed?'subscribe':'unsubscribe', {}, 'POST')
.then((asResponse) => {
this.user.setInfo(asResponse.data);
this.feedbacks.push({type:asResponse.result, msg:asResponse.desc});
})
.catch((sDesc) => {
this.user.subscribed = !this.user.subscribed;
this.feedbacks.push({type:'error', msg:sDesc?.message || sDesc});
})
.finally(() => {this.newsletterLoading = false;});
},
manageLogin() {
if(this.loginLoading) return;
var regexEmail = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if(!regexEmail.test(this.user.email)) this.feedbacks.push({type:'error', 'msg':this.lang.get('account.invalid_email')});
else if(this.settingPassword && this.password !== this.passwordConfirmation) this.feedbacks.push({type:'error', 'msg':this.lang.get('account.password_mismatch')});
else {
const sAction = this.action;
this.loginLoading = true;
this.api.request(sAction, {'email': this.user.email, 'password': this.password, 'name': this.user.name}, 'POST')
.then((asResponse) => {
this.feedbacks.push({type: asResponse.result, msg: asResponse.desc});
this.user.setInfo(asResponse.data);
this.passwordMode = false;
this.settingPassword = false;
this.password = '';
this.passwordConfirmation = '';
})
.catch((oError) => {
switch(oError.descKey) {
case 'account.set_password':
this.passwordMode = true;
this.settingPassword = true;
this.feedbacks.push({type:'warning', msg:oError.message});
break;
case 'account.password_required':
this.passwordMode = true;
this.feedbacks.push({type:'success', msg:oError.message});
break;
default:
this.feedbacks.push({type:'error', msg:oError.message});
}
})
.finally(() => {this.loginLoading = false;});
}
}
}
}
</script>
<template>
<div class="sub-section status">{{ lang.get('account.status.'+status, this.user.name) }}</div>
<div class="sub-section account-login">
<input type="email" name="email" id="email" :placeholder="lang.get('account.email_placeholder')" v-model="user.email" :disabled="loginLoading || loggedIn || passwordMode" @keyup.enter="manageLogin" />
<input v-if="passwordMode" type="password" name="password" id="password" :placeholder="lang.get('account.password')" v-model="password" :disabled="loginLoading" :autocomplete="settingPassword?'new-password':'current-password'" @keyup.enter="manageLogin" />
<input v-if="settingPassword" type="password" name="password_confirmation" id="password-confirmation" :placeholder="lang.get('account.confirm_password')" v-model="passwordConfirmation" :disabled="loginLoading" autocomplete="new-password" @keyup.enter="manageLogin" />
<SpotButton :classes="buttonClasses" :title="lang.get('account.'+action)" :icon="action" @click="manageLogin" />
</div>
<div class="sub-section newsletter radio" v-if="loggedIn">
<input type="checkbox" id="account-newsletter" v-model="user.subscribed" :true-value="1" :false-value="0" :disabled="newsletterLoading" @change="manageSubscription" />
<label for="account-newsletter">{{ lang.get('account.newsletter') }}</label>
</div>
<div v-if="feedbacks.length" class="sub-section feedback">
<p v-for="feedback in feedbacks" :key="feedback.type + '-' + feedback.msg" :class="feedback.type">
<SpotIcon :icon="feedback.type" :text="feedback.msg" />
</p>
</div>
</template>
-66
View File
@@ -1,66 +0,0 @@
<script>
import SpotButton from '@components/spotButton';
import SpotIcon from '@components/spotIcon';
export default {
components: {
SpotButton,
SpotIcon
},
data() {
return {
feedbacks: [],
loading: false
};
},
computed: {
buttonClasses() {
return [
'manage',
this.action,
this.loading?'loading':''
].filter(n => n).join(' ');
},
subscribed() {
return this.user.id_user > 0;
},
action() {
return this.subscribed?'unsubscribe':'subscribe';
}
},
inject: ['api', 'lang', 'user'],
methods: {
manage() {
if(this.loading) return;
var regexEmail = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if(!regexEmail.test(this.user.email)) this.feedbacks.push({type:'error', 'msg':this.lang.get('newsletter.invalid_email')});
else {
const sAction = this.action;
this.loading = true;
this.api.request(sAction, {'email': this.user.email, 'name': this.user.name}, 'POST')
.then((asResponse) => {
this.feedbacks.push({type: asResponse.result, msg: asResponse.desc});
this.user.setInfo(asResponse.data);
})
.catch((sDesc) => {this.feedbacks.push({type:'error', msg:sDesc?.message || sDesc});})
.finally(() => {this.loading = false;});
}
}
}
}
</script>
<template>
<div class="newsletter-form">
<input type="email" name="email" id="email" :placeholder="lang.get('newsletter.email_placeholder')" v-model="user.email" :disabled="loading || subscribed" />
<SpotButton :classes="buttonClasses" :title="lang.get('newsletter.'+action)" :icon="action" @click="manage" />
</div>
<div class="feedback">
<p v-for="feedback in feedbacks" :key="feedback.type + '-' + feedback.msg" :class="feedback.type">
<SpotIcon :icon="feedback.type" :text="feedback.msg" />
</p>
</div>
{{ lang.get('newsletter.'+(subscribed?'subscribed':'unsubscribed')+'_desc') }}
</template>
+5 -5
View File
@@ -2,14 +2,14 @@
import Simplebar from 'simplebar-vue';
import SpotIcon from '@components/spotIcon';
import ProjectNewsletter from '@components/projectNewsletter';
import ProjectAccount from '@components/projectAccount';
import logoIconUrl from '@images/icons/favicon.svg';
import logoTitleUrl from '@images/logo_title.svg';
export default {
components: {
SpotIcon,
ProjectNewsletter,
ProjectAccount,
Simplebar
},
props: {
@@ -125,10 +125,10 @@ export default {
</div>
</div>
</div>
<div class="settings-section settings-box newsletter">
<h1><SpotIcon :icon="'newsletter'" width="fixed" :text="lang.get('newsletter.title')" /></h1>
<div class="settings-section settings-box account">
<h1><SpotIcon :icon="'account'" width="fixed" :text="lang.get('account.title')" /></h1>
<div class="settings-section-body">
<ProjectNewsletter />
<ProjectAccount />
</div>
</div>
<div class="settings-section settings-box admin" v-if="user.hasClearance(consts.clearances.admin)">
+4 -1
View File
@@ -49,10 +49,13 @@ export default class Api {
}
const oResponse = await oRequest.json();
oResponse.descKey = this.lang.getLangKey(oResponse.desc);
oResponse.desc = this.lang.parse(oResponse.desc);
if(oResponse.result == this.errorCode) {
throw oResponse.desc;
const oError = new Error(oResponse.desc);
oError.descKey = oResponse.descKey;
throw oError;
}
return oResponse;
+7 -3
View File
@@ -32,6 +32,9 @@ import {
faLayerGroup,
faLink,
faLocationPin,
faRightFromBracket,
faRightToBracket,
faUser,
faMagnifyingGlass,
faMagnifyingGlassLocation,
faMapLocationDot,
@@ -47,7 +50,6 @@ import {
faTemperatureThreeQuarters,
faTriangleExclamation,
faVideo,
faWifi,
faWind
} from '@fortawesome/free-solid-svg-icons';
@@ -100,8 +102,10 @@ const ICONS = {
download: faFileArrowDown,
start: faCirclePlay,
/* Admin */
newsletter: faWifi,
/* Settings */
account: faUser,
login: faRightToBracket,
logout: faRightFromBracket,
project: faPersonHiking,
unsubscribe: faCircleXmark,
credits: faPaw,
+6 -4
View File
@@ -23,9 +23,11 @@ export default class Lang {
}
parse(message = '') {
if(this.prefix && typeof message === 'string' && message.startsWith(this.prefix)) {
return this.get(message.slice(this.prefix.length));
}
return message;
let sLangKey = this.getLangKey(message);
return (sLangKey != '')?this.get(sLangKey):message;
}
getLangKey(message = '') {
return (this.prefix && typeof message === 'string' && message.startsWith(this.prefix))?message.slice(this.prefix.length):'';
}
}
+4 -1
View File
@@ -179,7 +179,10 @@ h2 {
.feedback {
p {
margin: 0 0 var.$block-spacing 0;
margin: 0;
& + p {
margin-top: var.$block-spacing;
}
&.error {
color: color.$error;
}
+4
View File
@@ -37,6 +37,10 @@
color: color.$default-hover;
}
}
&.left {
text-align: left;
}
}
}
}
+1 -1
View File
@@ -110,7 +110,7 @@ $panel-actual-width: min($panel-width, #{$panel-width-max});
color: color.$default-inv;
&:hover, &:hover a, &:hover a:visited {
background-color: color.$default-bg;
background-color: color.$default-bg-light;
color: color.$default;
}
+9 -5
View File
@@ -33,6 +33,7 @@
margin-top: var.$block-spacing - var.$elem-spacing; //Gap counts for 1 $elem-spacing
font-size: 0.8em;
color: color.$subtitle;
text-align: center;
.find-me-spot {
color: color.$spot;
@@ -41,7 +42,6 @@
}
}
.settings-sections {
flex: 1 1 auto;
overflow: auto;
@@ -97,16 +97,20 @@
}
}
}
.sub-section + .sub-section {
margin-top: var.$block-spacing;
}
}
&.newsletter {
.newsletter-form {
&.account {
.account-login {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: var.$block-spacing;
margin-bottom: var.$block-spacing;
input#email {
input {
flex: 1 1 auto;
min-width: 0;