#54 user accounts - admin panel

This commit is contained in:
Andrea Vos 2020-10-15 20:55:24 +02:00
parent 4937cc39da
commit 9fd711c04b
9 changed files with 44 additions and 50 deletions

View File

@ -1,5 +1,4 @@
BASE_URL=http://localhost:3000 BASE_URL=http://localhost:3000
SECRET=secret
MAILER_HOST= MAILER_HOST=
MAILER_PORT= MAILER_PORT=

View File

@ -20,7 +20,7 @@
<p>{{ email }}</p> <p>{{ email }}</p>
</div> </div>
<p v-if="$user().roles === 'admin'"> <p v-if="$admin()">
<span class="badge badge-primary"><T>user.account.admin</T></span> <span class="badge badge-primary"><T>user.account.admin</T></span>
</p> </p>
@ -45,9 +45,7 @@
async changeUsername() { async changeUsername() {
await this.post(`/user/change-username`, { await this.post(`/user/change-username`, {
username: this.username username: this.username
}, { }, { headers: this.$auth() });
headers: {...this.$auth()},
});
}, },
async post(url, data, options = {}) { async post(url, data, options = {}) {
this.error = ''; this.error = '';

View File

@ -32,7 +32,7 @@
<span class="d-none d-md-inline"><T>nouns.neuter</T></span> <span class="d-none d-md-inline"><T>nouns.neuter</T></span>
<span class="d-md-none"><T>nouns.neuterShort</T></span> <span class="d-md-none"><T>nouns.neuterShort</T></span>
</th> </th>
<th v-if="secret"></th> <th v-if="$admin()"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -118,9 +118,6 @@
import { nounTemplates } from '../src/data'; import { nounTemplates } from '../src/data';
export default { export default {
props: {
secret: {},
},
data() { data() {
return { return {
form: { form: {
@ -142,9 +139,9 @@
methods: { methods: {
async submit(event) { async submit(event) {
this.submitting = true; this.submitting = true;
await this.$axios.$post(`/nouns/submit?secret=${this.secret}`, { await this.$axios.$post(`/nouns/submit`, {
data: this.form, data: this.form,
}); }, { headers: this.$auth() });
this.submitting = false; this.submitting = false;
this.afterSubmit = true; this.afterSubmit = true;

View File

@ -86,7 +86,6 @@ export default {
}, },
env: { env: {
BASE_URL: process.env.BASE_URL, BASE_URL: process.env.BASE_URL,
SECRET: process.env.SECRET,
PUBLIC_KEY: fs.readFileSync(__dirname + '/keys/public.pem').toString(), PUBLIC_KEY: fs.readFileSync(__dirname + '/keys/public.pem').toString(),
}, },
serverMiddleware: { serverMiddleware: {

View File

@ -16,4 +16,7 @@ export default ({app, store}) => {
authorization: 'Bearer ' + store.state.token, authorization: 'Bearer ' + store.state.token,
} : {}; } : {};
}; };
Vue.prototype.$admin = _ => {
return store.state.user && store.state.user.authenticated && store.state.user.roles === 'admin';
};
} }

View File

@ -14,7 +14,7 @@
<NounsExtra/> <NounsExtra/>
<Loading :value="nounsRaw"> <Loading :value="nounsRaw">
<section v-if="secret"> <section v-if="$admin()">
<div class="alert alert-info"> <div class="alert alert-info">
<strong>{{ nounsCountApproved() }}</strong> <T>nouns.approved</T>, <strong>{{ nounsCountApproved() }}</strong> <T>nouns.approved</T>,
<strong>{{ nounsCountPending() }}</strong> <T>nouns.pending</T>. <strong>{{ nounsCountPending() }}</strong> <T>nouns.pending</T>.
@ -44,7 +44,7 @@
</section> </section>
<section class="table-responsive"> <section class="table-responsive">
<table :class="'table table-striped table-hover table-fixed-' + (secret ? 4 : 3)"> <table :class="'table table-striped table-hover table-fixed-' + ($admin() ? 4 : 3)">
<thead> <thead>
<tr> <tr>
<th class="text-nowrap"> <th class="text-nowrap">
@ -59,7 +59,7 @@
<Icon v="neuter"/> <Icon v="neuter"/>
<T>nouns.neuter</T> <T>nouns.neuter</T>
</th> </th>
<th v-if="secret"></th> <th v-if="$admin()"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -83,7 +83,7 @@
</ul> </ul>
</small> </small>
<button v-if="!secret" class="btn btn-outline-primary btn-sm m-1 hover-show" @click="edit(noun)"> <button v-if="!$admin()" class="btn btn-outline-primary btn-sm m-1 hover-show" @click="edit(noun)">
<Icon v="pen"/> <Icon v="pen"/>
<T>nouns.edit</T> <T>nouns.edit</T>
</button> </button>
@ -128,7 +128,7 @@
</ul> </ul>
</small> </small>
</td> </td>
<td v-if="secret"> <td v-if="$admin()">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li v-if="!noun.approved"> <li v-if="!noun.approved">
<button class="btn btn-success btn-sm m-1" @click="approve(noun)"> <button class="btn btn-success btn-sm m-1" @click="approve(noun)">
@ -160,7 +160,7 @@
</template> </template>
<template v-else> <template v-else>
<tr> <tr>
<td :colspan="secret ? 4 : 3" class="text-center"> <td :colspan="$admin() ? 4 : 3" class="text-center">
<Icon v="search"/> <Icon v="search"/>
<T>nouns.empty</T> <T>nouns.empty</T>
</td> </td>
@ -173,7 +173,7 @@
<Separator icon="plus"/> <Separator icon="plus"/>
<NounSubmitForm ref="form" :secret="secret"/> <NounSubmitForm ref="form"/>
</div> </div>
</template> </template>
@ -189,12 +189,11 @@
return { return {
filter: '', filter: '',
nounsRaw: undefined, nounsRaw: undefined,
secret: this.$route.query.secret,
} }
}, },
mounted() { mounted() {
if (process.client) { if (process.client) {
this.$axios.$get(`/nouns/all?secret=${this.$route.query.secret || ''}`).then(data => { this.$axios.$get(`/nouns/all`, { headers: this.$auth() }).then(data => {
this.nounsRaw = data; this.nounsRaw = data;
}); });
if (window.location.hash) { if (window.location.hash) {
@ -221,7 +220,7 @@
this.$refs.form.edit(noun); this.$refs.form.edit(noun);
}, },
async approve(noun) { async approve(noun) {
await this.$axios.$post(`/nouns/approve/${noun.id}?secret=${this.secret || ''}`); await this.$axios.$post(`/nouns/approve/${noun.id}`, {}, { headers: this.$auth() });
if (noun.base) { if (noun.base) {
delete this.nouns[noun.base]; delete this.nouns[noun.base];
} }
@ -230,7 +229,7 @@
this.$forceUpdate(); this.$forceUpdate();
}, },
async hide(noun) { async hide(noun) {
await this.$axios.$post(`/nouns/hide/${noun.id}?secret=${this.secret || ''}`); await this.$axios.$post(`/nouns/hide/${noun.id}`, {}, { headers: this.$auth() });
noun.approved = false; noun.approved = false;
this.$forceUpdate(); this.$forceUpdate();
}, },
@ -238,7 +237,7 @@
if (!confirm('Czy na pewno usunąć ten wpis?')) { if (!confirm('Czy na pewno usunąć ten wpis?')) {
return false; return false;
} }
await this.$axios.$post(`/nouns/remove/${noun.id}?secret=${this.secret || ''}`); await this.$axios.$post(`/nouns/remove/${noun.id}`, {}, { headers: this.$auth() });
delete this.nouns[noun.id]; delete this.nouns[noun.id];
this.$forceUpdate(); this.$forceUpdate();
}, },

9
server/authenticate.js Normal file
View File

@ -0,0 +1,9 @@
import jwt from './jwt';
export default ({headers: { authorization }}) => {
if (!authorization || !authorization.startsWith('Bearer ')) {
return null;
}
return jwt.validate(authorization.substring(7));
}

View File

@ -1,6 +1,7 @@
const dbConnection = require('./db'); const dbConnection = require('./db');
const SQL = require('sql-template-strings'); const SQL = require('sql-template-strings');
import { ulid } from 'ulid' import { ulid } from 'ulid'
import authenticate from './authenticate';
const parseQuery = (queryString) => { const parseQuery = (queryString) => {
const query = {}; const query = {};
@ -64,20 +65,17 @@ const isTroll = (body) => {
export default async function (req, res, next) { export default async function (req, res, next) {
const db = await dbConnection(); const db = await dbConnection();
const user = authenticate(req);
const [url, queryString] = req.url.split('?'); const isAdmin = user && user.authenticated && user.roles === 'admin';
const query = parseQuery(queryString || '');
const isAdmin = query['secret'] === process.env.SECRET;
let result = {error: 'Not found'} let result = {error: 'Not found'}
if (req.method === 'GET' && url === '/all') { if (req.method === 'GET' && req.url === '/all') {
result = await db.all(` result = await db.all(`
SELECT * FROM nouns SELECT * FROM nouns
${isAdmin ? '' : 'WHERE approved = 1'} ${isAdmin ? '' : 'WHERE approved = 1'}
ORDER BY approved, masc ORDER BY approved, masc
`); `);
} else if (req.method === 'POST' && url === '/submit') { } else if (req.method === 'POST' && req.url === '/submit') {
if (isAdmin || !isTroll(req.body.data)) { if (isAdmin || !isTroll(req.body.data)) {
const id = ulid() const id = ulid()
await db.get(SQL` await db.get(SQL`
@ -94,14 +92,14 @@ export default async function (req, res, next) {
} }
} }
result = 'ok'; result = 'ok';
} else if (req.method === 'POST' && url.startsWith('/approve/') && isAdmin) { } else if (req.method === 'POST' && req.url.startsWith('/approve/') && isAdmin) {
await approve(db, getId(url)); await approve(db, getId(req.url));
result = 'ok'; result = 'ok';
} else if (req.method === 'POST' && url.startsWith('/hide/') && isAdmin) { } else if (req.method === 'POST' && req.url.startsWith('/hide/') && isAdmin) {
await hide(db, getId(url)); await hide(db, getId(req.url));
result = 'ok'; result = 'ok';
} else if (req.method === 'POST' && url.startsWith('/remove/') && isAdmin) { } else if (req.method === 'POST' && req.url.startsWith('/remove/') && isAdmin) {
await remove(db, getId(url)); await remove(db, getId(req.url));
result = 'ok'; result = 'ok';
} }

View File

@ -5,19 +5,12 @@ const SQL = require('sql-template-strings');
import { ulid } from 'ulid'; import { ulid } from 'ulid';
import translations from "./translations"; import translations from "./translations";
const mailer = require('./mailer'); const mailer = require('./mailer');
import authenticate from './authenticate';
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-'; const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
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 saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
const id = ulid(); const id = ulid();
await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES ( await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES (
@ -109,7 +102,7 @@ const validate = async (db, user, code) => {
await invalidateAuthenticator(db, authenticator); await invalidateAuthenticator(db, authenticator);
return await authenticate(db, user); return await issueAuthentication(db, user);
} }
const defaultUsername = async (db, email) => { const defaultUsername = async (db, email) => {
@ -129,7 +122,7 @@ const defaultUsername = async (db, email) => {
} }
} }
const authenticate = async (db, user) => { const issueAuthentication = async (db, user) => {
let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${user.email}`); let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${user.email}`);
if (!dbUser) { if (!dbUser) {
dbUser = { dbUser = {
@ -161,16 +154,15 @@ const changeUsername = async (db, user, username) => {
await db.get(SQL`UPDATE users SET username = ${username} WHERE email = ${user.email}`); await db.get(SQL`UPDATE users SET username = ${username} WHERE email = ${user.email}`);
return await authenticate(db, user); return await issueAuthentication(db, user);
} }
export default async function (req, res, next) { export default async function (req, res, next) {
const db = await dbConnection(); const db = await dbConnection();
const user = authenticate(req);
let result = {error: 'notfound'} let result = {error: 'notfound'}
const user = getUser(req.headers.authorization);
if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) { if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) {
result = await init(db, req.body.usernameOrEmail) result = await init(db, req.body.usernameOrEmail)
} else if (req.method === 'POST' && req.url === '/validate' && req.body.code) { } else if (req.method === 'POST' && req.url === '/validate' && req.body.code) {