Compare commits
No commits in common. "main" and "main2" have entirely different histories.
50 changed files with 5773 additions and 957 deletions
|
@ -1,15 +0,0 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{yaml,yml}]
|
||||
indent_size = 2
|
|
@ -1,16 +1,19 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": false,
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"overrides": [
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
},
|
||||
"extends": [
|
||||
// "eslint:recommended",
|
||||
// "airbnb-base"
|
||||
],
|
||||
"plugins": [
|
||||
"import"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
|
@ -22,33 +25,70 @@
|
|||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-undef": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"quote-props": [
|
||||
"error",
|
||||
"consistent-as-needed"
|
||||
"as-needed"
|
||||
],
|
||||
"padded-blocks": [
|
||||
"dot-notation": [
|
||||
"error",
|
||||
{
|
||||
"allowKeywords": false
|
||||
}
|
||||
],
|
||||
"eol-last": "error",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"no-multi-spaces": "error",
|
||||
"space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"import/order": "error",
|
||||
"no-return-await": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"padded-blocks": [
|
||||
"error",
|
||||
"never",
|
||||
{
|
||||
"allowSingleLineBlocks": false
|
||||
}
|
||||
],
|
||||
"space-infix-ops": "error",
|
||||
"radix": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"one-var": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"function-call-argument-newline": [
|
||||
"one-var-declaration-per-line": [
|
||||
"error",
|
||||
"consistent"
|
||||
"always"
|
||||
],
|
||||
"template-curly-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
135
.gitignore
vendored
135
.gitignore
vendored
|
@ -1,132 +1,5 @@
|
|||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
brainz-social.db
|
||||
brainz-social.db-shm
|
||||
brainz-social.db-wal
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.idea/
|
|
@ -1,14 +0,0 @@
|
|||
---
|
||||
allowedLicenses:
|
||||
- (MIT AND CC-BY-3.0)
|
||||
- (MIT OR CC0-1.0)
|
||||
- Apache-2.0
|
||||
- BSD-2-Clause
|
||||
- BSD-3-Clause
|
||||
- BlueOak-1.0.0
|
||||
- CC0-1.0
|
||||
- CC-BY-3.0
|
||||
- ISC
|
||||
- MIT
|
||||
- Python-2.0
|
||||
- WTFPL
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.36.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
- repo: https://github.com/kontrolilo/kontrolilo
|
||||
rev: v2.1.0
|
||||
hooks:
|
||||
- id: license-check-npm
|
11
LICENSE
11
LICENSE
|
@ -1,11 +0,0 @@
|
|||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
@ -1,2 +1,7 @@
|
|||
# haxsocial
|
||||
# BRAINZ SOCIAL
|
||||
|
||||
## Creating your first account
|
||||
|
||||
```node ./bin/create_account.js USERNAME EMAIL_ADDRESS $(mkpasswd -m bcrypt)```
|
||||
|
||||
For the provided password prompt, enter your chosen password.
|
77
app.js
77
app.js
|
@ -1,55 +1,82 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const glob = require('glob');
|
||||
const { match: createPathMatch } = require('path-to-regexp');
|
||||
const express = require("express");
|
||||
const {glob} = require("glob");
|
||||
const {match: createPathMatch} = require("path-to-regexp");
|
||||
const bodyParser = require("body-parser");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const qs = require("qs");
|
||||
const databaseHandler = require("./lib/database-handler");
|
||||
|
||||
(async () => {
|
||||
const app = express();
|
||||
const routes = await glob('**/*.js', {
|
||||
cwd: './routes',
|
||||
app.set("query parser", "extended");
|
||||
app.use(bodyParser.json({
|
||||
type: "application/*+json",
|
||||
verify (req, _res, buf) {
|
||||
req.rawBody = buf;
|
||||
},
|
||||
}));
|
||||
app.use(bodyParser.json({
|
||||
verify (req, _res, buf) {
|
||||
req.rawBody = buf;
|
||||
},
|
||||
}));
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: false,
|
||||
verify (req, _res, buf) {
|
||||
req.rawBody = buf;
|
||||
},
|
||||
}));
|
||||
app.use(cookieParser());
|
||||
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) ) {
|
||||
app.use((req, _res, next) => {
|
||||
console.log(`${req.path}${Object.keys(req.query).length ? "?" : ""}${qs.stringify(req.query)}`);
|
||||
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 (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`) ) {
|
||||
} 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 !== '' ) {
|
||||
if (candidateUrl !== "") {
|
||||
req.url = candidateUrl;
|
||||
console.log(candidateUrl);
|
||||
return next();
|
||||
}
|
||||
if ( secondCandidateUrl !== '' ) {
|
||||
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}`));
|
||||
});
|
||||
for (const routeScript in routes) {
|
||||
const route = routes[routeScript].replace(/\.js$/, "");
|
||||
pathMatches.push(createPathMatch(`/${route}`));
|
||||
const routeObj = require(`./routes/${route}`);
|
||||
if ( routeObj.get ) {
|
||||
app.get(`/${route}`, routeObj.get);
|
||||
if (routeObj.route) {
|
||||
routeObj.route(app.route(`/${route}`));
|
||||
}
|
||||
}
|
||||
app.listen(process.env.PORT || 3000);
|
||||
const server = app.listen(process.env.PORT || 3000, () => {
|
||||
process.on("SIGINT", () => {
|
||||
databaseHandler.db.close();
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
3
bin/create_account.js
Normal file
3
bin/create_account.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
const database_handler = require("../lib/database-handler");
|
||||
|
||||
database_handler.createAccount(process.argv[2], process.argv[3], process.argv[4]);
|
6
database.json
Normal file
6
database.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"dev": {
|
||||
"driver": "sqlite3",
|
||||
"filename": "brainz-social.db"
|
||||
}
|
||||
}
|
6
jsconfig.json
Normal file
6
jsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS"
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
34
lib/activitypub.js
Normal file
34
lib/activitypub.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const jsonld = require("jsonld");
|
||||
const databaseHandler = require("./database-handler");
|
||||
|
||||
module.exports = {
|
||||
jsonldCustomLoader: async (url, options) => {
|
||||
const cache = databaseHandler.getJsonldSchemaCache(url);
|
||||
if (cache && cache.schema) {
|
||||
return {
|
||||
contextUrl: null,
|
||||
document: JSON.parse(cache.schema),
|
||||
documentUrl: url,
|
||||
};
|
||||
}
|
||||
// TODO: Write HTTP client handler.
|
||||
const retData = await jsonld.documentLoaders.node()(url);
|
||||
databaseHandler.storeJsonldSchemaCache(url, JSON.stringify(retData.document), (Math.floor(Date.now()/1000) + 86400));
|
||||
return retData;
|
||||
},
|
||||
|
||||
compactedForm: async (urlOrObj) => {
|
||||
if (typeof urlOrObj === "string") {
|
||||
try {
|
||||
const url = new URL(urlOrObj);
|
||||
return jsonld.compact(await jsonld.expand(jsonld.documentLoaders.node()(url)), {});
|
||||
} catch (e) {
|
||||
return jsonld.compact(await jsonld.expand(JSON.parse(urlOrObj)), {});
|
||||
}
|
||||
} else {
|
||||
return jsonld.compact(await jsonld.expand(urlOrObj), {});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
jsonld.documentLoader = module.exports.jsonldCustomLoader;
|
166
lib/database-handler.js
Normal file
166
lib/database-handler.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
const db = require("better-sqlite3")("brainz-social.db");
|
||||
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
|
||||
application: new Proxy({}, {
|
||||
get (target, key) {
|
||||
if (typeof(key) === "Number") {
|
||||
return db.prepare("SELECT * FROM applications WHERE id = ?").get(key);
|
||||
} else {
|
||||
return db.prepare("SELECT * FROM applications WHERE client_id = ?").get(key);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
config: {
|
||||
getStatement: "SELECT * FROM config",
|
||||
|
||||
getByKey: (key) => {
|
||||
return db.prepare(module.exports.config.getStatement + " WHERE key = ?").get(key)?.value ?? null;
|
||||
},
|
||||
},
|
||||
|
||||
getConfig: (key) => {
|
||||
return module.exports.config.getByKey(key);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
createToken: (token, scope, application_id, user_id, created_at) => {
|
||||
db.prepare("INSERT INTO oauth_tokens (token, scopes, application_id, user_id, created_at, revoked) VALUES (?, ?, ?, ?, ?, false)").run(token, scope, application_id, user_id, created_at);
|
||||
},
|
||||
|
||||
getTokenData: (token) => {
|
||||
return db.prepare("SELECT application_id, user_id, created_at, revoked, scopes, token FROM oauth_tokens WHERE token = ?").get(token);
|
||||
},
|
||||
|
||||
revokeToken: (token) => {
|
||||
db.prepare("UPDATE oauth_tokens SET revoked = true WHERE token = ?").run(token);
|
||||
},
|
||||
|
||||
getAccountByUsername: (username) => {
|
||||
return db.prepare("SELECT id, username, email, password_hash, account_tier, created_at FROM accounts WHERE username = ?").get(username);
|
||||
},
|
||||
|
||||
createAccount: (username, email, password_hash) => {
|
||||
db.prepare("INSERT INTO accounts (username, email, password_hash, account_tier, created_at) VALUES (?, ?, ?, 0)").run(username, email, password_hash, Date.now());
|
||||
},
|
||||
|
||||
checkAuthCookie: (cookie_value) => {
|
||||
return db.prepare("SELECT id, cookie_value, created_at, user_id, revoked FROM cookies WHERE cookie_value = ?").get(cookie_value);
|
||||
},
|
||||
|
||||
revokeAuthCookie: (cookie_value) => {
|
||||
db.prepare("UPDATE cookies SET revoked = true WHERE cookie_value = ?").run(cookie_value);
|
||||
},
|
||||
|
||||
createAuthCookie: (cookie_value, created_at, user_id) => {
|
||||
db.prepare("INSERT INTO cookies (cookie_value, created_at, user_id, revoked) VALUES (?, ?, ?, false)").run(cookie_value, created_at, user_id);
|
||||
},
|
||||
|
||||
createCsrfToken: (url, created_at, cookie_value) => {
|
||||
const db_row_id = db.prepare("INSERT INTO csrf_token (url, created_at, cookie_value) VALUES (?, ?, ?)").run(url, created_at, cookie_value).lastInsertRowid;
|
||||
return db.prepare("SELECT id FROM csrf_token WHERE rowid = ?").get(db_row_id);
|
||||
},
|
||||
|
||||
createCsrfTokenAssociation: (...ids) => {
|
||||
for (const source_id in ids) {
|
||||
if (Number.parseInt(ids[source_id], 10) === ids[source_id]) {
|
||||
for (const destination_id in ids) {
|
||||
if (Number.parseInt(ids[destination_id], 10) === ids[destination_id]) {
|
||||
db.prepare("INSERT INTO csrf_token_relations (source_id, destination_id) VALUES (?, ?)").run(ids[source_id], ids[destination_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeAssociatedCsrfTokens: (id) => {
|
||||
db.prepare("DELETE FROM csrf_token WHERE id IN (SELECT destination_id AS id FROM csrf_token_relations WHERE source_id = ?)").run(id);
|
||||
},
|
||||
|
||||
getCsrfToken: (url) => {
|
||||
return db.prepare("SELECT id, url, created_at, cookie_value FROM csrf_token WHERE url = ?").get(url);
|
||||
},
|
||||
|
||||
createOauthCode: (code, application_id, user_id, scopes, created_at) => {
|
||||
db.prepare("INSERT INTO oauth_code (code, application_id, user_id, scopes, created_at, revoked) VALUES (?, ?, ?, ?, ?, false)").run(code, application_id, user_id, scopes, created_at);
|
||||
},
|
||||
|
||||
getOauthCode: (code) => {
|
||||
return db.prepare("SELECT code, application_id, user_id, scopes, created_at, revoked FROM oauth_code WHERE code = ?").get(code);
|
||||
},
|
||||
|
||||
revokeOauthCode: (code) => {
|
||||
db.prepare("UPDATE oauth_code SET revoked = true WHERE code = ?").run(code);
|
||||
},
|
||||
|
||||
selectApplicationByAuthToken: (token) => {
|
||||
return db.prepare("SELECT id, client_id, client_secret, redirect_uri, scopes, website FROM applications WHERE id in (SELECT application_id as id FROM oauth_tokens WHERE token = ?);").get(token);
|
||||
},
|
||||
|
||||
getVapidKey: () => {
|
||||
const vapidPublic = db.prepare("SELECT value FROM config WHERE key = vapid_key_public").get();
|
||||
const vapidPrivate = db.prepare("SELECT value FROM config WHERE key = vapid_key_private").get();
|
||||
if (vapidPublic.value && vapidPrivate.value) {
|
||||
return {public: vapidPublic, private: vapidPrivate};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
setVapidKey: (publicKey, privateKey) => {
|
||||
db.prepare("INSERT INTO config (key, value) VALUES (vapid_key_public, ?)").run(publicKey);
|
||||
db.prepare("INSERT INTO config (key, value) VALUES (vapid_key_private, ?)").run(privateKey);
|
||||
},
|
||||
|
||||
getJsonldSchemaCache: (url) => {
|
||||
return db.prepare("SELECT schema FROM jsonld_schema_cache WHERE schema_uri = ? AND expires > ?").get(url, Math.floor(Date.now() / 1000));
|
||||
},
|
||||
|
||||
storeJsonldSchemaCache: (url, schema, expiry) => {
|
||||
db.prepare("INSERT INTO jsonld_schema_cache (schema, schema_uri, expires) VALUES (?, ?, ?)").run(schema, url, expiry);
|
||||
},
|
||||
|
||||
getAccountByToken: (token) => {
|
||||
return db.prepare("SELECT id, username, email, password_hash, account_tier, created_at FROM accounts WHERE id IN (SELECT user_id FROM oauth_tokens WHERE token = ?)").get(token);
|
||||
},
|
||||
|
||||
getAccountActivityByAccount: (user_id) => {
|
||||
return db.prepare("SELECT id, object, type, local, uri_id, owner FROM activity_objects WHERE (type = 'https://www.w3.org/ns/activitystreams#Person' OR type = 'https://www.w3.org/ns/activitystreams#Service' OR type = 'https://www.w3.org/ns/activitystreams#Group') AND local = true AND owner in (SELECT username FROM accounts WHERE id = ?)").get(user_id);
|
||||
},
|
||||
|
||||
addActivity: (object, type, local, uri_id, owner, created_at) => {
|
||||
db.prepare("INSERT INTO activity_objects (object, type, local, uri_id, owner, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(object, type, local, uri_id, owner, created_at);
|
||||
},
|
||||
|
||||
activity: new Proxy({}, {
|
||||
get: (target, key) => {
|
||||
return db.prepare("SELECT * FROM activity_objects WHERE uri_id = ?").get(key);
|
||||
},
|
||||
|
||||
set: (target, key, value) => {
|
||||
db.prepare("INSERT INTO activity_objects (object, type, local, uri_id, owner, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(value.object, value.type, value.local.toString(), key, value.owner, value.created_at);
|
||||
},
|
||||
}),
|
||||
|
||||
getLastStatus: (owner) => {
|
||||
return db.prepare("SELECT created_at FROM activity_objects WHERE type = 'https://www.w3.org/ns/activitystreams#Note' AND owner = ? ORDER BY created_at DESC").get(owner);
|
||||
},
|
||||
|
||||
getStatusCount: (owner) => {
|
||||
return db.prepare("SELECT COUNT(*) AS count FROM activity_objects WHERE type = 'https://www.w3.org/ns/activitystreams#Note' AND owner = ?").get(owner);
|
||||
},
|
||||
|
||||
storeW3idSecurityKey: (key_uri, publicKey, privateKey, expires) => {
|
||||
db.prepare("INSERT INTO w3id_security_keys (key_uri, public_key, private_key, expires) VALUES (?, ?, ?, ?)").run(key_uri, publicKey, privateKey, expires);
|
||||
},
|
||||
};
|
56
lib/input_validate.js
Normal file
56
lib/input_validate.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
module.exports = {
|
||||
validate_exists: (body, fields) => {
|
||||
if (!body) {
|
||||
return {error: "Validation failed, no body provided."};
|
||||
}
|
||||
|
||||
for (const field in fields) {
|
||||
if (!body[fields[field]]) {
|
||||
return {error: `Validation failed, field ${field} missing from request.`};
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validate_appropriate_scopes: (max_scope, scope_requested) => {
|
||||
const max_scope_array_temp = max_scope.split(/[\s+]+/);
|
||||
const max_scope_array = [];
|
||||
for (const scope in max_scope_array_temp) {
|
||||
if (max_scope_array_temp[scope].match(/[a-zA-Z0-9:]/)) {
|
||||
max_scope_array.push(max_scope_array_temp[scope]);
|
||||
if (max_scope_array_temp[scope] === "read") {
|
||||
max_scope_array.push("read:accounts", "read:blocks", "read:bookmarks", "read:favorites", "read:filters", "read:follows", "read:lists", "read:mutes", "read:notifications", "read:search", "read:statuses");
|
||||
}
|
||||
|
||||
if (max_scope_array_temp[scope] === "write") {
|
||||
max_scope_array.push("write:accounts", "write:blocks", "write:bookmarks", "write:conversations", "write:favourites", "write:filters", "write:follows", "write:lists", "write:media", "write:mutes", "write:notifications", "write:reports", "write:statuses");
|
||||
}
|
||||
|
||||
if (max_scope_array_temp[scope] === "follow") {
|
||||
max_scope_array.push("read:blocks", "write:blocks", "read:follows", "write:follows", "read:mutes", "write:mutes");
|
||||
}
|
||||
|
||||
if (max_scope_array_temp[scope] === "admin:read") {
|
||||
max_scope_array.push("admin:read:accounts", "admin:read:reports", "admin:read:domain_allows", "admin:read:domain_blocks", "admin:read:ip_blocks", "admin:read:email_domain_blocks", "admin:read:canonical_email_blocks");
|
||||
}
|
||||
|
||||
if (max_scope_array_temp[scope] === "admin:write") {
|
||||
max_scope_array.push("admin:write:accounts", "admin:write:reports", "admin:write:domain_allows", "admin:write:domain_blocks", "admin:write:ip_blocks", "admin:write:email_domain_blocks", "admin:write:canonical_email_blocks");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scope_requested_array = scope_requested.split(/[\s+]+/);
|
||||
|
||||
for (const scope in scope_requested_array) {
|
||||
if (max_scope_array.includes(scope_requested_array[scope])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
53
middleware/auth.js
Normal file
53
middleware/auth.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
const databaseHandler = require("../lib/database-handler");
|
||||
const input_validate = require("../lib/input_validate");
|
||||
|
||||
module.exports = {
|
||||
auth_token: (needs_user, need_scopes) => {
|
||||
return (req, res, next) => {
|
||||
const token = databaseHandler.getTokenData(req.header("authorization").split(/\s+/)[1]);
|
||||
if (!token) {
|
||||
res.status(401);
|
||||
res.json({
|
||||
error: "UNAUTHENTICATED",
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.revoked) {
|
||||
res.status(401);
|
||||
res.json({
|
||||
error: "UNAUTHENTICATED",
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (needs_user && token.user_id === 0) {
|
||||
res.status(401);
|
||||
res.json({
|
||||
error: "INSUFFICIENT_AUTHORIZATION",
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (need_scopes && !input_validate.validate_appropriate_scopes(token.scopes, need_scopes)) {
|
||||
res.status(401);
|
||||
res.json({
|
||||
error: "INSUFFICIENT_SCOPE",
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.brainz) {
|
||||
req.brainz = {};
|
||||
}
|
||||
|
||||
req.brainz.token = token;
|
||||
|
||||
next();
|
||||
};
|
||||
},
|
||||
};
|
43
migrations/20230926224347-applications.js
Normal file
43
migrations/20230926224347-applications.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
"use strict";
|
||||
|
||||
const async = require("async");
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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,
|
||||
};
|
45
migrations/20230927130047-tokens.js
Normal file
45
migrations/20230927130047-tokens.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
"use strict";
|
||||
|
||||
const async = require("async");
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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, "oauth_tokens", {
|
||||
id: {type: "int", primaryKey: true, autoIncrements: true},
|
||||
token: {type: "string", unique: true},
|
||||
application_id: "int",
|
||||
scopes: "string",
|
||||
user_id: "int",
|
||||
revoked: "boolean",
|
||||
created_at: "int",
|
||||
}),
|
||||
db.addIndex.bind(db, "oauth_tokens", "oauth_token_index", ["token"]),
|
||||
db.addIndex.bind(db, "oauth_tokens", "oauth_token_user_id_index", ["user_id"]),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
async.series([
|
||||
db.removeIndex.bind(db, "oauth_tokens", "oauth_token_user_id_index"),
|
||||
db.removeIndex.bind(db, "oauth_tokens", "oauth_token_index"),
|
||||
db.dropTable("oauth_tokens"),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
39
migrations/20230928130221-accounts.js
Normal file
39
migrations/20230928130221-accounts.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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) {
|
||||
db.createTable("accounts", {
|
||||
id: {type: "int", primaryKey: true, autoIncrement: true},
|
||||
username: {type: "string", unique: true},
|
||||
email: "string",
|
||||
password_hash: "string",
|
||||
account_tier: "int",
|
||||
}, (result) => {
|
||||
if (result) {
|
||||
callback(result);
|
||||
}
|
||||
|
||||
db.runSql("INSERT INTO \"accounts\" (\"username\",\"email\",\"password_hash\",\"account_tier\") VALUES (\"guest\",\"null@null.null\",\"purposely_invalid_password\",0);", callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.dropTable("accounts");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
41
migrations/20230930200651-cookies.js
Normal file
41
migrations/20230930200651-cookies.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
"use strict";
|
||||
|
||||
const async = require("async");
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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, "cookies", {
|
||||
id: {type: "int", primaryKey: true, autoIncrement: true},
|
||||
cookie_value: {type: "string", unique: true},
|
||||
created_at: "int",
|
||||
user_id: "int",
|
||||
revoked: "boolean",
|
||||
}),
|
||||
db.addIndex.bind(db, "cookies", "cookies_cookie_value_index", ["cookie_value"]),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
async.series([
|
||||
db.removeIndex.bind(db, "cookies", "cookies_cookie_value_index"),
|
||||
db.dropTable.bind(db, "cookies"),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
31
migrations/20231001203802-csrfToken.js
Normal file
31
migrations/20231001203802-csrfToken.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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("csrf_token", {
|
||||
id: {type: "int", primaryKey: true, autoIncrement: true},
|
||||
url: "string",
|
||||
created_at: "int",
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.dropTable("csrf_token");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
31
migrations/20231001204050-csrfTokenRelated.js
Normal file
31
migrations/20231001204050-csrfTokenRelated.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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("csrf_token_relations", {
|
||||
id: {type: "int", primaryKey: true, autoIncrement: true},
|
||||
source_id: "int",
|
||||
destination_id: "int",
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.dropTable("csrf_token_relations");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
27
migrations/20231002003412-csrfTokenCookieAssociation.js
Normal file
27
migrations/20231002003412-csrfTokenCookieAssociation.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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.addColumn("csrf_token", "cookie_value", "string");
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.removeColumn("csrf_token", "cookie_value");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
42
migrations/20231002234759-oauthCode.js
Normal file
42
migrations/20231002234759-oauthCode.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
"use strict";
|
||||
|
||||
const async = require("async");
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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, "oauth_code", {
|
||||
id: {type: "int", primaryKey: true, autoIncrements: true},
|
||||
code: {type: "string", unique: true},
|
||||
application_id: "int",
|
||||
scopes: "string",
|
||||
user_id: "int",
|
||||
created_at: "int",
|
||||
}),
|
||||
db.addIndex.bind(db, "oauth_code", "oauth_code_index", ["code"]),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
async.series([
|
||||
db.removeIndex.bind(db, "oauth_code", "oauth_code_index"),
|
||||
db.dropTable.bind(db, "oauth_code"),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
27
migrations/20231004052157-oauthCodeRevokable.js
Normal file
27
migrations/20231004052157-oauthCodeRevokable.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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.addColumn("oauth_code", "revoked", "boolean");
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.removeColumn("oauth_code", "revoked");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
32
migrations/20231007184818-jsonldSchemaCache.js
Normal file
32
migrations/20231007184818-jsonldSchemaCache.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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("jsonld_schema_cache", {
|
||||
id: {type: "int", primaryKey: true, autoIncrements: true},
|
||||
schema_uri: "string",
|
||||
schema: "string",
|
||||
expires: "int",
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.dropTable("jsonld_schema_cache");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
34
migrations/20231008035624-activityObjects.js
Normal file
34
migrations/20231008035624-activityObjects.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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("activity_objects", {
|
||||
id: {type: "int", primaryKey: true, autoIncrements: true},
|
||||
object: "string",
|
||||
type: "string",
|
||||
local: "boolean",
|
||||
uri_id: "string",
|
||||
owner: "string",
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.dropTable("activity_objects");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
27
migrations/20231009020010-activityObjectCreatedAt.js
Normal file
27
migrations/20231009020010-activityObjectCreatedAt.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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.addColumn("activity_objects", "created_at", "string");
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.removeColumn("activity_objects", "created_at");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
33
migrations/20231009024300-w3idSecurityKeys.js
Normal file
33
migrations/20231009024300-w3idSecurityKeys.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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("w3id_security_keys", {
|
||||
id: {type: "int", primaryKey: true, autoIncrements: true},
|
||||
key_uri: "string",
|
||||
public_key: "string",
|
||||
private_key: "string",
|
||||
expires: "int",
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.dropTable("w3id_security_keys");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
30
migrations/20231120012518-accountCreatedAtColumn.js
Normal file
30
migrations/20231120012518-accountCreatedAtColumn.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
"use strict";
|
||||
|
||||
let dbm;
|
||||
let type;
|
||||
let 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.addColumn("accounts", "created_at", {
|
||||
type: "string",
|
||||
default: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (db) {
|
||||
return db.removeColumn("accounts", "created_at");
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
version: 1,
|
||||
};
|
4632
package-lock.json
generated
4632
package-lock.json
generated
File diff suppressed because it is too large
Load diff
29
package.json
29
package.json
|
@ -1,24 +1,33 @@
|
|||
{
|
||||
"name": "haxsocial",
|
||||
"name": "brainz-social",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"description": "Brainz Social",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://forgejo.hax.social/rallias/haxsocial.git"
|
||||
},
|
||||
"author": "Andrew Pietila <a.pietila@protonmail.com>",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"async": "^3.2.4",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^8.6.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"db-migrate": "^0.11.14",
|
||||
"db-migrate-sqlite3": "^0.5.0",
|
||||
"express": "^4.18.2",
|
||||
"glob": "^9.3.0",
|
||||
"path-to-regexp": "^6.2.1"
|
||||
"glob": "^10.3.10",
|
||||
"jsonld": "^8.3.1",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"qs": "^6.11.2",
|
||||
"sanitize-html": "^2.11.0",
|
||||
"web-push": "^3.6.6"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"devDependencies": {
|
||||
"eslint": "^8.36.0",
|
||||
"license-checker": "^25.0.1"
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.28.1"
|
||||
}
|
||||
}
|
||||
|
|
48
routes/api/v1/accounts.js
Normal file
48
routes/api/v1/accounts.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
const crypto = require("crypto");
|
||||
const bcrypt = require("bcrypt");
|
||||
const databaseHandler = require("../../../lib/database-handler");
|
||||
const input_validate = require("../../../lib/input_validate");
|
||||
const auth = require("../../../middleware/auth");
|
||||
|
||||
module.exports = {
|
||||
route: ((routeObj) => {
|
||||
routeObj.post(auth.auth_token(false, "write:accounts"), (req, res) => {
|
||||
const authToken = req.header("Authorization").split(/\s+/)[1];
|
||||
|
||||
const validation_result = input_validate.validate_exists(req.body, ["username", "email", "password", "agreement", "locale"]);
|
||||
if (validation_result !== true) {
|
||||
res.status(422);
|
||||
res.json(validation_result);
|
||||
return;
|
||||
}
|
||||
|
||||
const username = req.body.username;
|
||||
if (databaseHandler.getAccountByUsername(username)) {
|
||||
res.status(422);
|
||||
res.json({error: "Validation failed, username taken.", details: {username: {error: "ERR_TAKEN", description: "Username taken."}}});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: We're just taking emails at face value for now.
|
||||
const email = req.body.email;
|
||||
|
||||
const password = req.body.password;
|
||||
|
||||
const password_hash = bcrypt.hashSync(password, bcrypt.genSaltSync());
|
||||
databaseHandler.createAccount(username, email, password_hash);
|
||||
|
||||
const userObject = databaseHandler.getAccountByUsername(username);
|
||||
const userToken = crypto.randomBytes(32).toString("base64");
|
||||
const created_at = Math.floor(Date.now() / 1000);
|
||||
const application = databaseHandler.application[authToken.application_id];
|
||||
databaseHandler.createToken(userToken, application.scopes, application.id, userObject.id, created_at);
|
||||
res.status(200);
|
||||
res.json({
|
||||
access_token: userToken,
|
||||
token_type: "Bearer",
|
||||
scope: application.scopes,
|
||||
created_at,
|
||||
});
|
||||
});
|
||||
}),
|
||||
};
|
125
routes/api/v1/accounts/verify_credentials.js
Normal file
125
routes/api/v1/accounts/verify_credentials.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
const crypto = require("crypto");
|
||||
const databaseHandler = require("../../../../lib/database-handler");
|
||||
const auth = require("../../../../middleware/auth");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get(auth.auth_token(true, "read:accounts"), (req, res) => {
|
||||
const token = req.brainz.token;
|
||||
const account = databaseHandler.getAccountByToken(token.token);
|
||||
const accountActivityRow = databaseHandler.getAccountActivityByAccount(account.id);
|
||||
let accountActivity = {};
|
||||
if (accountActivityRow) {
|
||||
accountActivity = JSON.parse(accountActivityRow.object);
|
||||
} else {
|
||||
const keyPair = crypto.generateKeyPairSync("rsa", {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: "spki",
|
||||
format: "pem",
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8",
|
||||
format: "pem",
|
||||
},
|
||||
});
|
||||
|
||||
databaseHandler.storeW3idSecurityKey(`https://${req.headers.host}/users/${account.username}#main-key`, keyPair.publicKey, keyPair.privateKey, (Math.floor(Date.now() / 1000) + (86400 * 90)));
|
||||
|
||||
accountActivity = {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}`,
|
||||
"@type": "https://www.w3.org/ns/activitystreams#Person",
|
||||
"http://joinmastodon.org/ns#devices": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/collections/devices`,
|
||||
},
|
||||
"http://joinmastodon.org/ns#discoverable": true,
|
||||
"http://joinmastodon.org/ns#featured": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/collections/featured`,
|
||||
},
|
||||
"http://joinmastodon.org/ns#featuredTags": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/collections/tags`,
|
||||
},
|
||||
"http://joinmastodon.org/ns#indexable": true,
|
||||
"http://joinmastodon.org/ns#memorial": false,
|
||||
"http://www.w3.org/ns/ldp#inbox": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/inbox`,
|
||||
},
|
||||
"https://w3id.org/security#publicKey": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}#main-key`,
|
||||
"https://w3id.org/security#owner": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}`,
|
||||
},
|
||||
"https://w3id.org/security#publicKeyPem": keyPair.publicKey,
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#endpoints": {
|
||||
"https://www.w3.org/ns/activitystreams#sharedInbox": {
|
||||
"@id": `https://${req.headers.host}/inbox`,
|
||||
},
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#followers": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/followers`,
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#following": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/following`,
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#icon": {
|
||||
"@type": "https://www.w3.org/ns/activitystreams#Image",
|
||||
"https://www.w3.org/ns/activitystreams#mediaType": "image/png",
|
||||
"https://www.w3.org/ns/activitystreams#url": {
|
||||
"@id": `https://${req.headers.host}/res/avatar_not_found.png`,
|
||||
},
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#manuallyApprovesFollowers": false,
|
||||
"https://www.w3.org/ns/activitystreams#name": account.username,
|
||||
"https://www.w3.org/ns/activitystreams#outbox": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/outbox`,
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#preferredUsername": account.username,
|
||||
"https://www.w3.org/ns/activitystreams#published": {
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
|
||||
"@value": new Date(Date.now()).toISOString(),
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#summary": "",
|
||||
"https://www.w3.org/ns/activitystreams#tag": [],
|
||||
"https://www.w3.org/ns/activitystreams#url": {
|
||||
"@id": `https://${req.headers.host}/@${account.username}`,
|
||||
},
|
||||
};
|
||||
databaseHandler.addActivity(JSON.stringify(accountActivity), accountActivity["@type"], "true", accountActivity["@id"], account.username, (new Date(Number(account.created_at))).toISOString());
|
||||
}
|
||||
|
||||
const last_status = databaseHandler.getLastStatus(account.username);
|
||||
res.status(200);
|
||||
res.json({
|
||||
id: accountActivity["@id"],
|
||||
username: account.username,
|
||||
acct: account.username,
|
||||
url: accountActivity["https://www.w3.org/ns/activitystreams#url"]["@id"],
|
||||
display_name: accountActivity["https://www.w3.org/ns/activitystreams#preferredUsername"],
|
||||
avatar: accountActivity["https://www.w3.org/ns/activitystreams#icon"]["https://www.w3.org/ns/activitystreams#url"]["@id"],
|
||||
avatar_static: accountActivity["https://www.w3.org/ns/activitystreams#icon"]["https://www.w3.org/ns/activitystreams#url"]["@id"],
|
||||
header: accountActivity["https://www.w3.org/ns/activitystreams#icon"]["https://www.w3.org/ns/activitystreams#url"]["@id"],
|
||||
header_static: accountActivity["https://www.w3.org/ns/activitystreams#icon"]["https://www.w3.org/ns/activitystreams#url"]["@id"],
|
||||
locked: accountActivity["https://www.w3.org/ns/activitystreams#manuallyApprovesFollowers"],
|
||||
emojis: [],
|
||||
bot: accountActivity["@type"] === "https://www.w3.org/ns/activitystreams#Service",
|
||||
group: accountActivity["@type"] === "https://www.w3.org/ns/activitystreams#Group",
|
||||
discoverable: accountActivity["http://joinmastodon.org/ns#discoverable"],
|
||||
created_at: accountActivity["https://www.w3.org/ns/activitystreams#published"]["@value"],
|
||||
last_status_at: last_status ? last_status.created_at : null,
|
||||
statuses_count: databaseHandler.getStatusCount(account.username).count,
|
||||
// TODO: Proper followers and following count.
|
||||
followers_count: -1,
|
||||
following_count: -1,
|
||||
source: {
|
||||
note: "",
|
||||
fields: [],
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
language: "en",
|
||||
},
|
||||
});
|
||||
return;
|
||||
});
|
||||
},
|
||||
};
|
79
routes/api/v1/apps.js
Normal file
79
routes/api/v1/apps.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const crypto = require("crypto");
|
||||
const webpush = require("web-push");
|
||||
const database_handler = require("../../../lib/database-handler");
|
||||
const input_validate = require("../../../lib/input_validate");
|
||||
|
||||
module.exports = {
|
||||
route: ((routeObj) => {
|
||||
routeObj.post((req, res) => {
|
||||
const validation_result = input_validate.validate_exists(req.body, ["client_name", "redirect_uris"]);
|
||||
if (validation_result !== true) {
|
||||
res.status(422);
|
||||
res.json(validation_result);
|
||||
return;
|
||||
}
|
||||
|
||||
let scopes;
|
||||
let website;
|
||||
const client_name = req.body.client_name;
|
||||
const redirect_uris = req.body.redirect_uris;
|
||||
|
||||
if (redirect_uris !== "urn:ietf:wg:oauth:2.0:oob") {
|
||||
let 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.protocol.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;
|
||||
}
|
||||
|
||||
let 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;
|
||||
}
|
||||
|
||||
const client_id = crypto.randomBytes(32).toString("base64");
|
||||
const 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;
|
||||
});
|
||||
}),
|
||||
};
|
30
routes/api/v1/apps/verify_credentials.js
Normal file
30
routes/api/v1/apps/verify_credentials.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const webpush = require("web-push");
|
||||
const databaseHandler = require("../../../../lib/database-handler");
|
||||
const auth_middleware = require("../../../../middleware/auth");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get(auth_middleware.auth_token(false, ""), (req, res) => {
|
||||
const token = req.header("Authorization").split(/\s+/)[1];
|
||||
const application = databaseHandler.getApplicationByToken(token);
|
||||
let vapid_key = databaseHandler.getVapidKey();
|
||||
|
||||
if (vapid_key) {
|
||||
vapid_key = vapid_key["public"];
|
||||
}
|
||||
if (!vapid_key) {
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
databaseHandler.setVapidKey(vapidKeys.publicKey, vapidKeys.privateKey);
|
||||
vapid_key = vapidKeys.publicKey;
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.json({
|
||||
name: application.name,
|
||||
website: application.website,
|
||||
vapid_key: vapid_key["public"],
|
||||
});
|
||||
return;
|
||||
});
|
||||
},
|
||||
};
|
9
routes/api/v1/filters.js
Normal file
9
routes/api/v1/filters.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const auth = require("../../../middleware/auth");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get(auth.auth_token(true, "read:filters"), (req, res) => {
|
||||
return res.json([]);
|
||||
});
|
||||
},
|
||||
};
|
55
routes/api/v1/instance.js
Normal file
55
routes/api/v1/instance.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get((req, res) => {
|
||||
res.status(200);
|
||||
res.json({
|
||||
uri: req.headers.host,
|
||||
title: "Brainz Social",
|
||||
short_description: "",
|
||||
description: "",
|
||||
email: "a.pietila@protonmail.com",
|
||||
version: "4.2.0",
|
||||
urls: {},
|
||||
stats: {
|
||||
user_count: -1,
|
||||
status_count: -1,
|
||||
domain_count: -1
|
||||
},
|
||||
thumbnail: null,
|
||||
languages: [
|
||||
"en"
|
||||
],
|
||||
registrations: true,
|
||||
approval_required: false,
|
||||
invites_enabled: false,
|
||||
configuration: {
|
||||
accounts: {
|
||||
max_featured_tags: 4,
|
||||
},
|
||||
statuses: {
|
||||
max_characters: 500,
|
||||
max_media_attachments: 0,
|
||||
characters_reserved_per_url: 25
|
||||
},
|
||||
media_attachments: {
|
||||
supported_mime_types: [],
|
||||
image_size_limit: 0,
|
||||
image_matrix_limit: 0,
|
||||
video_size_limit: 0,
|
||||
video_frame_rate_limit: 0,
|
||||
video_matrix_limit: 0,
|
||||
},
|
||||
polls: {
|
||||
max_options: 0,
|
||||
max_characters_per_option: 0,
|
||||
min_expiration: Number.MAX_SAFE_INTEGER,
|
||||
max_expiration: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
contact_account: {},
|
||||
rules: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
});
|
||||
},
|
||||
};
|
9
routes/api/v1/notifications.js
Normal file
9
routes/api/v1/notifications.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const auth = require("../../../middleware/auth");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get(auth.auth_token(true, "read:statuses"), (req, res) => {
|
||||
return res.json([]);
|
||||
});
|
||||
},
|
||||
};
|
0
routes/api/v1/preferences.js
Normal file
0
routes/api/v1/preferences.js
Normal file
107
routes/api/v1/statuses.js
Normal file
107
routes/api/v1/statuses.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
const sanitizeHtml = require("sanitize-html");
|
||||
const auth = require("../../../middleware/auth");
|
||||
const databaseHandler = require("../../../lib/database-handler");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.post(auth.auth_token(true, "write:statuses"), (req, res) => {
|
||||
const id = process.hrtime.bigint().toString(10);
|
||||
const token = req.brainz.token;
|
||||
const account = databaseHandler.getAccountByToken(token.token);
|
||||
const created_at = Date.now();
|
||||
const statusText = sanitizeHtml(req.body.status, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {},
|
||||
});
|
||||
const status = {
|
||||
"@id": `https://${req.header.host}/users/${account.username}/statuses/${id}`,
|
||||
"@type": "https://www.w3.org/ns/activitystreams#Note",
|
||||
"http://ostatus.org#atomUri": `https://${req.header.host}/users/${account.username}/statuses/${id}`,
|
||||
"http://ostatus.org#conversation": null, // TODO: wut
|
||||
"http://ostatus.org#inReplyToAtomUri": null,
|
||||
"https://www.w3.org/ns/activitystreams#attachment": [],
|
||||
"https://www.w3.org/ns/activitystreams#attributedTo": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}`,
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#cc": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/followers`,
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#content": [
|
||||
`<p>${statusText.split("\n").join("</p><p>")}</p>`,
|
||||
],
|
||||
"https://www.w3.org/ns/activitystreams#inReplyTo": null,
|
||||
"https://www.w3.org/ns/activitystreams#published": {
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
|
||||
"@value": new Date(created_at).toISOString(),
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#replies": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/statuses/${id}/replies`,
|
||||
"@type": "https://www.w3.org/ns/activitystreams#Collection",
|
||||
"https://www.w3.org/ns/activitystreams#first": {
|
||||
"@type": "https://www.w3.org/ns/activitystreams#CollectionPage",
|
||||
"https://www.w3.org/ns/activitystreams#items": [],
|
||||
"https://www.w3.org/ns/activitystreams#next": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/statuses/${id}/replies?only_other_accounts=true&page=true`,
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#partOf": {
|
||||
"@id": `https://${req.headers.host}/users/${account.username}/statuses/${id}/replies`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#tag": [],
|
||||
"https://www.w3.org/ns/activitystreams#to": {
|
||||
"@id": "https://www.w3.org/ns/activitystreams#Public",
|
||||
},
|
||||
"https://www.w3.org/ns/activitystreams#url": {
|
||||
"@id": `https://${req.headers.host}/@${account.username}/${id}`,
|
||||
},
|
||||
};
|
||||
|
||||
status["https://www.w3.org/ns/activitystreams#summary"] = req.body.spoiler_text ?? "";
|
||||
|
||||
status["https://www.w3.org/ns/activitystreams#sensitive"] = req.body.spoiler_text ?? req.body.spoiler_text ? true : false;
|
||||
|
||||
databaseHandler.activity[`https://${req.header.host}/users/${account.username}/statuses/${id}`] = {
|
||||
object: JSON.stringify(status),
|
||||
type: status["@type"],
|
||||
local: true,
|
||||
owner: account.username,
|
||||
created_at,
|
||||
};
|
||||
|
||||
// TODO: Distribution queue.
|
||||
|
||||
const retObj = {
|
||||
id: status["@id"],
|
||||
created_at: status["https://www.w3.org/ns/activitystreams#published"]["@value"],
|
||||
in_reply_to_id: null,
|
||||
in_reply_to_account_id: null,
|
||||
sensitive: status["https://www.w3.org/ns/activitystreams#sensitive"],
|
||||
spoiler_text: status["https://www.w3.org/ns/activitystreams#summary"],
|
||||
visibility: "public", // TODO: Private posting capability.
|
||||
language: "en", // TODO: Other languages.
|
||||
uri: status["@id"],
|
||||
url: status["@id"],
|
||||
replies_count: 0, // Future copy paste jobs - These counts should be accurate.
|
||||
reblogs_count: 0,
|
||||
favourites_count: 0,
|
||||
favourited: false,
|
||||
reblogged: false,
|
||||
muted: false,
|
||||
bookmarked: false,
|
||||
content: status["https://www.w3.org/ns/activitystreams#content"][0],
|
||||
reblog: null,
|
||||
account: {},
|
||||
media_attachments: [], // TODO: Support media.
|
||||
mentions: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
card: null,
|
||||
poll: null,
|
||||
};
|
||||
|
||||
res.json(retObj);
|
||||
return;
|
||||
});
|
||||
},
|
||||
};
|
9
routes/api/v1/timelines/home.js
Normal file
9
routes/api/v1/timelines/home.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const auth = require("../../../../middleware/auth");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get(auth.auth_token(true, "read:statuses"), (req, res) => {
|
||||
return res.json([]);
|
||||
});
|
||||
},
|
||||
};
|
9
routes/api/v1/timelines/public.js
Normal file
9
routes/api/v1/timelines/public.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const auth = require("../../../../middleware/auth");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get(auth.auth_token(false, ""), (req, res) => {
|
||||
return res.json([]);
|
||||
});
|
||||
},
|
||||
};
|
94
routes/login.js
Normal file
94
routes/login.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
const crypto = require("crypto");
|
||||
const bcrypt = require("bcrypt");
|
||||
const databaseHandler = require("../lib/database-handler");
|
||||
const input_validate = require("../lib/input_validate");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get((req, res) => {
|
||||
if (req.cookies && req.cookies.auth !== undefined) {
|
||||
const cookie_db = databaseHandler.checkAuthCookie(req.cookies.auth);
|
||||
if (cookie_db && !cookie_db.revoked && cookie_db.created_at + 86400 > Math.floor(Date.now() / 1000)) {
|
||||
const new_cookie_value = crypto.randomBytes(32).toString("base64");
|
||||
databaseHandler.revokeAuthCookie(req.cookies.auth);
|
||||
databaseHandler.createAuthCookie(new_cookie_value, Math.floor(Date.now() / 1000), cookie_db.user_id);
|
||||
// TODO: Set correct redirect status.
|
||||
res.status(302);
|
||||
res.cookie("auth", new_cookie_value, {maxAge: 86400000, httpOnly: true});
|
||||
if (req.query && req.query.redirect_to) {
|
||||
// TODO: This should be validated so as to not go off-site without warning the user.
|
||||
res.redirect(req.query.redirect_to);
|
||||
} else {
|
||||
res.redirect("/");
|
||||
}
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.status(200);
|
||||
res.send(`<html><body><form action="/login" method="POST"><label for="username" >Username: </label><input type="text" id="username" name="username" /><br /><label for="password" >Password: </label><input type="password" id="password" name="password" /><br />${req.query.redirect_to ? `<input type="hidden" id="redirect_to" name="redirect_to" value="${req.query.redirect_to}" />` : ""}<input type="submit" /></form></body></html>`);
|
||||
res.end();
|
||||
});
|
||||
|
||||
routeObj.post((req, res) => {
|
||||
if (req.cookies && req.cookies.auth) {
|
||||
const cookie_db = databaseHandler.checkAuthCookie(req.cookies.auth);
|
||||
if (cookie_db && !cookie_db.revoked && cookie_db.created_at + 86400 > Math.floor(Date.now() / 1000)) {
|
||||
const new_cookie_value = crypto.randomBytes(32).toString("base64");
|
||||
databaseHandler.revokeAuthCookie(req.cookies.auth);
|
||||
databaseHandler.createAuthCookie(new_cookie_value, Math.floor(Date.now() / 1000), cookie_db.user_id);
|
||||
// TODO: Set correct redirect status.
|
||||
res.status(302);
|
||||
res.cookie("auth", new_cookie_value, {maxAge: 86400000, httpOnly: true});
|
||||
if (req.body && req.body.redirect_to) {
|
||||
// TODO: External site warning.
|
||||
res.redirect(req.body.redirect_to);
|
||||
} else {
|
||||
res.redirect("/");
|
||||
}
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const validation_result = input_validate.validate_exists(req.body, ["username", "password"]);
|
||||
|
||||
if (validation_result !== true) {
|
||||
res.status(422);
|
||||
res.json(validation_result);
|
||||
return;
|
||||
}
|
||||
|
||||
const user_obj = databaseHandler.getAccountByUsername(req.body.username);
|
||||
if (!user_obj) {
|
||||
res.status(401);
|
||||
// TODO: redirect_url
|
||||
res.send("<html><body>AUTH FAILURE<br /><form action=\"/login\" method=\"POST\"><label for=\"username\" >Username: </label><input type=\"text\" id=\"username\" name=\"username\" /><br /><label for=\"password\" >Password: </label><input type=\"password\" id=\"password\" name=\"password\" /><br /><input type=\"submit\" /></form></body></html>");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bcrypt.compareSync(req.body.password, user_obj.password_hash)) {
|
||||
const new_cookie_value = crypto.randomBytes(32).toString("base64");
|
||||
databaseHandler.createAuthCookie(new_cookie_value, Math.floor(Date.now() / 1000), user_obj.id);
|
||||
// TODO: Set correct redirect status.
|
||||
res.status(302);
|
||||
res.cookie("auth", new_cookie_value, {maxAge: 86400000, httpOnly: true});
|
||||
if (req.body && req.body.redirect_to) {
|
||||
// TODO: External site warning.
|
||||
res.redirect(req.body.redirect_to);
|
||||
} else {
|
||||
res.redirect("/");
|
||||
}
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(401);
|
||||
// TODO: redirect_url
|
||||
res.send("<html><body>AUTH FAILURE<br /><form action=\"/login\" method=\"POST\"><label for=\"username\" >Username: </label><input type=\"text\" id=\"username\" name=\"username\" /><br /><label for=\"password\" >Password: </label><input type=\"password\" id=\"password\" name=\"password\" /><br /><input type=\"submit\" /></form></body></html>");
|
||||
res.end();
|
||||
return;
|
||||
});
|
||||
},
|
||||
};
|
27
routes/nodeinfo/2.0.js
Normal file
27
routes/nodeinfo/2.0.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get((req, res) => {
|
||||
res.status(200);
|
||||
res.json({
|
||||
version: 2.0,
|
||||
software: {
|
||||
name: "Brainz Social",
|
||||
version: "0.0.1",
|
||||
},
|
||||
protocols: [],
|
||||
services: [],
|
||||
openRegistrations: true,
|
||||
usage: {
|
||||
users: {
|
||||
total: 0,
|
||||
activeHalfYear: 0,
|
||||
activeMonth: 0,
|
||||
},
|
||||
localPosts: 0,
|
||||
localComments: 0,
|
||||
},
|
||||
metadata: {},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
83
routes/oauth/authorize.js
Normal file
83
routes/oauth/authorize.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
const crypto = require("crypto");
|
||||
const qs = require("qs");
|
||||
const databaseHandler = require("../../lib/database-handler");
|
||||
const input_validate = require("../../lib/input_validate");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get((req, res) => {
|
||||
const validation_result = input_validate.validate_exists(req.query, ["response_type", "client_id", "redirect_uri"]);
|
||||
if (validation_result !== true) {
|
||||
res.status(422);
|
||||
res.send(`<html><body>Someone done goof'ed with your oauth request.<br />${validation_result.error}</body></html>`);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (!req.cookies || !req.cookies.auth) {
|
||||
const new_redirect_url = new URL("https://example.com");
|
||||
new_redirect_url.host = req.headers.host;
|
||||
new_redirect_url.pathname = req.path;
|
||||
new_redirect_url.search = qs.stringify(req.query);
|
||||
const redirecting_to = new URL("https://example.com");
|
||||
redirecting_to.host = req.headers.host;
|
||||
redirecting_to.pathname = "/login";
|
||||
redirecting_to.searchParams.set("redirect_to", new_redirect_url.toString());
|
||||
res.status(302);
|
||||
res.redirect(redirecting_to.toString());
|
||||
res.end();
|
||||
} else {
|
||||
const cookie_db = databaseHandler.checkAuthCookie(req.cookies.auth);
|
||||
if (cookie_db != null && !cookie_db.revoked && (cookie_db.created_at + 86400) > Math.floor(Date.now() / 1000)) {
|
||||
const new_cookie_value = crypto.randomBytes(32).toString("base64");
|
||||
databaseHandler.revokeAuthCookie(req.cookies.auth);
|
||||
databaseHandler.createAuthCookie(new_cookie_value, Math.floor(Date.now() / 1000), cookie_db.user_id);
|
||||
const application_obj = databaseHandler.application[req.query.client_id];
|
||||
let scope = "read";
|
||||
if (req.query.scope) {
|
||||
scope = req.query.scope;
|
||||
}
|
||||
if (!input_validate.validate_appropriate_scopes(application_obj.scopes, scope)) {
|
||||
res.status(422);
|
||||
res.cookie("auth", new_cookie_value);
|
||||
// TODO: XSS
|
||||
res.send(`<html><body>Error: invalid scopes requested. Contact the author of ${application_obj.client_name} to correct their scope request.</body></html>`);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.status(200);
|
||||
res.cookie("auth", new_cookie_value);
|
||||
const approve_url = new URL("https://example.com");
|
||||
approve_url.host = req.headers.host;
|
||||
approve_url.pathname = "/oauth/authorize/yes";
|
||||
approve_url.searchParams.set("response_type", req.query.response_type);
|
||||
approve_url.searchParams.set("client_id", req.query.client_id);
|
||||
approve_url.searchParams.set("redirect_uri", req.query.redirect_uri);
|
||||
approve_url.searchParams.set("scope", scope);
|
||||
const approve_csrf = crypto.randomBytes(32).toString("base64");
|
||||
approve_url.searchParams.set("csrf_token", approve_csrf);
|
||||
const deny_url = new URL(approve_url);
|
||||
deny_url.pathname = "/oauth/authorize/no";
|
||||
const deny_csrf = crypto.randomBytes(32).toString("base64");
|
||||
deny_url.searchParams.set("csrf_token", deny_csrf);
|
||||
const approve_csrf_id = databaseHandler.createCsrfToken(approve_url.toString(), Math.floor(Date.now() / 1000), new_cookie_value);
|
||||
const deny_csrf_id = databaseHandler.createCsrfToken(deny_url.toString(), Math.floor(Date.now() / 1000), new_cookie_value);
|
||||
databaseHandler.createCsrfTokenAssociation(approve_csrf_id, deny_csrf_id);
|
||||
res.send(`<html><body>The ${application_obj.client_name} application has requested the following scopes:<br /><ul><li>${scope.split(/(\s|\+)+/).join("</li><li>")}</li></ul><br /><a href="${approve_url.toString()}" >Approve</a><br /><a href="${deny_url.toString()}" >Deny</a></body></html>`);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const new_redirect_url = new URL("https://example.com");
|
||||
new_redirect_url.host = req.headers.host;
|
||||
new_redirect_url.pathname = req.path;
|
||||
new_redirect_url.search = qs.stringify(req.query);
|
||||
const redirecting_to = new URL("https://example.com");
|
||||
redirecting_to.host = req.headers.host;
|
||||
redirecting_to.pathname = "/login";
|
||||
redirecting_to.searchParams.set("redirect_to", new_redirect_url.toString());
|
||||
res.status(302);
|
||||
res.redirect(redirecting_to.toString());
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
57
routes/oauth/authorize/yes.js
Normal file
57
routes/oauth/authorize/yes.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
const crypto = require("crypto");
|
||||
const qs = require("qs");
|
||||
const databaseHandler = require("../../../lib/database-handler");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get((req, res) => {
|
||||
const request_url = new URL("https://example.com");
|
||||
request_url.host = req.headers.host;
|
||||
request_url.pathname = "/oauth/authorize/yes";
|
||||
request_url.search = new URLSearchParams(qs.stringify(req.query));
|
||||
const cookie_object = databaseHandler.checkAuthCookie(req.cookies.auth);
|
||||
if (cookie_object.revoked || (cookie_object.created_at + 86400) < Math.floor(Date.now() / 1000)) {
|
||||
res.status(302);
|
||||
const redirect_to = new URL("https://example.com");
|
||||
redirect_to.host = req.headers.host;
|
||||
redirect_to.pathname = "/login";
|
||||
redirect_to.searchParams.set("redirect_to", request_url.toString());
|
||||
res.redirect(redirect_to.toString());
|
||||
return;
|
||||
}
|
||||
databaseHandler.revokeAuthCookie(cookie_object.cookie_value);
|
||||
const new_cookie = crypto.randomBytes(32).toString("base64");
|
||||
databaseHandler.createAuthCookie(new_cookie, Math.floor(Date.now() / 1000), cookie_object.user_id);
|
||||
const csrf_token = databaseHandler.getCsrfToken(request_url.toString());
|
||||
if (csrf_token.cookie_value === req.cookies.auth && (Math.floor(Date.now() / 1000) < (csrf_token.created_at + 300))) {
|
||||
const response_code = crypto.randomBytes(32).toString("base64");
|
||||
const application_object = databaseHandler.application[req.query.client_id];
|
||||
databaseHandler.createOauthCode(response_code, application_object.id, cookie_object.user_id, req.query.scopes, Math.floor(Date.now() / 1000));
|
||||
if (req.query.redirect_uri === "urn:ietf:wg:oauth:2.0:oob") {
|
||||
res.status(200);
|
||||
res.cookie("auth", new_cookie);
|
||||
res.send(`<html><body>Your code: < ${response_code} >. Valid for 300 seconds.</body></html>`);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// TODO: Proper redirect code.
|
||||
res.status(302);
|
||||
const redirect_to = new URL(req.query.redirect_uri);
|
||||
redirect_to.searchParams.set("code", response_code);
|
||||
res.cookie("auth", new_cookie);
|
||||
res.redirect(redirect_to.toString());
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const redirect_to = new URL("http://example.com");
|
||||
redirect_to.host = req.headers.host;
|
||||
redirect_to.pathname = "/oauth/authorize";
|
||||
redirect_to.search = qs.stringify(req.query);
|
||||
res.status(302);
|
||||
res.redirect(redirect_to.toString());
|
||||
res.end();
|
||||
return;
|
||||
});
|
||||
},
|
||||
};
|
34
routes/oauth/revoke.js
Normal file
34
routes/oauth/revoke.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const databaseHandler = require("../../lib/database-handler");
|
||||
const input_validate = require("../../lib/input_validate");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.post((req, res) => {
|
||||
const validation_result = input_validate(req.body, ["client_id", "client_secret", "token"]);
|
||||
if (validation_result !== true) {
|
||||
res.status(422);
|
||||
res.json(validation_result);
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = databaseHandler.getTokenData(req.body.token);
|
||||
const application = databaseHandler.application[req.body.client_id];
|
||||
|
||||
if (application.client_secret !== req.body.client_secret) {
|
||||
res.status(401);
|
||||
res.json({error: "unauthorized_client", error_description: "You are not authorized to revoke this token."});
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokenData.application_id !== application.id) {
|
||||
res.status(401);
|
||||
res.json({error: "unauthorized_client", error_description: "You are not authorized to revoke this token."});
|
||||
return;
|
||||
}
|
||||
|
||||
databaseHandler.revokeToken(req.body.token);
|
||||
res.status(200);
|
||||
res.json({});
|
||||
});
|
||||
},
|
||||
};
|
99
routes/oauth/token.js
Normal file
99
routes/oauth/token.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
const crypto = require("crypto");
|
||||
const databaseHandler = require("../../lib/database-handler");
|
||||
const input_validate = require("../../lib/input_validate");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.post((req, res) => {
|
||||
let scope;
|
||||
|
||||
const validation_result = input_validate.validate_exists(req.body, ["grant_type", "client_id", "client_secret", "redirect_uri"]);
|
||||
if (validation_result !== true) {
|
||||
res.status(422);
|
||||
res.json(validation_result);
|
||||
return;
|
||||
}
|
||||
|
||||
const grant_type = req.body.grant_type;
|
||||
const client_id = req.body.client_id;
|
||||
const client_secret = req.body.client_secret;
|
||||
const redirect_uri = req.body.redirect_uri;
|
||||
// TODO: Validate redirect_uri for application.
|
||||
|
||||
scope = "read";
|
||||
|
||||
if (req.body.scope != undefined) {
|
||||
scope = req.body.scope.replace(/\+/, " ");
|
||||
}
|
||||
|
||||
const application = databaseHandler.application[client_id];
|
||||
|
||||
if (!application.client_id) {
|
||||
res.status(422);
|
||||
res.json({error: "UNREGISTERED_APPLICATION"});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
let application_scope = "read";
|
||||
if (application.scopes != undefined) {
|
||||
application_scope = application.scopes;
|
||||
}
|
||||
|
||||
if (client_secret !== application.client_secret) {
|
||||
res.status(401);
|
||||
res.json({error: "invalid_client", error_description: "client_id not known or does not match client_secret."});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input_validate.validate_appropriate_scopes(application_scope, scope)) {
|
||||
res.status(422);
|
||||
res.json({error: "Requested scopes in transaction application did not previously declare."});
|
||||
return;
|
||||
}
|
||||
|
||||
if (grant_type === "authorization_code") {
|
||||
const code = databaseHandler.getOauthCode(req.body.code);
|
||||
databaseHandler.revokeOauthCode(req.query.code);
|
||||
if (code.revoked) {
|
||||
res.status(401);
|
||||
res.json({error: "used_code", error_description: "Oauth codes are single use."});
|
||||
return;
|
||||
}
|
||||
|
||||
if ((code.created_at + 60) < Math.floor(Date.now() / 1000)) {
|
||||
res.status(401);
|
||||
res.json({error: "expired_code", error_description: "Oauth codes must be used within 60 seconds of issuance."});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(32).toString("base64");
|
||||
const created_at = Math.floor(Date.now() / 1000);
|
||||
databaseHandler.createToken(token, scope, application.id, code.user_id, created_at);
|
||||
res.status(200);
|
||||
res.json({
|
||||
access_token: token,
|
||||
token_type: "Bearer",
|
||||
scope: req.body.scope,
|
||||
created_at,
|
||||
});
|
||||
return;
|
||||
} if (grant_type !== "client_credentials") {
|
||||
res.status(422);
|
||||
res.json({error: "Validation failed, unrecognized grant_type"});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(32).toString("base64");
|
||||
const created_at = Math.floor(Date.now() / 1000);
|
||||
databaseHandler.createToken(token, scope, application.id, 0, created_at);
|
||||
res.status(200);
|
||||
res.json({
|
||||
access_token: token,
|
||||
token_type: "Bearer",
|
||||
scope,
|
||||
created_at,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
13
routes/users/:username/statuses/:statusId.json.js
Normal file
13
routes/users/:username/statuses/:statusId.json.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const databaseHandler = require("../../../../lib/database-handler");
|
||||
|
||||
module.exports = {
|
||||
route: (routeObj) => {
|
||||
routeObj.get((req, res) => {
|
||||
res.header("content-type", "application/activity+json");
|
||||
console.log(`Attempting object https://${req.header.host}/users/${req.params.username}/statuses/${req.params.statusId}`);
|
||||
console.log(databaseHandler.activity[`https://${req.header.host}/users/${req.params.username}/statuses/${req.params.statusId}`]);
|
||||
res.send(databaseHandler.activity[`https://${req.header.host}/users/${req.params.username}/statuses/${req.params.statusId}`].object);
|
||||
res.end();
|
||||
});
|
||||
},
|
||||
};
|
Loading…
Add table
Reference in a new issue