Implement /api/v1/apps Mastodon API.
This commit is contained in:
commit
339cbeb297
9 changed files with 2497 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
brainz-social.db
|
||||
node_modules/
|
72
app.js
Normal file
72
app.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const glob = require('glob');
|
||||
const log4js = require('log4js');
|
||||
const { match: createPathMatch } = require('path-to-regexp');
|
||||
var bodyParser = require('body-parser');
|
||||
|
||||
(async () => {
|
||||
const app = express();
|
||||
app.use(bodyParser.json({ type: 'application/*+json',
|
||||
verify: function (req, _res, buf, _encoding) {
|
||||
req.rawBody = buf;
|
||||
}
|
||||
}));
|
||||
app.use(bodyParser.json({
|
||||
verify: function (req, _res, buf, _encoding) {
|
||||
req.rawBody = buf;
|
||||
}
|
||||
}));
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: false,
|
||||
verify: function (req, _res, buf, _encoding) {
|
||||
req.rawBody = buf;
|
||||
}
|
||||
}));
|
||||
const routes = await glob('**/*.js', {
|
||||
cwd: './routes',
|
||||
dot: true,
|
||||
});
|
||||
const pathMatches = [];
|
||||
app.use((req, _res, next) => {
|
||||
const requestUrl = new URL(req.url, 'https://example.com/');
|
||||
let candidateUrl = '';
|
||||
let secondCandidateUrl = '';
|
||||
for ( const pathMatch in pathMatches ) {
|
||||
if ( pathMatches[pathMatch](requestUrl.pathname) ) {
|
||||
// If we get an exact match, we don't need to process further.
|
||||
return next();
|
||||
} else if ( requestUrl.pathname.endsWith('/') && pathMatches[pathMatch](`${requestUrl.pathname}index`) ) {
|
||||
// If we end with a /, and the index path matches, lets do the index path, but prioritize the non-index path.
|
||||
const secondRequestUrl = new URL(requestUrl);
|
||||
secondRequestUrl.pathname = `${requestUrl.pathname}index`;
|
||||
candidateUrl = secondRequestUrl.toString().substring(19);
|
||||
} else if ( pathMatches[pathMatch](`${requestUrl.pathname}/index`) ) {
|
||||
// If we don't end with a /, and the /index path matches, lets do the /index path, but prioritize paths checked previously.
|
||||
const secondRequestUrl = new URL(requestUrl);
|
||||
secondRequestUrl.pathname = `${requestUrl.pathname}/index`;
|
||||
secondCandidateUrl = secondRequestUrl.toString().substring(19);
|
||||
}
|
||||
}
|
||||
if ( candidateUrl !== '' ) {
|
||||
req.url = candidateUrl;
|
||||
return next();
|
||||
}
|
||||
if ( secondCandidateUrl !== '' ) {
|
||||
req.url = secondCandidateUrl;
|
||||
return next();
|
||||
}
|
||||
return next();
|
||||
} );
|
||||
for ( const routeScript in routes ) {
|
||||
const route = routes[routeScript].replace(/\.js$/, '');
|
||||
console.log(route);
|
||||
pathMatches.push( createPathMatch(`/${route}`));
|
||||
const routeObj = require(`./routes/${route}`);
|
||||
if ( routeObj.route ) {
|
||||
routeObj.route(app.route(`/${route}`));
|
||||
}
|
||||
}
|
||||
app.listen(process.env.PORT || 3000);
|
||||
})();
|
19
database-handler.js
Normal file
19
database-handler.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
const db = require('better-sqlite3')('brainz-social.db');
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
export default {
|
||||
db,
|
||||
|
||||
getConfig: (key) => {
|
||||
const row = db.prepare('SELECT * FROM config WHERE key = ?').get(key);
|
||||
return row.value;
|
||||
},
|
||||
|
||||
setConfig: (key, value) => {
|
||||
db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES(?, ?);").run(key, value);
|
||||
},
|
||||
|
||||
createApplication: (client_name, redirect_uri, scopes, website, client_id, client_secret) => {
|
||||
db.prepare("INSERT INTO applications (client_name, redirect_uri, scopes, website, client_id, client_secret) VALUES (?, ?, ?, ?, ?, ?);").run(client_name, redirect_uri, scopes, website, client_id, client_secret);
|
||||
}
|
||||
};
|
6
database.json
Normal file
6
database.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"dev": {
|
||||
"driver": "sqlite3",
|
||||
"filename": "brainz-social.db"
|
||||
}
|
||||
}
|
43
migrations/20230926224347-applications.js
Normal file
43
migrations/20230926224347-applications.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
var dbm;
|
||||
var type;
|
||||
var seed;
|
||||
|
||||
/**
|
||||
* We receive the dbmigrate dependency from dbmigrate initially.
|
||||
* This enables us to not have to rely on NODE_PATH.
|
||||
*/
|
||||
exports.setup = function(options, seedLink) {
|
||||
dbm = options.dbmigrate;
|
||||
type = dbm.dataType;
|
||||
seed = seedLink;
|
||||
};
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.createTable.bind(db, 'applications', {
|
||||
id: { type: 'int', primaryKey: true, autoIncrement: true },
|
||||
client_name: 'string',
|
||||
redirect_uri: 'string',
|
||||
scopes: 'string',
|
||||
website: 'string',
|
||||
client_id: { type: 'string', unique: true },
|
||||
client_secret: 'string'
|
||||
}),
|
||||
db.addIndex.bind(db, 'applications', 'clientIdIndex', ['client_id'], true)
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.removeIndex.bind(db, 'applications', 'clientIdIndex'),
|
||||
db.dropTable.bind(db, 'applications')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
"version": 1
|
||||
};
|
31
migrations/20230927011525-config.js
Normal file
31
migrations/20230927011525-config.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
'use strict';
|
||||
|
||||
var dbm;
|
||||
var type;
|
||||
var seed;
|
||||
|
||||
/**
|
||||
* We receive the dbmigrate dependency from dbmigrate initially.
|
||||
* This enables us to not have to rely on NODE_PATH.
|
||||
*/
|
||||
exports.setup = function(options, seedLink) {
|
||||
dbm = options.dbmigrate;
|
||||
type = dbm.dataType;
|
||||
seed = seedLink;
|
||||
};
|
||||
|
||||
exports.up = function(db) {
|
||||
return db.createTable('config', {
|
||||
id: { type: 'int', primaryKey: true, autoIncrement: true },
|
||||
key: { type: 'string', unique: true },
|
||||
value: 'string'
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db) {
|
||||
return db.dropTable('config');
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
"version": 1
|
||||
};
|
2219
package-lock.json
generated
Normal file
2219
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
17
package.json
Normal file
17
package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "brainz-social",
|
||||
"version": "0.0.1",
|
||||
"description": "Brainz Social",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Andrew Pietila <a.pietila@protonmail.com>",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.6.0",
|
||||
"db-migrate": "^0.11.14",
|
||||
"db-migrate-sqlite3": "^0.5.0",
|
||||
"web-push": "^3.6.6"
|
||||
}
|
||||
}
|
88
routes/api/v1/apps.js
Normal file
88
routes/api/v1/apps.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
const database_handler = require("../../../database-handler");
|
||||
const webpush = require("web-push");
|
||||
|
||||
export default {
|
||||
route: ((routeObj) => {
|
||||
routeObj.post((req, res) => {
|
||||
var client_name, redirect_uris, scopes, website;
|
||||
if ( !req.body ) {
|
||||
res.status(422);
|
||||
res.json({error: "Validation failed, unparsable or no body."});
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !req.body.client_name ) {
|
||||
res.status(422);
|
||||
res.json({error: "Validation failed, no client_name."});
|
||||
return;
|
||||
}
|
||||
|
||||
client_name = req.body.client_name;
|
||||
|
||||
if ( !req.body.redirect_uris ) {
|
||||
res.status(422);
|
||||
res.json({error: "Validation failed, no redirect_uris."})
|
||||
return;
|
||||
}
|
||||
|
||||
redirect_uris = req.body.redirect_uris;
|
||||
|
||||
if ( redirect_uris !== "urn:ietf:wg:oauth:2.0:oob" ) {
|
||||
var url;
|
||||
try {
|
||||
url = new URL(redirect_uris);
|
||||
} catch (e) {
|
||||
res.status(422);
|
||||
res.json({error: "Validation failed, redirect_uris not parsable as absolute URL."});
|
||||
return;
|
||||
}
|
||||
|
||||
if ( url.schema.toLower === "http" ) {
|
||||
res.status(422);
|
||||
res.json({error: "Validation failed, http redirect_uris not allowed."});
|
||||
}
|
||||
}
|
||||
|
||||
scopes = "read";
|
||||
|
||||
if ( req.body.scopes ) {
|
||||
scopes = req.body.scopes;
|
||||
}
|
||||
|
||||
website = null;
|
||||
|
||||
if ( req.body.website ) {
|
||||
website = req.body.website;
|
||||
}
|
||||
|
||||
var client_id, client_secret, vapid_key;
|
||||
|
||||
// Not that anything is implemented yet regarding Web Push/VAPID/whatever,
|
||||
// this is a required item per the Mastodon API documentation.
|
||||
vapid_key = database_handler.getConfig("vapid_key_public");
|
||||
|
||||
if ( !vapid_key ) {
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
database_handler.setConfig("vapid_key_public", vapidKeys.publicKey);
|
||||
database_handler.setConfig("vapid_key_private", vapidKeys.privateKey);
|
||||
vapid_key = vapidKeys.publicKey;
|
||||
}
|
||||
|
||||
client_id = crypto.randomBytes(32).toString('base64');
|
||||
client_secret = crypto.randomBytes(32).toString('base64');
|
||||
|
||||
database_handler.createApplication(client_name, redirect_uris, scopes, website, client_id, client_secret);
|
||||
|
||||
res.status(200);
|
||||
res.json({
|
||||
name: client_name,
|
||||
website,
|
||||
vapid_key,
|
||||
client_id,
|
||||
client_secret
|
||||
});
|
||||
|
||||
return;
|
||||
})
|
||||
})
|
||||
}
|
Loading…
Add table
Reference in a new issue