diff --git a/locale/pl/translations.suml b/locale/pl/translations.suml
index 1c3b0c2e..d5d3d434 100644
--- a/locale/pl/translations.suml
+++ b/locale/pl/translations.suml
@@ -521,6 +521,21 @@ support:
user:
header: 'Konto'
headerLong: 'Twoje konto'
+ tokenExpired: 'Sesja wygasła. Odśwież stronę i spróbuj ponownie.'
+ login:
+ placeholder: 'Email (lub nazwa użytkownika, jeśli już posiadasz konto)'
+ action: 'Zaloguj'
+ emailSent: 'Na adres %email% wysłałośmy email z sześciocyfrowym kodem. Wpisz go poniżej. Kod jest jednorazowy i ważny przez 15 minut.'
+ userNotFound: 'Użytkownik nie został znaleziony.'
+ email:
+ subject: 'Twój kod logowania to %code%'
+ content: |
+ Aby potwierdzić swój adres email, użyj kodu: %code%.
+
+ Jeśli nie zamawiałxś tego kodu, po prostu zignoruj tę wiadomość.
+ code:
+ action: 'Sprawdź'
+ invalid: 'Kod nieprawidłowy.'
share: 'Udostępnij'
diff --git a/migrations/002-users.sql b/migrations/002-users.sql
index 13eb33de..d461d18c 100644
--- a/migrations/002-users.sql
+++ b/migrations/002-users.sql
@@ -10,7 +10,7 @@ CREATE TABLE users (
CREATE TABLE authenticators (
id TEXT NOT NULL PRIMARY KEY,
- userId TEXT NOT NULL,
+ userId TEXT,
type TEXT NOT NULL,
payload TEXT NOT NULL,
validUntil INTEGER,
diff --git a/routes/user.vue b/routes/user.vue
index 56035c19..a42f0bee 100644
--- a/routes/user.vue
+++ b/routes/user.vue
@@ -2,14 +2,61 @@
- user.header
+ user.headerLong
+
+
+
+ Logged in as {{payload.uid}} .
+
+
+
+
+
+
+ user.login.emailSent
+
+
+
+
+
+
{{JSON.stringify(token)}}
{{JSON.stringify(payload, null, 4)}}
-
Zaloguj
@@ -21,7 +68,11 @@
data() {
return {
token: null,
- }
+ usernameOrEmail: '',
+ code: '',
+
+ error: '',
+ };
},
computed: {
payload() {
@@ -38,8 +89,33 @@
},
methods: {
async login() {
- this.token = await this.$axios.$post(`/user`);
+ 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);
+
+ if (response.error) {
+ this.error = response.error;
+ this.usernameOrEmail = '';
+ this.code = '';
+ return;
+ }
+
+ this.token = response.token;
+ }
},
head() {
return head({
diff --git a/server/mailer.js b/server/mailer.js
new file mode 100644
index 00000000..703350d0
--- /dev/null
+++ b/server/mailer.js
@@ -0,0 +1,21 @@
+const mailer = require('mailer');
+
+module.exports = (to, subject, body) => {
+ mailer.send({
+ host: process.env.MAILER_HOST,
+ port: parseInt(process.env.MAILER_PORT),
+ ssl: parseInt(process.env.MAILER_PORT) === 465,
+ authentication: 'login',
+ username: process.env.MAILER_USER,
+ password: process.env.MAILER_PASS,
+ from: process.env.MAILER_FROM,
+ to,
+ subject,
+ body,
+ },
+ function(err){
+ if (err) {
+ console.error(err);
+ }
+ });
+}
diff --git a/server/notify.js b/server/notify.js
index 2a6faf6d..eac7f0f8 100644
--- a/server/notify.js
+++ b/server/notify.js
@@ -1,6 +1,6 @@
const dbConnection = require('./db');
-const mailer = require('mailer');
require('dotenv').config({ path:__dirname + '/../.env' });
+const mailer = require('./mailer');
async function notify() {
const db = await dbConnection();
@@ -15,23 +15,7 @@ async function notify() {
for (let admin of process.env.MAILER_ADMINS.split(',')) {
console.log('Sending email to ' + admin)
- mailer.send({
- host: process.env.MAILER_HOST,
- port: parseInt(process.env.MAILER_PORT),
- ssl: parseInt(process.env.MAILER_PORT) === 465,
- authentication: 'login',
- username: process.env.MAILER_USER,
- password: process.env.MAILER_PASS,
- from: process.env.MAILER_FROM,
- to: admin,
- subject: '[Zaimki.pl] Wpisy oczekują na moderację',
- body: 'Liczba wpisów: ' + awaitingModeration,
- },
- function(err, result){
- if (err) {
- console.log(err);
- }
- });
+ mailer(admin, '[Zaimki.pl] Wpisy oczekują na moderację', 'Liczba wpisów: ' + awaitingModeration);
}
}
diff --git a/server/user.js b/server/user.js
index e0db553c..61e17664 100644
--- a/server/user.js
+++ b/server/user.js
@@ -1,16 +1,154 @@
import jwt from './jwt';
import { makeId } from '../src/helpers';
+const dbConnection = require('./db');
+const SQL = require('sql-template-strings');
+import { ulid } from 'ulid';
+import translations from "./translations";
+const mailer = require('./mailer');
+
+const now = Math.floor(Date.now() / 1000);
+
+const getUser = (authorization) => {
+ if (!authorization || !authorization.startsWith('Bearer ')) {
+ return null;
+ }
+
+ return jwt.validate(authorization.substring(7));
+}
+
+const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
+ const id = ulid();
+ await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES (
+ ${id},
+ ${user ? user.id : null},
+ ${type},
+ ${JSON.stringify(payload)},
+ ${validForMinutes ? (now + validForMinutes * 60) : null}
+ )`);
+ return id;
+}
+
+const findAuthenticator = async (db, id, type) => {
+ const authenticator = await db.get(SQL`SELECT * FROM authenticators
+ WHERE id = ${id}
+ AND type = ${type}
+ AND (validUntil IS NULL OR validUntil > ${now})
+ `);
+
+ if (authenticator) {
+ authenticator.payload = JSON.parse(authenticator.payload);
+ }
+
+ return authenticator
+}
+
+const invalidateAuthenticator = async (db, id) => {
+ await db.get(SQL`UPDATE authenticators
+ SET validUntil = ${now}
+ WHERE id = ${id}
+ `);
+}
+
+const init = async (db, usernameOrEmail) => {
+ let user = undefined;
+
+ const isEmail = usernameOrEmail.indexOf('@') > -1;
+
+ if (isEmail) {
+ user = await db.get(SQL`SELECT * FROM users WHERE email = ${usernameOrEmail}`);
+ } else {
+ user = await db.get(SQL`SELECT * FROM users WHERE username = ${usernameOrEmail}`);
+ }
+
+ if (!user && !isEmail) {
+ return {error: 'user.login.userNotFound'}
+ }
+
+ const payload = {
+ username: isEmail ? (user ? user.username : null) : usernameOrEmail,
+ email: isEmail ? usernameOrEmail : user.email,
+ code: makeId(6, '0123456789'),
+ }
+
+ const codeKey = await saveAuthenticator(db, 'email', user, payload, 15);
+
+ mailer(
+ payload.email,
+ `[${translations.title}] ${translations.user.login.email.subject.replace('%code%', payload.code)}`,
+ translations.user.login.email.content.replace('%code%', payload.code),
+ )
+
+ return {
+ token: jwt.sign({...payload, code: null, codeKey}, '15m'),
+ };
+}
+
+const validate = async (db, user, code) => {
+ if (!user || !user.codeKey) {
+ return {error: 'user.tokenExpired'};
+ }
+
+ const authenticator = await findAuthenticator(db, user.codeKey, 'email');
+ if (!authenticator) {
+ return {error: 'user.tokenExpired'};
+ }
+
+ if (authenticator.payload.code !== code) {
+ return {error: 'user.code.invalid'};
+ }
+
+ return await authenticate(db, user, authenticator);
+}
+
+const defaultUsername = async (db, email) => {
+ const base = email.substring(0, email.indexOf('@'))
+ .padEnd(4, '0')
+ .replace(/[^A-Za-z0-9._-]/g, '_');
+
+ let c = 0;
+ while (true) {
+ let proposal = base + (c || '');
+ let dbUser = await db.get(SQL`SELECT id FROM users WHERE username = ${proposal}`);
+ if (!dbUser) {
+ return proposal;
+ }
+ c++;
+ }
+}
+
+const authenticate = async (db, user, authenticator) => {
+ let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${user.email}`);
+ if (!dbUser) {
+ dbUser = {
+ id: ulid(),
+ username: await defaultUsername(db, user.email),
+ email: user.email,
+ roles: 'user',
+ avatarSource: null,
+ }
+ }
+
+ invalidateAuthenticator(db, authenticator);
+
+ return {
+ token: jwt.sign({
+ ...dbUser,
+ authenticated: true,
+ }),
+ };
+}
export default async function (req, res, next) {
const db = await dbConnection();
- let result = {error: 'Not found'}
- if (req.method === 'GET' && req.url === '/all') {
- jwt.sign({
- username: 'andrea',
- email: 'andrea@avris.it',
- secret: makeId(6, '0123456789'),
- })
+ let result = {error: 'notfound'}
+
+ const user = getUser(req.headers.authorization);
+
+ if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) {
+ 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);
}
res.setHeader('content-type', 'application/json');