Added API key authentication.
This commit is contained in:
@@ -26,7 +26,7 @@ export function curateActor(actor, context = {}) {
|
||||
cup: actor.cup,
|
||||
waist: actor.waist,
|
||||
hip: actor.hip,
|
||||
naturalBoobs: actor.naturalBoobs,
|
||||
naturalBoobs: actor.natural_boobs,
|
||||
height: actor.height && {
|
||||
metric: actor.height,
|
||||
imperial: unit(actor.height, 'cm').splitUnit(['ft', 'in']).map((value) => Math.round(value.toNumber())),
|
||||
@@ -36,7 +36,7 @@ export function curateActor(actor, context = {}) {
|
||||
imperial: Math.round(unit(actor.weight, 'kg').toNumeric('lbs')),
|
||||
},
|
||||
eyes: actor.eyes,
|
||||
hairColor: actor.hairColor,
|
||||
hairColor: actor.hair_color,
|
||||
hasTattoos: actor.has_tattoos,
|
||||
tattoos: actor.tattoos,
|
||||
hasPiercings: actor.has_piercings,
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function post(path, data, options = {}) {
|
||||
try {
|
||||
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
body: data && JSON.stringify(data),
|
||||
...postHeaders,
|
||||
});
|
||||
|
||||
@@ -95,8 +95,6 @@ export async function post(path, data, options = {}) {
|
||||
return body;
|
||||
}
|
||||
|
||||
console.log(body.statusMessage);
|
||||
|
||||
showFeedback(false, options, body.statusMessage);
|
||||
throw new Error(body.statusMessage);
|
||||
} catch (error) {
|
||||
@@ -109,7 +107,7 @@ export async function patch(path, data, options = {}) {
|
||||
try {
|
||||
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
body: data && JSON.stringify(data),
|
||||
...postHeaders,
|
||||
});
|
||||
|
||||
@@ -136,7 +134,7 @@ export async function del(path, options = {}) {
|
||||
try {
|
||||
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(options.data),
|
||||
body: options.data && JSON.stringify(options.data),
|
||||
...postHeaders,
|
||||
});
|
||||
|
||||
|
||||
103
src/auth.js
103
src/auth.js
@@ -4,13 +4,17 @@ import crypto from 'crypto';
|
||||
import fs from 'fs/promises';
|
||||
import { createAvatar } from '@dicebear/core';
|
||||
import { shapes } from '@dicebear/collection';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { knexOwner as knex } from './knex.js';
|
||||
import redis from './redis.js';
|
||||
import { curateUser, fetchUser } from './users.js';
|
||||
import { HttpError } from './errors.js';
|
||||
import initLogger from './logger.js';
|
||||
import slugify from '../utils/slugify.js';
|
||||
import initLogger, { initAccessLogger } from './logger.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const accessLogger = initAccessLogger();
|
||||
const scrypt = util.promisify(crypto.scrypt);
|
||||
|
||||
async function verifyPassword(password, storedPassword) {
|
||||
@@ -138,3 +142,100 @@ export async function signup(credentials, userIp) {
|
||||
|
||||
return fetchUser(userId);
|
||||
}
|
||||
|
||||
function curateKey(key) {
|
||||
return {
|
||||
id: key.id,
|
||||
identifier: key.identifier,
|
||||
lastUsedAt: key.last_used_at,
|
||||
lastUsedIp: key.last_used_ip,
|
||||
createdAt: key.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchUserKeys(reqUser) {
|
||||
const keys = await knex('users_keys')
|
||||
.where('user_id', reqUser.id)
|
||||
.orderBy('created_at', 'asc');
|
||||
|
||||
return keys.map((key) => curateKey(key));
|
||||
}
|
||||
|
||||
export async function verifyKey(userId, key, req) {
|
||||
if (!key || !userId) {
|
||||
throw new HttpError('The API credentials are not provided.', 401);
|
||||
}
|
||||
|
||||
const hashedKey = (await scrypt(key, '', 64)).toString('hex'); // salt redundant for randomly generated key
|
||||
|
||||
const storedKey = await knex('users_keys')
|
||||
.where('user_id', userId)
|
||||
.where('key', hashedKey)
|
||||
.first();
|
||||
|
||||
if (!storedKey) {
|
||||
throw new HttpError('The API credentials are invalid.', 401);
|
||||
}
|
||||
|
||||
accessLogger.access({
|
||||
userId,
|
||||
identifier: storedKey.identifier,
|
||||
ip: req.userIp,
|
||||
});
|
||||
|
||||
knex('users_keys')
|
||||
.where('id', storedKey.id)
|
||||
.update('last_used_at', knex.raw('now()'))
|
||||
.update('last_used_ip', req.userIp)
|
||||
.then(() => {
|
||||
// no need to wait for this
|
||||
});
|
||||
}
|
||||
|
||||
export async function createKey(reqUser) {
|
||||
const cooldownKey = `traxxx:key_create_cooldown:${reqUser.id}`;
|
||||
|
||||
if (reqUser.role !== 'admin' && await redis.exists(cooldownKey)) {
|
||||
throw new HttpError(`You can only create a new API key once every ${config.apiAccess.keyCooldown} minutes.`, 429);
|
||||
}
|
||||
|
||||
const keys = await fetchUserKeys(reqUser);
|
||||
|
||||
if (keys.length >= config.apiAccess.keyLimit) {
|
||||
throw new HttpError(`You can only hold ${config.apiAccess.keyLimit} API keys at one time. Please remove a key.`, 400);
|
||||
}
|
||||
|
||||
const key = crypto.randomBytes(config.apiAccess.keySize).toString('base64url');
|
||||
const hashedKey = (await scrypt(key, '', 64)).toString('hex'); // salt redundant for randomly generated key
|
||||
|
||||
const identifier = slugify([faker.word.adjective(), faker.animal[faker.animal.type()]()]);
|
||||
|
||||
const [newKey] = await knex('users_keys')
|
||||
.insert({
|
||||
user_id: reqUser.id,
|
||||
key: hashedKey,
|
||||
identifier,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
await redis.set(cooldownKey, identifier);
|
||||
await redis.expire(cooldownKey, config.apiAccess.keyCooldown * 60);
|
||||
|
||||
return {
|
||||
...curateKey(newKey),
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
export async function removeUserKey(reqUser, identifier) {
|
||||
await knex('users_keys')
|
||||
.where('user_id', reqUser.id)
|
||||
.where('identifier', identifier)
|
||||
.delete();
|
||||
}
|
||||
|
||||
export async function flushUserKeys(reqUser) {
|
||||
await knex('users_keys')
|
||||
.where('user_id', reqUser.id)
|
||||
.delete();
|
||||
}
|
||||
|
||||
@@ -34,3 +34,20 @@ export default function initLogger(customLabel) {
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function initAccessLogger() {
|
||||
return winston.createLogger({
|
||||
level: 'access',
|
||||
levels: {
|
||||
access: 0,
|
||||
},
|
||||
format: winston.format.printf((data) => JSON.stringify({ ...data.message, timestamp: new Date() })),
|
||||
transports: [
|
||||
new winston.transports.DailyRotateFile({
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
filename: path.join('log', 'access_%DATE%.log'),
|
||||
level: 'access',
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export function curateUser(user, _assets = {}) {
|
||||
emailVerified: user.email_verified,
|
||||
identityVerified: user.identity_verified,
|
||||
avatar: `/media/avatars/${user.id}_${user.username}.png`,
|
||||
role: user.role,
|
||||
createdAt: user.created_at,
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export const actorsSchema = `
|
||||
query: String
|
||||
limit: Int! = 30
|
||||
page: Int! = 1
|
||||
order: [String]
|
||||
order: [String!]
|
||||
): ActorsResult
|
||||
|
||||
actor(
|
||||
@@ -54,7 +54,7 @@ export const actorsSchema = `
|
||||
): Actor
|
||||
|
||||
actorsById(
|
||||
ids: [Int]!
|
||||
ids: [Int!]!
|
||||
): [Actor]
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export const actorsSchema = `
|
||||
}
|
||||
|
||||
type ActorsResult {
|
||||
nodes: [Actor]
|
||||
nodes: [Actor!]!
|
||||
total: Int
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ export const actorsSchema = `
|
||||
gender: String
|
||||
dateOfBirth: Date
|
||||
age: Int
|
||||
ageFromBirth: Int
|
||||
ageThen: Int
|
||||
origin: Location
|
||||
residence: Location
|
||||
height: Int
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
|
||||
import IPCIDR from 'ip-cidr';
|
||||
|
||||
import { login, signup } from '../auth.js';
|
||||
import {
|
||||
login,
|
||||
signup,
|
||||
fetchUserKeys,
|
||||
createKey,
|
||||
removeUserKey,
|
||||
flushUserKeys,
|
||||
} from '../auth.js';
|
||||
|
||||
import { fetchUser } from '../users.js';
|
||||
|
||||
function getIp(req) {
|
||||
@@ -68,4 +77,28 @@ export async function signupApi(req, res) {
|
||||
req.session.user = user;
|
||||
res.send(user);
|
||||
}
|
||||
|
||||
export async function fetchUserKeysApi(req, res) {
|
||||
const keys = await fetchUserKeys(req.user);
|
||||
|
||||
res.send(stringify(keys));
|
||||
}
|
||||
|
||||
export async function createKeyApi(req, res) {
|
||||
const key = await createKey(req.user);
|
||||
|
||||
res.send(stringify(key));
|
||||
}
|
||||
|
||||
export async function removeUserKeyApi(req, res) {
|
||||
await removeUserKey(req.user, req.params.keyIdentifier);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
export async function flushUserKeysApi(req, res) {
|
||||
await flushUserKeys(req.user);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
@@ -37,7 +37,7 @@ export const entitiesSchema = `
|
||||
}
|
||||
|
||||
type EntitiesResult {
|
||||
nodes: [Entity]
|
||||
nodes: [Entity!]!
|
||||
}
|
||||
|
||||
type Entity {
|
||||
@@ -47,7 +47,7 @@ export const entitiesSchema = `
|
||||
url: String
|
||||
type: String
|
||||
parent: Entity
|
||||
children: [Entity]
|
||||
children: [Entity!]!
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import config from 'config';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import {
|
||||
@@ -24,6 +25,8 @@ import {
|
||||
fetchActorsByIdGraphql,
|
||||
} from './actors.js';
|
||||
|
||||
import { verifyKey } from '../auth.js';
|
||||
|
||||
const schema = buildSchema(`
|
||||
type Query {
|
||||
movies(
|
||||
@@ -61,6 +64,13 @@ const DateScalar = new GraphQLScalarType({
|
||||
});
|
||||
|
||||
export async function graphqlApi(req, res) {
|
||||
if (!config.apiAccess.graphqlEnabled) {
|
||||
res.status(404).send();
|
||||
return;
|
||||
}
|
||||
|
||||
await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
|
||||
|
||||
const data = await graphql({
|
||||
schema,
|
||||
source: req.body.query,
|
||||
|
||||
@@ -75,9 +75,9 @@ export const scenesSchema = `
|
||||
scenes(
|
||||
query: String
|
||||
scope: String
|
||||
entities: [String]
|
||||
actorIds: [String]
|
||||
tags: [String]
|
||||
entities: [String!]
|
||||
actorIds: [String!]
|
||||
tags: [String!]
|
||||
limit: Int! = 30
|
||||
page: Int! = 1
|
||||
): ReleasesResult
|
||||
@@ -87,16 +87,16 @@ export const scenesSchema = `
|
||||
): Release
|
||||
|
||||
scenesById(
|
||||
ids: [Int]!
|
||||
ids: [Int!]!
|
||||
): [Release]
|
||||
}
|
||||
|
||||
type ReleasesAggregate {
|
||||
actors: [Actor]
|
||||
actors: [Actor!]
|
||||
}
|
||||
|
||||
type ReleasesResult {
|
||||
nodes: [Release]
|
||||
nodes: [Release!]!
|
||||
total: Int
|
||||
aggregates: ReleasesAggregate
|
||||
}
|
||||
@@ -112,13 +112,13 @@ export const scenesSchema = `
|
||||
shootId: Int
|
||||
channel: Entity
|
||||
network: Entity
|
||||
actors: [Actor]
|
||||
tags: [Tag]
|
||||
actors: [Actor!]!
|
||||
tags: [Tag!]!
|
||||
poster: Media
|
||||
trailer: Media
|
||||
photos: [Media]
|
||||
covers: [Media]
|
||||
movies: [Release]
|
||||
photos: [Media!]!
|
||||
covers: [Media!]!
|
||||
movies: [Release!]!
|
||||
}
|
||||
|
||||
type Tag {
|
||||
|
||||
@@ -28,6 +28,10 @@ import {
|
||||
loginApi,
|
||||
logoutApi,
|
||||
signupApi,
|
||||
fetchUserKeysApi,
|
||||
createKeyApi,
|
||||
removeUserKeyApi,
|
||||
flushUserKeysApi,
|
||||
} from './auth.js';
|
||||
|
||||
import {
|
||||
@@ -162,6 +166,12 @@ export default async function initServer() {
|
||||
router.post('/api/templates', createTemplateApi);
|
||||
router.delete('/api/templates/:templateId', removeTemplateApi);
|
||||
|
||||
// API KEYS
|
||||
router.get('/api/me/keys', fetchUserKeysApi);
|
||||
router.post('/api/keys', createKeyApi);
|
||||
router.delete('/api/me/keys/:keyIdentifier', removeUserKeyApi);
|
||||
router.delete('/api/me/keys', flushUserKeysApi);
|
||||
|
||||
// ALERTS
|
||||
router.get('/api/alerts', fetchAlertsApi);
|
||||
router.post('/api/alerts', createAlertApi);
|
||||
@@ -182,7 +192,10 @@ export default async function initServer() {
|
||||
// TAGS
|
||||
router.get('/api/tags', fetchTagsApi);
|
||||
|
||||
router.post('/graphql', graphqlApi);
|
||||
if (config.apiAccess.graphqlEnabled) {
|
||||
router.post('/graphql', graphqlApi);
|
||||
}
|
||||
|
||||
router.use(consentHandler);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
|
||||
Reference in New Issue
Block a user