mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-18 12:05:28 -04:00
#54 user accounts - admin panel
This commit is contained in:
parent
4937cc39da
commit
9fd711c04b
@ -1,5 +1,4 @@
|
|||||||
BASE_URL=http://localhost:3000
|
BASE_URL=http://localhost:3000
|
||||||
SECRET=secret
|
|
||||||
|
|
||||||
MAILER_HOST=
|
MAILER_HOST=
|
||||||
MAILER_PORT=
|
MAILER_PORT=
|
||||||
|
@ -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 = '';
|
||||||
|
@ -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;
|
||||||
|
@ -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: {
|
||||||
|
@ -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';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -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
9
server/authenticate.js
Normal 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));
|
||||||
|
}
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user