#60 [api] Public API
This commit is contained in:
parent
8fcf96040e
commit
629a12698e
|
@ -182,7 +182,7 @@
|
|||
if (this.nounsRaw !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.nounsRaw = await this.$axios.$get(`/nouns/all`);
|
||||
this.nounsRaw = await this.$axios.$get(`/nouns`);
|
||||
},
|
||||
async setFilter(filter) {
|
||||
this.filter = filter;
|
||||
|
|
|
@ -37,12 +37,20 @@
|
|||
<Icon v="gitlab" set="b"/>
|
||||
</SquareButton>
|
||||
</div>
|
||||
<p v-if="config.user.enabled" class="small">
|
||||
<nuxt-link :to="`/${config.user.termsRoute}`">
|
||||
<Icon v="gavel"/>
|
||||
<T>terms.header</T>
|
||||
</nuxt-link>
|
||||
</p>
|
||||
<ul v-if="config.user.enabled" class="list-inline small">
|
||||
<li class="list-inline-item">
|
||||
<nuxt-link :to="`/${config.user.termsRoute}`">
|
||||
<Icon v="gavel"/>
|
||||
<T>terms.header</T>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<nuxt-link to="/api">
|
||||
<Icon v="laptop-code"/>
|
||||
<T>api.header</T>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
<Share/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -87,8 +87,8 @@
|
|||
|
||||
return jwt.verify(this.token, process.env.PUBLIC_KEY, {
|
||||
algorithm: 'RS256',
|
||||
audience: process.env.BASE_URL,
|
||||
issuer: process.env.BASE_URL,
|
||||
audience: this.$base,
|
||||
issuer: this.$base,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -118,7 +118,6 @@
|
|||
|
||||
<script>
|
||||
import { nounTemplates } from '../src/data';
|
||||
import config from "../data/config.suml";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
data() {
|
||||
return {
|
||||
preset: {
|
||||
url: process.env.BASE_URL + this.$route.path,
|
||||
url: this.$base + this.$route.path,
|
||||
title: this.title,
|
||||
extra: {
|
||||
media: '',
|
||||
|
|
|
@ -247,3 +247,16 @@ profile:
|
|||
redirects:
|
||||
- { from: '^/neutratywy', to: '/rzeczowniki' }
|
||||
- { from: '^/literatura', to: '/korpus' }
|
||||
|
||||
api:
|
||||
examples:
|
||||
pronouns_all: ['/api/pronouns']
|
||||
pronouns_one:
|
||||
- '/api/pronouns/ono/jej'
|
||||
- '/api/pronouns/ono/jej?examples[]=Czy%20chcia%C5%82%7Bverb_end_inter%7Dby%C5%9B%20skorzysta%C4%87%20z%20naszej%20oferty%3F%7CCzy%20chci%7Bverb_middle_inter%7Dby%C5%9Bcie%20skorzysta%C4%87%20z%20naszej%20oferty%3F%7C0'
|
||||
|
||||
sources_all: ['/api/sources']
|
||||
sources_one: ['/api/sources/queerZaimki']
|
||||
|
||||
nouns_all: ['/api/nouns']
|
||||
nouns_search: ['/api/nouns/search/ateis']
|
||||
|
|
|
@ -805,3 +805,8 @@ table:
|
|||
count: 'Liczba'
|
||||
sort: 'Przeciągnij by posortować'
|
||||
scrollUp: 'Przewiń na samą górę'
|
||||
|
||||
api:
|
||||
header: 'Publiczne API'
|
||||
example: 'Przykład'
|
||||
query: 'Query string parameters'
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import translations from './server/translations';
|
||||
import config from './server/config';
|
||||
import { loadSuml } from './server/loader';
|
||||
import fs from 'fs';
|
||||
import {buildDict} from "./src/helpers";
|
||||
|
||||
const config = loadSuml('config');
|
||||
const translations = loadSuml('translations');
|
||||
|
||||
const locale = config.locale;
|
||||
const title = translations.title;
|
||||
const description = translations.description;
|
||||
|
@ -158,6 +160,8 @@ export default {
|
|||
routes.push({ path: '/' + config.template.any.route, component: resolve(__dirname, 'routes/any.vue') });
|
||||
}
|
||||
|
||||
routes.push({ path: '/api', component: resolve(__dirname, 'routes/api.vue') });
|
||||
|
||||
routes.push({ name: 'all', path: '*', component: resolve(__dirname, 'routes/template.vue') });
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ import { locales } from '../src/data';
|
|||
import {buildDict} from "../src/helpers";
|
||||
|
||||
export default ({ app }) => {
|
||||
Vue.prototype.$base = process.env.BASE_URL;
|
||||
Vue.prototype.$t = t;
|
||||
Vue.prototype.config = config;
|
||||
Vue.prototype.locales = buildDict(function* () {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<h2>
|
||||
<Icon v="laptop-code"/>
|
||||
<T>api.header</T>
|
||||
</h2>
|
||||
|
||||
<section v-for="group in groups" v-if="group.enabled" class="py-2">
|
||||
<h3>
|
||||
<Icon :v="group.icon"/>
|
||||
<T>{{group.header}}</T>
|
||||
</h3>
|
||||
<ul>
|
||||
<li v-for="([method, path, queryString], endpoint) in group.endpoints" class="my-3">
|
||||
<p>
|
||||
<span class="badge badge-primary">{{method}}</span>
|
||||
<code>{{path}}</code>
|
||||
<a v-for="example in config.api.examples[endpoint]" :href="$base + example" class="badge badge-light border mx-1" target="_blank" rel="noopener">
|
||||
<Icon v="cog"/>
|
||||
<T>api.example</T>
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="queryString" class="mb-0 small">
|
||||
<T>api.query</T>:
|
||||
</p>
|
||||
<ul v-if="queryString" class="small">
|
||||
<li v-for="(description, param) in queryString">
|
||||
<code>{{param}}</code> – <span v-html="description"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {head} from "../src/helpers";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
groups: [{
|
||||
enabled: this.config.template.enabled,
|
||||
header: 'home.header',
|
||||
icon: 'tags',
|
||||
endpoints: {
|
||||
pronouns_all: ['GET', '/api/pronouns'],
|
||||
pronouns_one: ['GET', '/api/pronouns/{pronoun}', {
|
||||
'examples[]': 'Overwrite the default example sentences with custom ones. For each of them use the following format: <code>{sentenceSingular}|{sentencePlural}|{isHonorific}</code>. If <code>sentencePlural</code> is missing, if defaults to being the same as <code>sentenceSingular</code>. <code>isHonorific</code> can be <code>0</code> (default) or <code>1</code>.',
|
||||
}],
|
||||
},
|
||||
}, {
|
||||
enabled: this.config.sources.enabled,
|
||||
header: 'sources.header',
|
||||
icon: 'books',
|
||||
endpoints: {
|
||||
sources_all: ['GET', '/api/sources'],
|
||||
sources_one: ['GET', '/api/sources/{key}'],
|
||||
},
|
||||
}, {
|
||||
enabled: this.config.nouns.enabled,
|
||||
header: 'nouns.header',
|
||||
icon: 'atom-alt',
|
||||
endpoints: {
|
||||
nouns_all: ['GET', '/api/nouns'],
|
||||
nouns_search: ['GET', '/api/nouns/search/{term}'],
|
||||
},
|
||||
}],
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return head({
|
||||
title: this.$t('api.header'),
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -243,14 +243,14 @@
|
|||
if (!this.selectedTemplate.pronoun()) {
|
||||
return null;
|
||||
}
|
||||
return this.addSlash(process.env.BASE_URL + '/' + (this.usedBaseEquals ? this.usedBase : this.longLink));
|
||||
return this.addSlash(this.$base + '/' + (this.usedBaseEquals ? this.usedBase : this.longLink));
|
||||
},
|
||||
linkMultiple() {
|
||||
if (!this.multiple.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.addSlash(process.env.BASE_URL + '/' + this.multiple.join('&'));
|
||||
return this.addSlash(this.$base + '/' + this.multiple.join('&'));
|
||||
},
|
||||
sources() {
|
||||
return getSources(this.selectedTemplate);
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
for (let pronoun in this.profile.pronouns) {
|
||||
if (!this.profile.pronouns.hasOwnProperty(pronoun)) { continue; }
|
||||
|
||||
const link = pronoun.replace(new RegExp('^' + process.env.BASE_URL), '').replace(new RegExp('^/'), '');
|
||||
const link = pronoun.replace(new RegExp('^' + this.$base), '').replace(new RegExp('^/'), '');
|
||||
const template = buildTemplate(templates, link);
|
||||
|
||||
if (template) {
|
||||
|
|
|
@ -188,7 +188,7 @@
|
|||
this.$router.push(`/@${this.$user().username}`)
|
||||
},
|
||||
validatePronoun(pronoun) {
|
||||
const link = pronoun.replace(new RegExp('^' + process.env.BASE_URL), '').replace(new RegExp('^/'), '');
|
||||
const link = pronoun.replace(new RegExp('^' + this.$base), '').replace(new RegExp('^/'), '');
|
||||
const template = buildTemplate(templates, link);
|
||||
|
||||
return template ? null : 'profile.pronounsNotFound'
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import Suml from 'suml';
|
||||
const fs = require('fs');
|
||||
export default new Suml().parse(fs.readFileSync('./data/config.suml').toString());
|
|
@ -5,7 +5,7 @@ import session from 'express-session';
|
|||
import cookieParser from 'cookie-parser';
|
||||
import grant from "grant";
|
||||
import router from "./routes/user";
|
||||
import config from './config';
|
||||
import { loadSuml } from './loader';
|
||||
|
||||
const app = express()
|
||||
|
||||
|
@ -18,7 +18,7 @@ app.use(session({
|
|||
}));
|
||||
|
||||
app.use(async function (req, res, next) {
|
||||
req.config = config;
|
||||
req.config = loadSuml('config');
|
||||
req.rawUser = authenticate(req);
|
||||
req.user = req.rawUser && req.rawUser.authenticated ? req.rawUser : null;
|
||||
req.admin = req.user && req.user.roles === 'admin';
|
||||
|
@ -34,8 +34,9 @@ app.use(require('./routes/user').default);
|
|||
app.use(require('./routes/profile').default);
|
||||
app.use(require('./routes/admin').default);
|
||||
|
||||
app.use(require('./routes/nouns').default);
|
||||
app.use(require('./routes/templates').default);
|
||||
app.use(require('./routes/sources').default);
|
||||
app.use(require('./routes/nouns').default);
|
||||
|
||||
export default {
|
||||
path: '/api',
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
const fs = require('fs');
|
||||
import Suml from 'suml';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
export const loadSuml = name => new Suml().parse(fs.readFileSync(`./data/${name}.suml`).toString());
|
||||
|
||||
export const loadTsv = name => Papa.parse(fs.readFileSync(`./data/${name}.tsv`).toString(), {
|
||||
dynamicTyping: true,
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
delimiter: '\t',
|
||||
}).data;
|
|
@ -1,11 +1,13 @@
|
|||
import { Router } from 'express';
|
||||
import SQL from 'sql-template-strings';
|
||||
import {createCanvas, loadImage, registerFont} from "canvas";
|
||||
import translations from "../translations";
|
||||
import { loadSuml } from '../loader';
|
||||
import avatar from '../avatar';
|
||||
import {buildTemplate, parseTemplates} from "../../src/buildTemplate";
|
||||
import {loadTsv} from "../../src/tsv";
|
||||
|
||||
const translations = loadSuml('translations');
|
||||
|
||||
const drawCircle = (context, image, x, y, size) => {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
|
|
|
@ -23,7 +23,7 @@ const approve = async (db, id) => {
|
|||
|
||||
const router = Router();
|
||||
|
||||
router.get('/nouns/all', async (req, res) => {
|
||||
router.get('/nouns', async (req, res) => {
|
||||
return res.json(await req.db.all(SQL`
|
||||
SELECT * FROM nouns
|
||||
WHERE locale = ${req.config.locale}
|
||||
|
@ -32,6 +32,17 @@ router.get('/nouns/all', async (req, res) => {
|
|||
`));
|
||||
});
|
||||
|
||||
router.get('/nouns/search/:term', async (req, res) => {
|
||||
const term = '%' + req.params.term + '%';
|
||||
return res.json(await req.db.all(SQL`
|
||||
SELECT * FROM nouns
|
||||
WHERE locale = ${req.config.locale}
|
||||
AND approved >= ${req.admin ? 0 : 1}
|
||||
AND (masc like ${term} OR fem like ${term} OR neutr like ${term} OR mascPl like ${term} OR femPl like ${term} OR neutrPl like ${term})
|
||||
ORDER BY approved, masc
|
||||
`));
|
||||
});
|
||||
|
||||
router.post('/nouns/submit', async (req, res) => {
|
||||
if (!(req.user && req.user.admin) && isTroll(JSON.stringify(req.body))) {
|
||||
return res.json('ok');
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Router } from 'express';
|
||||
import mailer from "../../src/mailer";
|
||||
import {camelCase, capitalise} from "../../src/helpers";
|
||||
import { camelCase } from "../../src/helpers";
|
||||
import { loadTsv } from '../loader';
|
||||
|
||||
const generateId = title => {
|
||||
return camelCase(title.split(' ').slice(0, 2));
|
||||
|
@ -21,8 +22,26 @@ const buildEmail = (data, user) => {
|
|||
return `<ul>${human.join('')}</ul><pre>${tsv.join('\t')}</pre>`;
|
||||
}
|
||||
|
||||
const loadSources = () => {
|
||||
return loadTsv('sources/sources').map(s => {
|
||||
if (s.author) {
|
||||
s.author = s.author.replace('^', '');
|
||||
}
|
||||
s.fragments = s.fragments.split('@').map(f => f.replace(/\|/g, '\n'));
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/sources', async (req, res) => {
|
||||
return res.json(loadSources());
|
||||
});
|
||||
|
||||
router.get('/sources/:key', async (req, res) => {
|
||||
return res.json([...loadSources().filter(s => s.key === req.params.key), null][0]);
|
||||
});
|
||||
|
||||
router.post('/sources/submit', async (req, res) => {
|
||||
const emailBody = buildEmail(req.body, req.user);
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { Router } from 'express';
|
||||
import { loadTsv } from '../loader';
|
||||
import {buildTemplate, parseTemplates} from "../../src/buildTemplate";
|
||||
import {buildList} from "../../src/helpers";
|
||||
import {Example} from "../../src/classes";
|
||||
|
||||
const buildExample = e => new Example(
|
||||
Example.parse(e.singular),
|
||||
Example.parse(e.plural || e.singular),
|
||||
e.isHonorific,
|
||||
)
|
||||
|
||||
const requestExamples = r => {
|
||||
if (!r || !r.length) {
|
||||
return loadTsv('templates/examples');
|
||||
}
|
||||
|
||||
return buildList(function* () {
|
||||
for (let rr of r) {
|
||||
let [singular, plural, isHonorific] = rr.split('|');
|
||||
yield { singular, plural, isHonorific: !!isHonorific};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const addExamples = (pronoun, examples) => {
|
||||
return buildList(function* () {
|
||||
for (let example of examples) {
|
||||
yield buildExample(example).format(pronoun);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/pronouns', async (req, res) => {
|
||||
const templates = parseTemplates(loadTsv('templates/templates'));
|
||||
for (let template in templates) {
|
||||
if (!templates.hasOwnProperty(template)) { continue; }
|
||||
templates[template].examples = addExamples(templates[template], requestExamples(req.query.examples))
|
||||
}
|
||||
return res.json(templates);
|
||||
});
|
||||
|
||||
router.get('/pronouns/:pronoun*', async (req, res) => {
|
||||
const pronoun = buildTemplate(
|
||||
parseTemplates(loadTsv('templates/templates')),
|
||||
req.params.pronoun + req.params[0],
|
||||
);
|
||||
if (pronoun) {
|
||||
pronoun.examples = addExamples(pronoun, requestExamples(req.query.examples))
|
||||
}
|
||||
return res.json(pronoun);
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -2,13 +2,15 @@ import { Router } from 'express';
|
|||
import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
import {buildDict, makeId, now} from "../../src/helpers";
|
||||
import translations from "../translations";
|
||||
import jwt from "../../src/jwt";
|
||||
import mailer from "../../src/mailer";
|
||||
import config from '../config';
|
||||
import { loadSuml } from '../loader';
|
||||
import avatar from '../avatar';
|
||||
import { config as socialLoginConfig, handlers as socialLoginHandlers } from '../social';
|
||||
|
||||
const config = loadSuml('config');
|
||||
const translations = loadSuml('translations');
|
||||
|
||||
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
|
||||
|
||||
const normalise = s => s.trim().toLowerCase();
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import Suml from 'suml';
|
||||
const fs = require('fs');
|
||||
export default new Suml().parse(fs.readFileSync('./data/translations.suml').toString());
|
|
@ -6,14 +6,6 @@ export class ExamplePart {
|
|||
this.variable = variable;
|
||||
this.str = str;
|
||||
}
|
||||
|
||||
format(form) {
|
||||
if (!this.variable) {
|
||||
return this.str[form.plural];
|
||||
}
|
||||
|
||||
return form[this.str[form.plural]];
|
||||
}
|
||||
}
|
||||
|
||||
export class Example {
|
||||
|
@ -44,12 +36,12 @@ export class Example {
|
|||
return parts;
|
||||
}
|
||||
|
||||
format(form) {
|
||||
return Example.ucfirst(this.parts.map(part => part.format(form)).join(''));
|
||||
}
|
||||
format(pronoun) {
|
||||
const plural = this.isHonorific ? pronoun.pluralHonorific[0] : pronoun.plural[0];
|
||||
|
||||
static ucfirst(str) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
return capitalise(this[plural ? 'pluralParts' : 'singularParts'].map(part => {
|
||||
return part.variable ? pronoun.getMorpheme(part.str) : part.str;
|
||||
}).join(''));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Reference in New Issue