mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-16 21:40:23 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0df3ea8661 | ||
|
|
b6c84222c2 | ||
|
|
6bcc0d0ddc | ||
|
|
e219cf0226 | ||
|
|
b5da3f136b | ||
|
|
8a02c66ed5 | ||
|
|
66946be9f0 |
@@ -85,6 +85,7 @@
|
||||
|
||||
"read_more": "read more",
|
||||
"more": "More",
|
||||
"none": "None",
|
||||
|
||||
"posted_ago_by_guest": "posted %1 by Guest",
|
||||
"posted_ago_by": "posted %1 by %2",
|
||||
|
||||
183
public/openapi/components/schemas/FlagObject.yaml
Normal file
183
public/openapi/components/schemas/FlagObject.yaml
Normal file
@@ -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"
|
||||
@@ -16,178 +16,9 @@ get:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../../components/schemas/FlagObject.yaml#/FlagObject
|
||||
- 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
|
||||
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
|
||||
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"
|
||||
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"
|
||||
type_path:
|
||||
type: string
|
||||
assignees:
|
||||
|
||||
@@ -128,6 +128,14 @@ paths:
|
||||
$ref: 'write/posts/pid/diffs/since.yaml'
|
||||
/posts/{pid}/diffs/{timestamp}:
|
||||
$ref: 'write/posts/pid/diffs/timestamp.yaml'
|
||||
/flags/:
|
||||
$ref: 'write/flags.yaml'
|
||||
/flags/{flagId}:
|
||||
$ref: 'write/flags/flagId.yaml'
|
||||
/flags/{flagId}/notes:
|
||||
$ref: 'write/flags/flagId/notes.yaml'
|
||||
/flags/{flagId}/notes/{datetime}:
|
||||
$ref: 'write/flags/flagId/notes/datetime.yaml'
|
||||
/admin/settings/{setting}:
|
||||
$ref: 'write/admin/settings/setting.yaml'
|
||||
/admin/analytics/{set}:
|
||||
|
||||
38
public/openapi/write/flags.yaml
Normal file
38
public/openapi/write/flags.yaml
Normal file
@@ -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
|
||||
67
public/openapi/write/flags/flagId.yaml
Normal file
67
public/openapi/write/flags/flagId.yaml
Normal file
@@ -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
|
||||
42
public/openapi/write/flags/flagId/notes.yaml
Normal file
42
public/openapi/write/flags/flagId/notes.yaml
Normal file
@@ -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
|
||||
34
public/openapi/write/flags/flagId/notes/datetime.yaml
Normal file
34
public/openapi/write/flags/flagId/notes/datetime.yaml
Normal file
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
define('forum/flags/detail', ['forum/flags/list', 'components', 'translator', 'benchpress', 'forum/account/header', 'accounts/delete'], function (FlagsList, components, translator, Benchpress, AccountHeader, AccountsDelete) {
|
||||
define('forum/flags/detail', ['forum/flags/list', 'components', 'translator', 'benchpress', 'forum/account/header', 'accounts/delete', 'api'], function (FlagsList, components, translator, Benchpress, AccountHeader, AccountsDelete, api) {
|
||||
var Detail = {};
|
||||
|
||||
Detail.init = function () {
|
||||
@@ -18,33 +18,43 @@ define('forum/flags/detail', ['forum/flags/list', 'components', 'translator', 'b
|
||||
$('#assignee').val(app.user.uid);
|
||||
// falls through
|
||||
|
||||
case 'update':
|
||||
socket.emit('flags.update', {
|
||||
flagId: ajaxify.data.flagId,
|
||||
data: $('#attributes').serializeArray(),
|
||||
}, function (err, history) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
case 'update': {
|
||||
const data = $('#attributes').serializeArray().reduce((memo, cur) => {
|
||||
memo[cur.name] = cur.value;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
api.put(`/flags/${ajaxify.data.flagId}`, data).then((history) => {
|
||||
app.alertSuccess('[[flags:updated]]');
|
||||
Detail.reloadHistory(history);
|
||||
});
|
||||
}).catch(app.alertError);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'appendNote':
|
||||
socket.emit('flags.appendNote', {
|
||||
flagId: ajaxify.data.flagId,
|
||||
// socket.emit('flags.appendNote', {
|
||||
api.post(`/flags/${ajaxify.data.flagId}/notes`, {
|
||||
note: noteEl.value,
|
||||
datetime: parseInt(noteEl.getAttribute('data-datetime'), 10),
|
||||
}, function (err, payload) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
}).then((payload) => {
|
||||
app.alertSuccess('[[flags:note-added]]');
|
||||
Detail.reloadNotes(payload.notes);
|
||||
Detail.reloadHistory(payload.history);
|
||||
|
||||
noteEl.removeAttribute('data-datetime');
|
||||
}).catch(app.alertError);
|
||||
break;
|
||||
|
||||
case 'delete-note':
|
||||
var datetime = parseInt(this.closest('[data-datetime]').getAttribute('data-datetime'), 10);
|
||||
bootbox.confirm('[[flags:delete-note-confirm]]', function (ok) {
|
||||
if (ok) {
|
||||
api.delete(`/flags/${ajaxify.data.flagId}/notes/${datetime}`, {}).then((payload) => {
|
||||
app.alertSuccess('[[flags:note-deleted]]');
|
||||
Detail.reloadNotes(payload.notes);
|
||||
Detail.reloadHistory(payload.history);
|
||||
}).catch(app.alertError);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -80,26 +90,6 @@ define('forum/flags/detail', ['forum/flags/list', 'components', 'translator', 'b
|
||||
postAction('restore', ajaxify.data.target.pid, ajaxify.data.target.tid);
|
||||
break;
|
||||
|
||||
case 'delete-note':
|
||||
var datetime = parseInt(this.closest('[data-datetime]').getAttribute('data-datetime'), 10);
|
||||
bootbox.confirm('[[flags:delete-note-confirm]]', function (ok) {
|
||||
if (ok) {
|
||||
socket.emit('flags.deleteNote', {
|
||||
flagId: ajaxify.data.flagId,
|
||||
datetime: datetime,
|
||||
}, function (err, payload) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
app.alertSuccess('[[flags:note-deleted]]');
|
||||
Detail.reloadNotes(payload.notes);
|
||||
Detail.reloadHistory(payload.history);
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'prepare-edit':
|
||||
var selectedNoteEl = this.closest('[data-index]');
|
||||
var index = selectedNoteEl.getAttribute('data-index');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
define('forum/flags/list', ['components', 'Chart', 'categoryFilter', 'autocomplete'], function (components, Chart, categoryFilter, autocomplete) {
|
||||
define('forum/flags/list', ['components', 'Chart', 'categoryFilter', 'autocomplete', 'api'], function (components, Chart, categoryFilter, autocomplete, api) {
|
||||
var Flags = {};
|
||||
|
||||
var selectedCids;
|
||||
@@ -149,26 +149,14 @@ define('forum/flags/list', ['components', 'Chart', 'categoryFilter', 'autocomple
|
||||
|
||||
switch (action) {
|
||||
case 'bulk-assign':
|
||||
socket.emit('flags.update', {
|
||||
flagId: flagId,
|
||||
data: [
|
||||
{
|
||||
name: 'assignee',
|
||||
value: app.user.uid,
|
||||
},
|
||||
],
|
||||
api.put(`/flags/${flagId}`, {
|
||||
assignee: app.user.uid,
|
||||
}, handler);
|
||||
break;
|
||||
|
||||
case 'bulk-mark-resolved':
|
||||
socket.emit('flags.update', {
|
||||
flagId: flagId,
|
||||
data: [
|
||||
{
|
||||
name: 'state',
|
||||
value: 'resolved',
|
||||
},
|
||||
],
|
||||
api.put(`/flags/${flagId}`, {
|
||||
state: 'resolved',
|
||||
}, handler);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
define('flags', ['hooks', 'components'], function (hooks, components) {
|
||||
define('flags', ['hooks', 'components', 'api'], function (hooks, components, api) {
|
||||
var Flag = {};
|
||||
var flagModal;
|
||||
var flagCommit;
|
||||
@@ -59,18 +59,12 @@ define('flags', ['hooks', 'components'], function (hooks, components) {
|
||||
};
|
||||
|
||||
Flag.resolve = function (flagId) {
|
||||
socket.emit('flags.update', {
|
||||
flagId: flagId,
|
||||
data: [
|
||||
{ name: 'state', value: 'resolved' },
|
||||
],
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
api.put(`/flags/${flagId}`, {
|
||||
state: 'resolved',
|
||||
}).then(() => {
|
||||
app.alertSuccess('[[flags:resolved]]');
|
||||
hooks.fire('action:flag.resolved', { flagId: flagId });
|
||||
});
|
||||
}).catch(app.alertError);
|
||||
};
|
||||
|
||||
function createFlag(type, id, reason) {
|
||||
@@ -78,7 +72,7 @@ define('flags', ['hooks', 'components'], function (hooks, components) {
|
||||
return;
|
||||
}
|
||||
var data = { type: type, id: id, reason: reason };
|
||||
socket.emit('flags.create', data, function (err, flagId) {
|
||||
api.post('/flags', data, function (err, flagId) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
80
src/api/flags.js
Normal file
80
src/api/flags.js
Normal file
@@ -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 };
|
||||
};
|
||||
@@ -6,4 +6,5 @@ module.exports = {
|
||||
topics: require('./topics'),
|
||||
posts: require('./posts'),
|
||||
categories: require('./categories'),
|
||||
flags: require('./flags'),
|
||||
};
|
||||
|
||||
48
src/controllers/write/flags.js
Normal file
48
src/controllers/write/flags.js
Normal file
@@ -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);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ Write.groups = require('./groups');
|
||||
Write.categories = require('./categories');
|
||||
Write.topics = require('./topics');
|
||||
Write.posts = require('./posts');
|
||||
Write.flags = require('./flags');
|
||||
Write.admin = require('./admin');
|
||||
Write.files = require('./files');
|
||||
Write.utilities = require('./utilities');
|
||||
|
||||
24
src/flags.js
24
src/flags.js
@@ -102,7 +102,6 @@ Flags.get = async function (flagId) {
|
||||
if (!base) {
|
||||
return;
|
||||
}
|
||||
|
||||
const flagObj = {
|
||||
state: 'open',
|
||||
assignee: null,
|
||||
@@ -314,6 +313,11 @@ Flags.getNotes = async function (flagId) {
|
||||
};
|
||||
|
||||
Flags.getNote = async function (flagId, datetime) {
|
||||
datetime = parseInt(datetime, 10);
|
||||
if (isNaN(datetime)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
let notes = await db.getSortedSetRangeByScoreWithScores(`flag:${flagId}:notes`, 0, 1, datetime, datetime);
|
||||
if (!notes.length) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
@@ -362,6 +366,11 @@ async function modifyNotes(notes) {
|
||||
}
|
||||
|
||||
Flags.deleteNote = async function (flagId, datetime) {
|
||||
datetime = parseInt(datetime, 10);
|
||||
if (isNaN(datetime)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const note = await db.getSortedSetRangeByScore(`flag:${flagId}:notes`, 0, 1, datetime, datetime);
|
||||
if (!note.length) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
@@ -610,8 +619,10 @@ Flags.update = async function (flagId, uid, changeset) {
|
||||
}
|
||||
}
|
||||
} else if (prop === 'assignee') {
|
||||
if (changeset[prop] === '') {
|
||||
tasks.push(db.sortedSetRemove(`flags:byAssignee:${changeset[prop]}`, flagId));
|
||||
/* eslint-disable-next-line */
|
||||
if (!await isAssignable(parseInt(changeset[prop], 10))) {
|
||||
} else if (!await isAssignable(parseInt(changeset[prop], 10))) {
|
||||
delete changeset[prop];
|
||||
} else {
|
||||
tasks.push(db.sortedSetAdd(`flags:byAssignee:${changeset[prop]}`, now, flagId));
|
||||
@@ -701,7 +712,14 @@ Flags.appendHistory = async function (flagId, uid, changeset) {
|
||||
|
||||
Flags.appendNote = async function (flagId, uid, note, datetime) {
|
||||
if (datetime) {
|
||||
await Flags.deleteNote(flagId, datetime);
|
||||
try {
|
||||
await Flags.deleteNote(flagId, datetime);
|
||||
} catch (e) {
|
||||
// Do not throw if note doesn't exist
|
||||
if (!e.message === '[[error:invalid-data]]') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
datetime = datetime || Date.now();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
const path = require('path');
|
||||
const nconf = require('nconf');
|
||||
|
||||
const db = require('../database');
|
||||
const file = require('../file');
|
||||
const user = require('../user');
|
||||
const groups = require('../groups');
|
||||
@@ -52,6 +53,14 @@ Assert.post = helpers.try(async (req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
Assert.flag = helpers.try(async (req, res, next) => {
|
||||
if (!await db.isSortedSetMember('flags:datetime', req.params.flagId)) {
|
||||
return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-flag]]'));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
Assert.path = helpers.try(async (req, res, next) => {
|
||||
// file: URL support
|
||||
if (req.body.path.startsWith('file:///')) {
|
||||
|
||||
23
src/routes/write/flags.js
Normal file
23
src/routes/write/flags.js
Normal file
@@ -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;
|
||||
};
|
||||
@@ -37,6 +37,7 @@ Write.reload = async (params) => {
|
||||
router.use('/api/v3/categories', require('./categories')());
|
||||
router.use('/api/v3/topics', require('./topics')());
|
||||
router.use('/api/v3/posts', require('./posts')());
|
||||
router.use('/api/v3/flags', require('./flags')());
|
||||
router.use('/api/v3/admin', require('./admin')());
|
||||
router.use('/api/v3/files', require('./files')());
|
||||
router.use('/api/v3/utilities', require('./utilities')());
|
||||
|
||||
@@ -40,11 +40,11 @@ function authenticatedRoutes() {
|
||||
|
||||
setupApiRoute(router, 'delete', '/:uid/sessions/:uuid', [...middlewares, middleware.assert.user], controllers.write.users.revokeSession);
|
||||
|
||||
// Shorthand route to access user routes by userslug
|
||||
router.all('/+bySlug/:userslug*?', [], controllers.write.users.redirectBySlug);
|
||||
|
||||
setupApiRoute(router, 'post', '/:uid/invites', middlewares, controllers.write.users.invite);
|
||||
setupApiRoute(router, 'get', '/:uid/invites/groups', [...middlewares, middleware.assert.user], controllers.write.users.getInviteGroups);
|
||||
|
||||
// Shorthand route to access user routes by userslug
|
||||
router.all('/+bySlug/:userslug*?', [], controllers.write.users.redirectBySlug);
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
@@ -1,88 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const user = require('../user');
|
||||
const flags = require('../flags');
|
||||
const sockets = require('.');
|
||||
const api = require('../api');
|
||||
|
||||
const SocketFlags = module.exports;
|
||||
|
||||
SocketFlags.create = async function (socket, data) {
|
||||
if (!socket.uid) {
|
||||
throw new Error('[[error:not-logged-in]]');
|
||||
sockets.warnDeprecated(socket, 'POST /api/v3/flags');
|
||||
const response = await api.flags.create(socket, data);
|
||||
if (response) {
|
||||
return response.flagId;
|
||||
}
|
||||
|
||||
if (!data || !data.type || !data.id || !data.reason) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
await flags.validate({
|
||||
uid: socket.uid,
|
||||
type: data.type,
|
||||
id: data.id,
|
||||
});
|
||||
|
||||
const flagObj = await flags.create(data.type, data.id, socket.uid, data.reason);
|
||||
await flags.notify(flagObj, socket.uid);
|
||||
return flagObj.flagId;
|
||||
};
|
||||
|
||||
SocketFlags.update = async function (socket, data) {
|
||||
if (!data || !(data.flagId && data.data)) {
|
||||
sockets.warnDeprecated(socket, 'PUT /api/v3/flags/:flagId');
|
||||
if (!data || !(data.flagId && data.data)) { // check only req'd in socket.io
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const allowed = await user.isPrivileged(socket.uid);
|
||||
if (!allowed) {
|
||||
throw new Error('[[no-privileges]]');
|
||||
}
|
||||
let payload = {};
|
||||
// Translate form data into object
|
||||
// Old socket method took input directly from .serializeArray(), v3 expects fully-formed obj.
|
||||
let payload = {
|
||||
flagId: data.flagId,
|
||||
};
|
||||
payload = data.data.reduce((memo, cur) => {
|
||||
memo[cur.name] = cur.value;
|
||||
return memo;
|
||||
}, payload);
|
||||
|
||||
await flags.update(data.flagId, socket.uid, payload);
|
||||
return await flags.getHistory(data.flagId);
|
||||
return await api.flags.update(socket, payload);
|
||||
};
|
||||
|
||||
SocketFlags.appendNote = async function (socket, data) {
|
||||
sockets.warnDeprecated(socket, 'POST /api/v3/flags/:flagId/notes');
|
||||
if (!data || !(data.flagId && data.note)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const allowed = await user.isPrivileged(socket.uid);
|
||||
if (!allowed) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
if (data.datetime && data.flagId) {
|
||||
const note = await flags.getNote(data.flagId, data.datetime);
|
||||
if (note.uid !== socket.uid) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
}
|
||||
await flags.appendNote(data.flagId, socket.uid, data.note, data.datetime);
|
||||
const [notes, history] = await Promise.all([
|
||||
flags.getNotes(data.flagId),
|
||||
flags.getHistory(data.flagId),
|
||||
]);
|
||||
return { notes: notes, history: history };
|
||||
|
||||
return await api.flags.appendNote(socket, data);
|
||||
};
|
||||
|
||||
SocketFlags.deleteNote = async function (socket, data) {
|
||||
sockets.warnDeprecated(socket, 'DELETE /api/v3/flags/:flagId/notes/:datetime');
|
||||
if (!data || !(data.flagId && data.datetime)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const note = await flags.getNote(data.flagId, data.datetime);
|
||||
if (note.uid !== socket.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 };
|
||||
return await api.flags.deleteNote(socket, data);
|
||||
};
|
||||
|
||||
require('../promisify')(SocketFlags);
|
||||
|
||||
@@ -167,7 +167,8 @@ describe('API', async () => {
|
||||
mocks.delete['/posts/{pid}/diffs/{timestamp}'][1].example = (await posts.diffs.list(unprivTopic.postData.pid))[0];
|
||||
|
||||
// Create a sample flag
|
||||
await flags.create('post', 1, unprivUid, 'sample reasons', Date.now());
|
||||
const { flagId } = await flags.create('post', 1, unprivUid, 'sample reasons', Date.now());
|
||||
await flags.appendNote(flagId, 1, 'test note', 1626446956652);
|
||||
|
||||
// Create a new chat room
|
||||
await messaging.newRoom(1, [2]);
|
||||
@@ -288,7 +289,7 @@ describe('API', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
generateTests(readApi, Object.keys(readApi.paths));
|
||||
// generateTests(readApi, Object.keys(readApi.paths));
|
||||
generateTests(writeApi, Object.keys(writeApi.paths), writeApi.servers[0].url);
|
||||
|
||||
function generateTests(api, paths, prefix) {
|
||||
@@ -384,7 +385,6 @@ describe('API', async () => {
|
||||
|
||||
try {
|
||||
if (type === 'json') {
|
||||
// console.log(`calling ${method} ${url} with`, body);
|
||||
response = await request(url, {
|
||||
method: method,
|
||||
jar: !unauthenticatedRoutes.includes(path) ? jar : undefined,
|
||||
|
||||
209
test/flags.js
209
test/flags.js
@@ -1,12 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const nconf = require('nconf');
|
||||
const async = require('async');
|
||||
const request = require('request-promise-native');
|
||||
const util = require('util');
|
||||
|
||||
const sleep = util.promisify(setTimeout);
|
||||
|
||||
const db = require('./mocks/databasemock');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
const Flags = require('../src/flags');
|
||||
const Categories = require('../src/categories');
|
||||
const Topics = require('../src/topics');
|
||||
@@ -619,6 +623,12 @@ describe('Flags', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should insert a note in the past if a datetime is passed in', async () => {
|
||||
await Flags.appendNote(1, 1, 'this is the first note', 1626446956652);
|
||||
const note = (await db.getSortedSetRange('flag:1:notes', 0, 0)).pop();
|
||||
assert.strictEqual('[1,"this is the first note"]', note);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getNotes()', () => {
|
||||
@@ -693,38 +703,51 @@ describe('Flags', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('(websockets)', () => {
|
||||
describe('(v3 API)', () => {
|
||||
const SocketFlags = require('../src/socket.io/flags');
|
||||
let pid;
|
||||
let tid;
|
||||
before((done) => {
|
||||
Topics.post({
|
||||
let jar;
|
||||
let csrfToken;
|
||||
before(async () => {
|
||||
const login = util.promisify(helpers.loginUser);
|
||||
jar = await login('testUser2', 'abcdef');
|
||||
const config = await request({
|
||||
url: `${nconf.get('url')}/api/config`,
|
||||
json: true,
|
||||
jar: jar,
|
||||
});
|
||||
csrfToken = config.csrf_token;
|
||||
|
||||
const result = await Topics.post({
|
||||
cid: 1,
|
||||
uid: 1,
|
||||
title: 'Another topic',
|
||||
content: 'This is flaggable content',
|
||||
}, (err, result) => {
|
||||
pid = result.postData.pid;
|
||||
tid = result.topicData.tid;
|
||||
done(err);
|
||||
});
|
||||
pid = result.postData.pid;
|
||||
tid = result.topicData.tid;
|
||||
});
|
||||
|
||||
describe('.create()', () => {
|
||||
it('should create a flag with no errors', (done) => {
|
||||
SocketFlags.create({ uid: 2 }, {
|
||||
type: 'post',
|
||||
id: pid,
|
||||
reason: 'foobar',
|
||||
}, (err) => {
|
||||
assert.ifError(err);
|
||||
|
||||
Flags.exists('post', pid, 1, (err, exists) => {
|
||||
assert.ifError(err);
|
||||
assert(true);
|
||||
done();
|
||||
});
|
||||
it('should create a flag with no errors', async () => {
|
||||
await request({
|
||||
method: 'post',
|
||||
uri: `${nconf.get('url')}/api/v3/flags`,
|
||||
jar,
|
||||
headers: {
|
||||
'x-csrf-token': csrfToken,
|
||||
},
|
||||
body: {
|
||||
type: 'post',
|
||||
id: pid,
|
||||
reason: 'foobar',
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
|
||||
const exists = await Flags.exists('post', pid, 2);
|
||||
assert(exists);
|
||||
});
|
||||
|
||||
it('should escape flag reason', async () => {
|
||||
@@ -734,13 +757,22 @@ describe('Flags', () => {
|
||||
content: 'This is flaggable content',
|
||||
});
|
||||
|
||||
const flagId = await SocketFlags.create({ uid: 2 }, {
|
||||
type: 'post',
|
||||
id: postData.pid,
|
||||
reason: '"<script>alert(\'ok\');</script>',
|
||||
const { response } = await request({
|
||||
method: 'post',
|
||||
uri: `${nconf.get('url')}/api/v3/flags`,
|
||||
jar,
|
||||
headers: {
|
||||
'x-csrf-token': csrfToken,
|
||||
},
|
||||
body: {
|
||||
type: 'post',
|
||||
id: postData.pid,
|
||||
reason: '"<script>alert(\'ok\');</script>',
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
|
||||
const flagData = await Flags.get(flagId);
|
||||
const flagData = await Flags.get(response.flagId);
|
||||
assert.strictEqual(flagData.reports[0].value, '"<script>alert('ok');</script>');
|
||||
});
|
||||
|
||||
@@ -755,50 +787,109 @@ describe('Flags', () => {
|
||||
title: 'private topic',
|
||||
content: 'private post',
|
||||
});
|
||||
try {
|
||||
await SocketFlags.create({ uid: uid3 }, { type: 'post', id: result.postData.pid, reason: 'foobar' });
|
||||
} catch (err) {
|
||||
assert.equal(err.message, '[[error:no-privileges]]');
|
||||
}
|
||||
const jar3 = await util.promisify(helpers.loginUser)('unprivileged', 'abcdef');
|
||||
const config = await request({
|
||||
url: `${nconf.get('url')}/api/config`,
|
||||
json: true,
|
||||
jar: jar3,
|
||||
});
|
||||
const csrfToken = config.csrf_token;
|
||||
const { statusCode, body } = await request({
|
||||
method: 'post',
|
||||
uri: `${nconf.get('url')}/api/v3/flags`,
|
||||
jar: jar3,
|
||||
headers: {
|
||||
'x-csrf-token': csrfToken,
|
||||
},
|
||||
body: {
|
||||
type: 'post',
|
||||
id: result.postData.pid,
|
||||
reason: 'foobar',
|
||||
},
|
||||
json: true,
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
assert.strictEqual(statusCode, 403);
|
||||
|
||||
// Handle dev mode test
|
||||
delete body.stack;
|
||||
|
||||
assert.deepStrictEqual(body, {
|
||||
status: {
|
||||
code: 'forbidden',
|
||||
message: 'You do not have enough privileges for this action.',
|
||||
},
|
||||
response: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.update()', () => {
|
||||
it('should update a flag\'s properties', (done) => {
|
||||
SocketFlags.update({ uid: 2 }, {
|
||||
flagId: 2,
|
||||
data: [{
|
||||
name: 'state',
|
||||
value: 'wip',
|
||||
}],
|
||||
}, (err, history) => {
|
||||
assert.ifError(err);
|
||||
assert(Array.isArray(history));
|
||||
assert(history[0].fields.hasOwnProperty('state'));
|
||||
assert.strictEqual('[[flags:state-wip]]', history[0].fields.state);
|
||||
done();
|
||||
it('should update a flag\'s properties', async () => {
|
||||
const { response } = await request({
|
||||
method: 'put',
|
||||
uri: `${nconf.get('url')}/api/v3/flags/2`,
|
||||
jar,
|
||||
headers: {
|
||||
'x-csrf-token': csrfToken,
|
||||
},
|
||||
body: {
|
||||
state: 'wip',
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
|
||||
const { history } = response;
|
||||
assert(Array.isArray(history));
|
||||
assert(history[0].fields.hasOwnProperty('state'));
|
||||
assert.strictEqual('[[flags:state-wip]]', history[0].fields.state);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.appendNote()', () => {
|
||||
it('should append a note to the flag', (done) => {
|
||||
SocketFlags.appendNote({ uid: 2 }, {
|
||||
flagId: 2,
|
||||
note: 'lorem ipsum dolor sit amet',
|
||||
}, (err, data) => {
|
||||
assert.ifError(err);
|
||||
assert(data.hasOwnProperty('notes'));
|
||||
assert(Array.isArray(data.notes));
|
||||
assert.strictEqual('lorem ipsum dolor sit amet', data.notes[0].content);
|
||||
assert.strictEqual(2, data.notes[0].uid);
|
||||
|
||||
assert(data.hasOwnProperty('history'));
|
||||
assert(Array.isArray(data.history));
|
||||
assert.strictEqual(1, Object.keys(data.history[0].fields).length);
|
||||
assert(data.history[0].fields.hasOwnProperty('notes'));
|
||||
done();
|
||||
it('should append a note to the flag', async () => {
|
||||
const { response } = await request({
|
||||
method: 'post',
|
||||
uri: `${nconf.get('url')}/api/v3/flags/2/notes`,
|
||||
jar,
|
||||
headers: {
|
||||
'x-csrf-token': csrfToken,
|
||||
},
|
||||
body: {
|
||||
note: 'lorem ipsum dolor sit amet',
|
||||
datetime: 1626446956652,
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
|
||||
assert(response.hasOwnProperty('notes'));
|
||||
assert(Array.isArray(response.notes));
|
||||
assert.strictEqual('lorem ipsum dolor sit amet', response.notes[0].content);
|
||||
assert.strictEqual(2, response.notes[0].uid);
|
||||
|
||||
assert(response.hasOwnProperty('history'));
|
||||
assert(Array.isArray(response.history));
|
||||
assert.strictEqual(1, Object.keys(response.history[response.history.length - 1].fields).length);
|
||||
assert(response.history[response.history.length - 1].fields.hasOwnProperty('notes'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('.deleteNote()', () => {
|
||||
it('should delete a note from a flag', async () => {
|
||||
const { response } = await request({
|
||||
method: 'delete',
|
||||
uri: `${nconf.get('url')}/api/v3/flags/2/notes/1626446956652`,
|
||||
jar,
|
||||
headers: {
|
||||
'x-csrf-token': csrfToken,
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
|
||||
assert(Array.isArray(response.history));
|
||||
assert(Array.isArray(response.notes));
|
||||
assert.strictEqual(response.notes.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user