Flags API (#9666)
* feat: new routes for flags API + flag get + flag creation, migration from socket method + flag update, migration from socket method * fixed bug where you could not unassign someone from a flag * feat: tests for new flags API added missing files for schema update * fix: flag tests to use Write API instead of sockets * feat: flag notes API + tests * chore: remove debug line * test: fix breaking test on mongov1.18.x
parent
71bc258731
commit
cc6cbfcdc4
@ -0,0 +1,183 @@
|
||||
FlagObject:
|
||||
description: The resulting object of a call to `Flags.get()`
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
state:
|
||||
type: string
|
||||
flagId:
|
||||
type: number
|
||||
type:
|
||||
type: string
|
||||
targetId:
|
||||
type: number
|
||||
targetUid:
|
||||
type: number
|
||||
datetime:
|
||||
type: number
|
||||
datetimeISO:
|
||||
type: string
|
||||
target_readable:
|
||||
type: string
|
||||
target:
|
||||
type: object
|
||||
properties: {}
|
||||
additionalProperties:
|
||||
description: Properties change depending on the target type (user, post, etc.)
|
||||
assignee:
|
||||
type: number
|
||||
nullable: true
|
||||
reports:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
timestamp:
|
||||
type: number
|
||||
timestampISO:
|
||||
type: string
|
||||
reporter:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: A friendly name for a given user account
|
||||
displayname:
|
||||
type: string
|
||||
description: This is either username or fullname depending on forum and user settings
|
||||
userslug:
|
||||
type: string
|
||||
description: An URL-safe variant of the username (i.e. lower-cased, spaces
|
||||
removed, etc.)
|
||||
picture:
|
||||
nullable: true
|
||||
reputation:
|
||||
type: number
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
icon:text:
|
||||
type: string
|
||||
description: A single-letter representation of a username. This is used in the
|
||||
auto-generated icon given to users without an
|
||||
avatar
|
||||
icon:bgColor:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code assigned to the user. This
|
||||
value is used in conjunction with `icon:text` for
|
||||
the user's auto-generated icon
|
||||
example: "#f44336"
|
||||
- $ref: '#/FlagHistoryObject'
|
||||
- $ref: '#/FlagNotesObject'
|
||||
FlagHistoryObject:
|
||||
type: object
|
||||
properties:
|
||||
history:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
fields:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
meta:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
labelClass:
|
||||
type: string
|
||||
enum: ['default', 'primary', 'success', 'info', 'danger']
|
||||
required:
|
||||
- key
|
||||
datetime:
|
||||
type: number
|
||||
datetimeISO:
|
||||
type: string
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: A friendly name for a given user account
|
||||
displayname:
|
||||
type: string
|
||||
description: This is either username or fullname depending on forum and user settings
|
||||
userslug:
|
||||
type: string
|
||||
description: An URL-safe variant of the username (i.e. lower-cased, spaces
|
||||
removed, etc.)
|
||||
picture:
|
||||
nullable: true
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
icon:text:
|
||||
type: string
|
||||
description: A single-letter representation of a username. This is used in the
|
||||
auto-generated icon given to users without
|
||||
an avatar
|
||||
icon:bgColor:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code assigned to the user. This
|
||||
value is used in conjunction with
|
||||
`icon:text` for the user's auto-generated
|
||||
icon
|
||||
example: "#f44336"
|
||||
required:
|
||||
- uid
|
||||
- datetime
|
||||
- datetimeISO
|
||||
- user
|
||||
FlagNotesObject:
|
||||
type: object
|
||||
properties:
|
||||
notes:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
uid:
|
||||
type: number
|
||||
content:
|
||||
type: string
|
||||
datetime:
|
||||
type: number
|
||||
datetimeISO:
|
||||
type: string
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: A friendly name for a given user account
|
||||
userslug:
|
||||
type: string
|
||||
description: An URL-safe variant of the username (i.e. lower-cased, spaces
|
||||
removed, etc.)
|
||||
picture:
|
||||
type: string
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
icon:text:
|
||||
type: string
|
||||
description: A single-letter representation of a username. This is used in the
|
||||
auto-generated icon given to users without
|
||||
an avatar
|
||||
icon:bgColor:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code assigned to the user. This
|
||||
value is used in conjunction with
|
||||
`icon:text` for the user's auto-generated
|
||||
icon
|
||||
example: "#f44336"
|
@ -0,0 +1,38 @@
|
||||
post:
|
||||
tags:
|
||||
- flags
|
||||
summary: create a flag
|
||||
description: This operation creates a new flag (with a report). If a flag already exists for a given user or post, a report will be appended to the existing flag. The response will change depending on the privilege level of the calling uid. Privileged users (moderators and up) will see the full flag details, whereas regular users will see an empty (but successful) response.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: ['post', 'user']
|
||||
example: 'post'
|
||||
id:
|
||||
type: number
|
||||
example: 2
|
||||
reason:
|
||||
type: string
|
||||
example: 'Spam'
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
- reason
|
||||
responses:
|
||||
'200':
|
||||
description: flag successfully created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
$ref: ../components/schemas/FlagObject.yaml#/FlagObject
|
@ -0,0 +1,67 @@
|
||||
get:
|
||||
tags:
|
||||
- flags
|
||||
summary: get a flag
|
||||
description: This operation retrieve a flag's details. It is only available to privileged users (that is, moderators, global moderators, and administrators).
|
||||
parameters:
|
||||
- in: path
|
||||
name: flagId
|
||||
schema:
|
||||
type: number
|
||||
required: true
|
||||
description: a valid flag id
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: flag successfully retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
$ref: ../../components/schemas/FlagObject.yaml#/FlagObject
|
||||
put:
|
||||
tags:
|
||||
- flags
|
||||
summary: update a flag
|
||||
description: This operation updates a flag's details. It is only available to privileged users (that is, moderators, global moderators, and administrators).
|
||||
parameters:
|
||||
- in: path
|
||||
name: flagId
|
||||
schema:
|
||||
type: number
|
||||
required: true
|
||||
description: a valid flag id
|
||||
example: 1
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
datetime:
|
||||
type: number
|
||||
example: 1625859990035
|
||||
state:
|
||||
type: string
|
||||
enum: ['open', 'wip', 'resolved', 'rejected']
|
||||
example: 'wip'
|
||||
assignee:
|
||||
type: number
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: flag successfully updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
$ref: ../../components/schemas/FlagObject.yaml#/FlagHistoryObject
|
@ -0,0 +1,42 @@
|
||||
post:
|
||||
tags:
|
||||
- flags
|
||||
summary: append a flag note
|
||||
description: This operation append a shared note for a given flag. It is only available to privileged users (that is, moderators, global moderators, and administrators).
|
||||
parameters:
|
||||
- in: path
|
||||
name: flagId
|
||||
schema:
|
||||
type: number
|
||||
required: true
|
||||
description: a valid flag id
|
||||
example: 1
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
note:
|
||||
type: string
|
||||
example: 'test note'
|
||||
datetime:
|
||||
type: number
|
||||
example: 1626446956652
|
||||
required:
|
||||
- note
|
||||
responses:
|
||||
'200':
|
||||
description: flag note successfully added or updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
allOf:
|
||||
- $ref: ../../../components/schemas/FlagObject.yaml#/FlagNotesObject
|
||||
- $ref: ../../../components/schemas/FlagObject.yaml#/FlagHistoryObject
|
@ -0,0 +1,34 @@
|
||||
delete:
|
||||
tags:
|
||||
- flags
|
||||
summary: delete a flag note
|
||||
description: This operation deletes a shared note for a given flag. It is only available to privileged users (that is, moderators, global moderators, and administrators).
|
||||
parameters:
|
||||
- in: path
|
||||
name: flagId
|
||||
schema:
|
||||
type: number
|
||||
required: true
|
||||
description: a valid flag id
|
||||
example: 1
|
||||
- in: path
|
||||
name: datetime
|
||||
schema:
|
||||
type: number
|
||||
required: true
|
||||
description: A valid UNIX timestamp
|
||||
example: 1626446956652
|
||||
responses:
|
||||
'200':
|
||||
description: Flag note deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
allOf:
|
||||
- $ref: ../../../../components/schemas/FlagObject.yaml#/FlagNotesObject
|
||||
- $ref: ../../../../components/schemas/FlagObject.yaml#/FlagHistoryObject
|
@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
const user = require('../user');
|
||||
const flags = require('../flags');
|
||||
|
||||
const flagsApi = module.exports;
|
||||
|
||||
flagsApi.create = async (caller, data) => {
|
||||
const required = ['type', 'id', 'reason'];
|
||||
if (!required.every(prop => !!data[prop])) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const { type, id, reason } = data;
|
||||
|
||||
await flags.validate({
|
||||
uid: caller.uid,
|
||||
type: type,
|
||||
id: id,
|
||||
});
|
||||
|
||||
const flagObj = await flags.create(type, id, caller.uid, reason);
|
||||
flags.notify(flagObj, caller.uid);
|
||||
|
||||
return flagObj;
|
||||
};
|
||||
|
||||
flagsApi.update = async (caller, data) => {
|
||||
const allowed = await user.isPrivileged(caller.uid);
|
||||
if (!allowed) {
|
||||
throw new Error('[[no-privileges]]');
|
||||
}
|
||||
|
||||
const { flagId } = data;
|
||||
delete data.flagId;
|
||||
|
||||
await flags.update(flagId, caller.uid, data);
|
||||
return await flags.getHistory(flagId);
|
||||
};
|
||||
|
||||
flagsApi.appendNote = async (caller, data) => {
|
||||
const allowed = await user.isPrivileged(caller.uid);
|
||||
if (!allowed) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
if (data.datetime && data.flagId) {
|
||||
try {
|
||||
const note = await flags.getNote(data.flagId, data.datetime);
|
||||
if (note.uid !== caller.uid) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
} catch (e) {
|
||||
// Okay if not does not exist in database
|
||||
if (!e.message === '[[error:invalid-data]]') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
await flags.appendNote(data.flagId, caller.uid, data.note, data.datetime);
|
||||
const [notes, history] = await Promise.all([
|
||||
flags.getNotes(data.flagId),
|
||||
flags.getHistory(data.flagId),
|
||||
]);
|
||||
return { notes: notes, history: history };
|
||||
};
|
||||
|
||||
flagsApi.deleteNote = async (caller, data) => {
|
||||
const note = await flags.getNote(data.flagId, data.datetime);
|
||||
if (note.uid !== caller.uid) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
await flags.deleteNote(data.flagId, data.datetime);
|
||||
|
||||
const [notes, history] = await Promise.all([
|
||||
flags.getNotes(data.flagId),
|
||||
flags.getHistory(data.flagId),
|
||||
]);
|
||||
return { notes: notes, history: history };
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
const user = require('../../user');
|
||||
const flags = require('../../flags');
|
||||
const api = require('../../api');
|
||||
const helpers = require('../helpers');
|
||||
|
||||
const Flags = module.exports;
|
||||
|
||||
Flags.create = async (req, res) => {
|
||||
const flagObj = await api.flags.create(req, { ...req.body });
|
||||
helpers.formatApiResponse(200, res, await user.isPrivileged(req.uid) ? flagObj : undefined);
|
||||
};
|
||||
|
||||
Flags.get = async (req, res) => {
|
||||
const isPrivileged = await user.isPrivileged(req.uid);
|
||||
if (!isPrivileged) {
|
||||
return helpers.formatApiResponse(403, res);
|
||||
}
|
||||
|
||||
helpers.formatApiResponse(200, res, await flags.get(req.params.flagId));
|
||||
};
|
||||
|
||||
Flags.update = async (req, res) => {
|
||||
const history = await api.flags.update(req, {
|
||||
flagId: req.params.flagId,
|
||||
...req.body,
|
||||
});
|
||||
|
||||
helpers.formatApiResponse(200, res, { history });
|
||||
};
|
||||
|
||||
Flags.appendNote = async (req, res) => {
|
||||
const payload = await api.flags.appendNote(req, {
|
||||
flagId: req.params.flagId,
|
||||
...req.body,
|
||||
});
|
||||
|
||||
helpers.formatApiResponse(200, res, payload);
|
||||
};
|
||||
|
||||
Flags.deleteNote = async (req, res) => {
|
||||
const payload = await api.flags.deleteNote(req, {
|
||||
...req.params,
|
||||
});
|
||||
|
||||
helpers.formatApiResponse(200, res, payload);
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const middleware = require('../../middleware');
|
||||
const controllers = require('../../controllers');
|
||||
const routeHelpers = require('../helpers');
|
||||
|
||||
const { setupApiRoute } = routeHelpers;
|
||||
|
||||
module.exports = function () {
|
||||
const middlewares = [middleware.ensureLoggedIn];
|
||||
|
||||
setupApiRoute(router, 'post', '/', [...middlewares], controllers.write.flags.create);
|
||||
// setupApiRoute(router, 'delete', ...); // does not exist
|
||||
|
||||
setupApiRoute(router, 'get', '/:flagId', [...middlewares, middleware.assert.flag], controllers.write.flags.get);
|
||||
setupApiRoute(router, 'put', '/:flagId', [...middlewares, middleware.assert.flag], controllers.write.flags.update);
|
||||
|
||||
setupApiRoute(router, 'post', '/:flagId/notes', [...middlewares, middleware.assert.flag], controllers.write.flags.appendNote);
|
||||
setupApiRoute(router, 'delete', '/:flagId/notes/:datetime', [...middlewares, middleware.assert.flag], controllers.write.flags.deleteNote);
|
||||
|
||||
return router;
|
||||
};
|
Loading…
Reference in New Issue