#54 user accounts - account management
This commit is contained in:
parent
5b97cbab95
commit
63326225a4
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<section>
|
||||
<Alert type="danger" :message="error"/>
|
||||
|
||||
<form @submit.prevent="changeUsername">
|
||||
<h3 class="h6"><T>user.account.changeUsername.header</T></h3>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" v-model="username"
|
||||
required minlength="4" maxlength="16"/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-primary">
|
||||
<T>user.account.changeUsername.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<h3 class="h6"><T>user.account.changeEmail.header</T></h3>
|
||||
<p>{{ email }}</p>
|
||||
</div>
|
||||
|
||||
<p v-if="$user().roles === 'admin'">
|
||||
<span class="badge badge-primary"><T>user.account.admin</T></span>
|
||||
</p>
|
||||
|
||||
<button class="btn btn-outline-secondary btn-sm" @click="logout">
|
||||
<Icon v="sign-out"/>
|
||||
<T>user.logout</T>
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: this.$user().username,
|
||||
email: this.$user().email,
|
||||
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async changeUsername() {
|
||||
await this.post(`/user/change-username`, {
|
||||
username: this.username
|
||||
}, {
|
||||
headers: {...this.$auth()},
|
||||
});
|
||||
},
|
||||
async post(url, data, options = {}) {
|
||||
this.error = '';
|
||||
|
||||
const response = await this.$axios.$post(url, data, options);
|
||||
|
||||
if (response.error) {
|
||||
this.error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('setToken', response.token);
|
||||
this.$cookies.set('token', this.$store.state.token);
|
||||
},
|
||||
logout() {
|
||||
this.$store.commit('setToken', null);
|
||||
this.$cookies.removeAll();
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div v-if="message" :class="'alert alert-' + type">
|
||||
<p class="mb-0">
|
||||
<Icon v="exclamation-triangle"/>
|
||||
<T>{{message}}</T>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
type: { required: true },
|
||||
message: {},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<section>
|
||||
<Alert type="danger" :message="error"/>
|
||||
|
||||
<div v-if="token === null">
|
||||
<form @submit.prevent="login">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" v-model="usernameOrEmail"
|
||||
:placeholder="$t('user.login.placeholder')" autofocus required/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary">
|
||||
<Icon v="sign-in"/>
|
||||
<T>user.login.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else-if="payload && !payload.code">
|
||||
<div class="alert alert-success">
|
||||
<p class="mb-0">
|
||||
<Icon v="envelope-open-text"/>
|
||||
<T :params="{email: payload.email}">user.login.emailSent</T>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="validate">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control text-center" v-model="code"
|
||||
placeholder="000000" autofocus required minlength="0" maxlength="6"
|
||||
inputmode="numeric" pattern="[0-9]{6}" autocomplete="one-time-code"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary">
|
||||
<Icon v="key"/>
|
||||
<T>user.code.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
token: null,
|
||||
usernameOrEmail: '',
|
||||
code: '',
|
||||
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
payload() {
|
||||
if (!this.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.$store.commit('setToken', this.token);
|
||||
this.$cookies.set('token', this.$store.state.token);
|
||||
|
||||
return jwt.verify(this.token, process.env.PUBLIC_KEY, {
|
||||
algorithm: 'RS256',
|
||||
audience: process.env.BASE_URL,
|
||||
issuer: process.env.BASE_URL,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
await this.post(`/user/init`, {
|
||||
usernameOrEmail: this.usernameOrEmail
|
||||
});
|
||||
},
|
||||
async validate() {
|
||||
await this.post(`/user/validate`, {
|
||||
code: this.code
|
||||
}, {
|
||||
headers: {
|
||||
authorization: 'Bearer ' + this.token,
|
||||
},
|
||||
});
|
||||
},
|
||||
async post(url, data, options = {}) {
|
||||
this.error = '';
|
||||
|
||||
const response = await this.$axios.$post(url, data, options);
|
||||
|
||||
this.usernameOrEmail = '';
|
||||
this.code = '';
|
||||
|
||||
if (response.error) {
|
||||
this.error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
this.token = response.token;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -596,7 +596,17 @@ user:
|
|||
code:
|
||||
action: 'Sprawdź'
|
||||
invalid: 'Kod nieprawidłowy.'
|
||||
|
||||
account:
|
||||
changeUsername:
|
||||
header: 'Nazwa użytkownika'
|
||||
action: 'Zmień'
|
||||
invalid: 'Nazwa użytkownika musi mieć od 4 do 16 znaków i zawierać wyłącznie cyfry, litery, kropkę, myślnik i podłogę.'
|
||||
taken: 'Ta nazwa użytkownika jest zajęta.'
|
||||
changeEmail:
|
||||
header: 'Adres email'
|
||||
action: 'Zmień'
|
||||
admin: 'Adminię'
|
||||
logout: 'Wyloguj'
|
||||
|
||||
share: 'Udostępnij'
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import t from "../src/translator";
|
||||
|
||||
export default ({app, store}) => {
|
||||
const token = app.$cookies.get('token');
|
||||
if (token) {
|
||||
|
@ -6,4 +9,11 @@ export default ({app, store}) => {
|
|||
app.$cookies.removeAll();
|
||||
}
|
||||
}
|
||||
|
||||
Vue.prototype.$user = _ => store.state.user;
|
||||
Vue.prototype.$auth = _ => {
|
||||
return store.state.token ? {
|
||||
authorization: 'Bearer ' + store.state.token,
|
||||
} : {};
|
||||
};
|
||||
}
|
||||
|
|
118
routes/user.vue
118
routes/user.vue
|
@ -5,129 +5,15 @@
|
|||
<T>user.headerLong</T>
|
||||
</h2>
|
||||
|
||||
<section>
|
||||
<div v-if="error" class="alert alert-danger">
|
||||
<p class="mb-0">
|
||||
<Icon v="exclamation-triangle"/>
|
||||
<T>{{error}}</T>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="$store.state.user">
|
||||
Logged in as <strong>@{{$store.state.user.username}}</strong>.
|
||||
|
||||
<button class="btn btn-outline-secondary btn-sm" @click="logout">
|
||||
<Icon v="sign-out"/>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="token === null">
|
||||
<form @submit.prevent="login">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" v-model="usernameOrEmail"
|
||||
:placeholder="$t('user.login.placeholder')" autofocus required/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary">
|
||||
<Icon v="sign-in"/>
|
||||
<T>user.login.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else-if="payload && !payload.code">
|
||||
<div class="alert alert-success">
|
||||
<p class="mb-0">
|
||||
<Icon v="envelope-open-text"/>
|
||||
<T :params="{email: payload.email}">user.login.emailSent</T>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="validate">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control text-center" v-model="code"
|
||||
placeholder="000000" autofocus required minlength="0" maxlength="6"
|
||||
inputmode="numeric" pattern="[0-9]{6}" autocomplete="one-time-code"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary">
|
||||
<Icon v="key"/>
|
||||
<T>user.code.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<Account v-if="$user()"/>
|
||||
<Login v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { head } from "../src/helpers";
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
token: null,
|
||||
usernameOrEmail: '',
|
||||
code: '',
|
||||
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
payload() {
|
||||
if (!this.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.$store.commit('setToken', this.token);
|
||||
this.$cookies.set('token', this.$store.state.token);
|
||||
|
||||
return jwt.verify(this.token, process.env.PUBLIC_KEY, {
|
||||
algorithm: 'RS256',
|
||||
audience: process.env.BASE_URL,
|
||||
issuer: process.env.BASE_URL,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
await this.post(`/user/init`, {
|
||||
usernameOrEmail: this.usernameOrEmail
|
||||
});
|
||||
},
|
||||
async validate() {
|
||||
await this.post(`/user/validate`, {
|
||||
code: this.code
|
||||
}, {
|
||||
headers: {
|
||||
authorization: 'Bearer ' + this.token,
|
||||
},
|
||||
});
|
||||
},
|
||||
async post(url, data, options = {}) {
|
||||
this.error = '';
|
||||
|
||||
const response = await this.$axios.$post(url, data, options);
|
||||
|
||||
this.usernameOrEmail = '';
|
||||
this.code = '';
|
||||
|
||||
if (response.error) {
|
||||
this.error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
this.token = response.token;
|
||||
},
|
||||
logout() {
|
||||
this.token = null;
|
||||
this.$store.commit('setToken', null);
|
||||
this.$cookies.removeAll();
|
||||
}
|
||||
},
|
||||
head() {
|
||||
return head({
|
||||
title: this.$t('user.headerLong'),
|
||||
|
|
|
@ -8,6 +8,8 @@ const mailer = require('./mailer');
|
|||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
|
||||
|
||||
const getUser = (authorization) => {
|
||||
if (!authorization || !authorization.startsWith('Bearer ')) {
|
||||
return null;
|
||||
|
@ -105,14 +107,16 @@ const validate = async (db, user, code) => {
|
|||
return {error: 'user.code.invalid'};
|
||||
}
|
||||
|
||||
return await authenticate(db, user, authenticator);
|
||||
await invalidateAuthenticator(db, authenticator);
|
||||
|
||||
return await authenticate(db, user);
|
||||
}
|
||||
|
||||
const defaultUsername = async (db, email) => {
|
||||
const base = email.substring(0, email.indexOf('@'))
|
||||
.padEnd(4, '0')
|
||||
.substring(0, 12)
|
||||
.replace(/[^A-Za-z0-9._-]/g, '_');
|
||||
.replace(new RegExp(`[^${USERNAME_CHARS}]`, 'g'), '_');
|
||||
|
||||
let c = 0;
|
||||
while (true) {
|
||||
|
@ -125,7 +129,7 @@ const defaultUsername = async (db, email) => {
|
|||
}
|
||||
}
|
||||
|
||||
const authenticate = async (db, user, authenticator) => {
|
||||
const authenticate = async (db, user) => {
|
||||
let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${user.email}`);
|
||||
if (!dbUser) {
|
||||
dbUser = {
|
||||
|
@ -137,8 +141,6 @@ const authenticate = async (db, user, authenticator) => {
|
|||
}
|
||||
}
|
||||
|
||||
invalidateAuthenticator(db, authenticator);
|
||||
|
||||
return {
|
||||
token: jwt.sign({
|
||||
...dbUser,
|
||||
|
@ -147,6 +149,21 @@ const authenticate = async (db, user, authenticator) => {
|
|||
};
|
||||
}
|
||||
|
||||
const changeUsername = async (db, user, username) => {
|
||||
if (username.length < 4 || username.length > 16 || !username.match(new RegExp(`^[${USERNAME_CHARS}]+$`))) {
|
||||
return { error: 'user.account.changeUsername.invalid' }
|
||||
}
|
||||
|
||||
const dbUser = await db.get(SQL`SELECT * FROM users WHERE username = ${username}`);
|
||||
if (dbUser) {
|
||||
return { error: 'user.account.changeUsername.taken' }
|
||||
}
|
||||
|
||||
await db.get(SQL`UPDATE users SET username = ${username} WHERE email = ${user.email}`);
|
||||
|
||||
return await authenticate(db, user);
|
||||
}
|
||||
|
||||
export default async function (req, res, next) {
|
||||
const db = await dbConnection();
|
||||
|
||||
|
@ -158,6 +175,8 @@ export default async function (req, res, next) {
|
|||
result = await init(db, req.body.usernameOrEmail)
|
||||
} else if (req.method === 'POST' && req.url === '/validate' && req.body.code) {
|
||||
result = await validate(db, user, req.body.code);
|
||||
} else if (req.method === 'POST' && req.url === '/change-username' && user && user.authenticated && req.body.username) {
|
||||
result = await changeUsername(db, user, req.body.username);
|
||||
}
|
||||
|
||||
res.setHeader('content-type', 'application/json');
|
||||
|
|
Reference in New Issue