Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const RightService = require('../../../services/RightService');

module.exports = async (req, res) => {
const caverId = req.param('caverId');
const caveId = req.param('caveId');
const entranceId = req.param('entranceId');

const hasAdminRight = RightService.hasGroup(
req.token?.groups,
Expand All @@ -18,39 +18,35 @@ module.exports = async (req, res) => {
!hasModeratorRight &&
req.token.id !== parseInt(caverId, 10)
) {
return res.forbidden('You can only manage your own explored caves.');
return res.forbidden('You can only manage your own explored entrances.');
}

const cave = await TCave.findOne({ id: caveId });
if (!cave || cave.isDeleted) {
return res.notFound(`Cave with id ${caveId} not found.`);
const entrance = await TEntrance.findOne({ id: entranceId });
if (!entrance || entrance.isDeleted) {
return res.notFound(`Entrance with id ${entranceId} not found.`);
}

const caver = await TCaver.findOne({ id: caverId });
if (!caver || caver.isDeleted) {
return res.notFound(`Caver with id ${caverId} not found.`);
}

// Check if a relationship already exists
const existingQuery = `
SELECT 1 FROM j_caver_cave_explorer
WHERE id_cave = $1 AND id_caver = $2
// Atomic upsert: INSERT with ON CONFLICT eliminates the TOCTOU race
const insertQuery = `
INSERT INTO j_caver_entrance_explorer (id_entrance, id_caver)
VALUES ($1, $2)
ON CONFLICT (id_entrance, id_caver) DO NOTHING
`;
const existingResult = await sails.sendNativeQuery(existingQuery, [
caveId,
const result = await sails.sendNativeQuery(insertQuery, [
entranceId,
caverId,
]);

if (existingResult.rows.length > 0) {
return res.badRequest('Caver is already exploring this cave.');
if (result.rowCount === 0) {
return res.conflict(
'Caver is already registered as an explorer of this entrance.'
);
}

// Create the relationship
const insertQuery = `
INSERT INTO j_caver_cave_explorer (id_cave, id_caver)
VALUES ($1, $2)
`;
await sails.sendNativeQuery(insertQuery, [caveId, caverId]);

return res.status(204).send();
};
10 changes: 8 additions & 2 deletions api/controllers/v1/caver/delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const ControllerService = require('../../../services/ControllerService');
const RightService = require('../../../services/RightService');
const { toCaver } = require('../../../services/mapping/converters');
const CaverService = require('../../../services/CaverService');
const CommonService = require('../../../services/CommonService');

const DEFAULT_DELETED_CAVER_ID = 8;

Expand Down Expand Up @@ -38,7 +39,7 @@ module.exports = async (req, res) => {
let mergeIntoEntity;
if (shouldMergeInto) {
mergeIntoEntity = await TCaver.findOne(mergeIntoId)
.populate('exploredCaves')
.populate('exploredEntrances')
.populate('groups')
.populate('subscribedToCountries')
.populate('subscribedToMassifs')
Expand Down Expand Up @@ -150,13 +151,18 @@ module.exports = async (req, res) => {
: null;

await Promise.all([
linkedEntitiesDeleteOrMerge('exploredCaves'),
linkedEntitiesDeleteOrMerge('exploredEntrances'),
linkedEntitiesDeleteOrMerge('groups'),
linkedEntitiesDeleteOrMerge('subscribedToCountries'),
linkedEntitiesDeleteOrMerge('subscribedToMassifs'),
linkedEntitiesDeleteOrMerge('subscribedToRegions'),
linkedEntitiesDeleteOrMerge('grottos'),
linkedEntitiesDeleteOrMerge('documents', allDocumentIds),
// Clean up legacy j_caver_cave_explorer rows (table preserved, no Waterline association)
CommonService.query(
'DELETE FROM j_caver_cave_explorer WHERE id_caver = $1',
[caverId]
),
]);

await TCaver.destroyOne({ id: caverId });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const RightService = require('../../../services/RightService');

module.exports = async (req, res) => {
const caverId = req.param('caverId');
const caveId = req.param('caveId');
const entranceId = req.param('entranceId');

const hasAdminRight = RightService.hasGroup(
req.token?.groups,
Expand All @@ -18,12 +18,12 @@ module.exports = async (req, res) => {
!hasModeratorRight &&
req.token.id !== parseInt(caverId, 10)
) {
return res.forbidden('You can only manage your own explored caves.');
return res.forbidden('You can only manage your own explored entrances.');
}

const cave = await TCave.findOne({ id: caveId });
if (!cave || cave.isDeleted) {
return res.notFound(`Cave with id ${caveId} not found.`);
const entrance = await TEntrance.findOne({ id: entranceId });
if (!entrance || entrance.isDeleted) {
return res.notFound(`Entrance with id ${entranceId} not found.`);
}

const caver = await TCaver.findOne({ id: caverId });
Expand All @@ -33,23 +33,25 @@ module.exports = async (req, res) => {

// Check if a relationship exists
const existingQuery = `
SELECT 1 FROM j_caver_cave_explorer
WHERE id_cave = $1 AND id_caver = $2
SELECT 1 FROM j_caver_entrance_explorer
WHERE id_entrance = $1 AND id_caver = $2
`;
const existingResult = await sails.sendNativeQuery(existingQuery, [
caveId,
entranceId,
caverId,
]);

if (existingResult.rows.length === 0) {
return res.badRequest('Caver is not exploring this cave.');
return res.notFound(
'Caver is not registered as an explorer of this entrance.'
);
}

// Remove the relationship
const deleteQuery = `
DELETE FROM j_caver_cave_explorer
WHERE id_cave = $1 AND id_caver = $2
DELETE FROM j_caver_entrance_explorer
WHERE id_entrance = $1 AND id_caver = $2
`;
await sails.sendNativeQuery(deleteQuery, [caveId, caverId]);
await sails.sendNativeQuery(deleteQuery, [entranceId, caverId]);
return res.status(204).send();
};
22 changes: 22 additions & 0 deletions api/models/JCaverEntranceExplorer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* JCaverEntranceExplorer.js
*
* @description :: jCaverEntranceExplorer model
* @docs :: http://sailsjs.org/#!documentation/models
*/

module.exports = {
tableName: 'j_caver_entrance_explorer',

attributes: {
caver: {
columnName: 'id_caver',
model: 'TCaver',
},

entrance: {
columnName: 'id_entrance',
model: 'TEntrance',
},
},
};
6 changes: 0 additions & 6 deletions api/models/TCave.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,6 @@ module.exports = {
through: 'JGrottoCaveExplorer',
},

explorerCavers: {
collection: 'TCaver',
via: 'cave',
through: 'JCaverCaveExplorer',
},

partneringGrottos: {
collection: 'TGrotto',
via: 'cave',
Expand Down
6 changes: 3 additions & 3 deletions api/models/TCaver.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,10 @@ module.exports = {
through: 'JCaverGroup',
},

exploredCaves: {
collection: 'TCave',
exploredEntrances: {
collection: 'TEntrance',
via: 'caver',
through: 'JCaverCaveExplorer',
through: 'JCaverEntranceExplorer',
},

subscribedToMassifs: {
Expand Down
6 changes: 6 additions & 0 deletions api/models/TEntrance.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ module.exports = {
via: 'entrance',
},

explorerCavers: {
collection: 'TCaver',
via: 'entrance',
through: 'JCaverEntranceExplorer',
},

redirectTo: {
type: 'number',
allowNull: true,
Expand Down
23 changes: 1 addition & 22 deletions api/services/CaverService.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ module.exports = {
limit: 10,
sort: [{ dateInscription: 'DESC' }],
})
.populate('exploredCaves')
.populate('exploredEntrances')
.populate('grottos')
.populate('groups')
.populate('subscribedToCountries')
Expand All @@ -82,25 +82,6 @@ module.exports = {
? 'CAVER'
: 'AUTHOR';

// Set entrances for explored caves
// eslint-disable-next-line global-require
const CaveService = require('./CaveService');
await CaveService.setEntrances(caver.exploredCaves);

await NameService.setNames([...caver.exploredCaves], 'cave');

// Split caves between entrances and networks (cave)
caver.exploredNetworks = [];
caver.exploredEntrances = [];
for (const cave of caver.exploredCaves) {
if (cave.entrances.length > 1) {
caver.exploredNetworks.push(cave);
}
if (cave.entrances.length === 1) {
caver.exploredEntrances.push(cave.entrances.pop());
}
}

await Promise.all([
NameService.setNames(caver.exploredEntrances, 'entrance'),
NameService.setNames(caver.grottos, 'grotto'),
Expand All @@ -117,9 +98,7 @@ module.exports = {
language: caver.language,
groups: caver.groups,
grottos: caver.grottos,
exploredCaves: caver.exploredCaves,
exploredEntrances: caver.exploredEntrances,
exploredNetworks: caver.exploredNetworks,
documents: caver.documents,
subscribedToMassifs: caver.subscribedToMassifs,
subscribedToCountries: caver.subscribedToCountries,
Expand Down
1 change: 0 additions & 1 deletion api/services/mapping/converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ const c = {

// Convert collections
listParser('exploredEntrances', c.toSimpleEntrance);
listParser('exploredNetworks', c.toSimpleCave);
listParser('groups', (group) => group);
listParser('documents', c.toSimpleDocument);
listParser('grottos', c.toSimpleOrganization, 'organizations');
Expand Down
40 changes: 19 additions & 21 deletions assets/swaggerV1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -928,62 +928,64 @@ paths:
'403':
description: You are not authorized to remove a caver from a group.

'/caves/{caveId}/cavers/{caverId}':
'/entrances/{entranceId}/cavers/{caverId}':
put:
tags:
- caves
- entrances
- cavers
description: Add a caver as a cave explorer. Users can only add caves to their own profile. Moderators and administrators can add caves to any user's profile.
description: Add a caver as an entrance explorer. Users can only add entrances to their own profile. Moderators and administrators can add entrances to any user's profile.
parameters:
- name: caveId
- name: entranceId
in: path
description: Cave id to add the explorer to.
description: Entrance id to add the explorer to.
required: true
schema:
type: string
- name: caverId
in: path
description: Caver id to be added as cave explorer.
description: Caver id to be added as entrance explorer.
required: true
schema:
type: string
responses:
'204':
description: Successful operation
'400':
description: Cave not found or caver is already exploring this cave.
'401':
description: Authentication required.
'403':
description: You can only manage your own explored caves (unless you are a moderator or administrator).
description: You can only manage your own explored entrances (unless you are a moderator or administrator).
'404':
description: Entrance or caver not found.
'409':
description: Caver is already registered as an explorer of this entrance.

delete:
tags:
- caves
- entrances
- cavers
description: Remove a caver as a cave explorer. Users can only remove caves from their own profile. Moderators and administrators can remove caves from any user's profile.
description: Remove a caver as an entrance explorer. Users can only remove entrances from their own profile. Moderators and administrators can remove entrances from any user's profile.
parameters:
- name: caveId
- name: entranceId
in: path
description: Cave id to remove the explorer from.
description: Entrance id to remove the explorer from.
required: true
schema:
type: string
- name: caverId
in: path
description: Caver id to be removed as cave explorer.
description: Caver id to be removed as entrance explorer.
required: true
schema:
type: string
responses:
'204':
description: Successful operation
'400':
description: Cave not found or caver is not exploring this cave.
'401':
description: Authentication required.
'403':
description: You can only manage your own explored caves (unless you are a moderator or administrator).
description: You can only manage your own explored entrances (unless you are a moderator or administrator).
'404':
description: Entrance, caver, or explorer relationship not found.

'/cavers/{caverId}/organizations/{organizationId}':
put:
Expand Down Expand Up @@ -7515,10 +7517,6 @@ components:
type: array
items:
$ref: '#/components/schemas/Entrance'
exploredNetworks:
type: array
items:
$ref: '#/components/schemas/Cave'
groups:
type: array
items:
Expand Down
4 changes: 2 additions & 2 deletions config/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ module.exports.policies = {
'v1/caver/users-count': true,
'v1/caver/find': ['validateId'],
'v1/caver/get-subscriptions': ['validateId'],
'v1/caver/add-explored-cave': 'tokenAuth',
'v1/caver/add-explored-entrance': 'tokenAuth',
'v1/caver/create': 'tokenAuth',
'v1/caver/get-groups': 'tokenAuth',
'v1/caver/get-admins': 'tokenAuth',
Expand All @@ -74,7 +74,7 @@ module.exports.policies = {
'v1/caver/get-contributors': 'tokenAuth',
'v1/caver/get-users': 'tokenAuth',
'v1/caver/put-on-group': 'tokenAuth',
'v1/caver/remove-explored-cave': 'tokenAuth',
'v1/caver/remove-explored-entrance': 'tokenAuth',
'v1/caver/ban': 'tokenAuth',
'v1/caver/unban': 'tokenAuth',
'v1/caver/remove-from-group': 'tokenAuth',
Expand Down
Loading