Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backend: extend access token #48

Merged
merged 9 commits into from
Jul 3, 2024
2 changes: 2 additions & 0 deletions backend/migrations/20240701212445_extend_permissions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
UPDATE access_tokens
SET permissions = permissions || '{"can_list_entities": true, "can_access_entity": true, "can_add_entity": true, "can_add_comment": true}'::jsonb;
55 changes: 46 additions & 9 deletions backend/src/api/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,12 @@ pub async fn viewer_view_request(
) -> Result<AppJson<EntitiesAndClusters>, AppError> {
tracing::trace!("Received view request {}", request);

if !token.perms.can_list_entities {
return Err(AppError::Forbidden);
}

if !is_token_allowed_for_family(&token, &request.family_id) {
return Err(AppError::Unauthorized);
return Err(AppError::Forbidden);
}

let dyn_config = app_state.dyn_config.read().await;
Expand Down Expand Up @@ -206,8 +210,12 @@ async fn viewer_search_request(
) -> Result<AppJson<ViewerCachedEntitiesWithPagination>, AppError> {
tracing::trace!("Received search request {}", request);

if !token.perms.can_list_entities {
return Err(AppError::Forbidden);
}

if !is_token_allowed_for_family(&token, &request.family_id) {
return Err(AppError::Unauthorized);
return Err(AppError::Forbidden);
}

// Check if some of the constraints are forbidden
Expand Down Expand Up @@ -245,14 +253,14 @@ async fn viewer_search_request(
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct PublicNewEntityRequest {
entity: PublicNewEntity,
comment: PublicNewComment,
comment: Option<PublicNewComment>,
hcaptcha_token: Option<String>,
}

#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct PublicNewEntityResponse {
entity: PublicEntity,
comment: PublicComment,
comment: Option<PublicComment>,
}

async fn check_captcha(state: AppState, response: Option<String>) -> Result<(), AppError> {
Expand Down Expand Up @@ -299,16 +307,26 @@ async fn check_captcha(state: AppState, response: Option<String>) -> Result<(),
async fn viewer_new_entity(
DbConn(mut conn): DbConn,
State(state): State<AppState>,
token: MapUserTokenClaims,
Json(request): Json<PublicNewEntityRequest>,
) -> Result<AppJson<PublicNewEntityResponse>, AppError> {
if !token.perms.can_add_entity {
return Err(AppError::Forbidden);
}

if !token.perms.can_add_comment && request.comment.is_some() {
return Err(AppError::Forbidden);
}

check_captcha(state, request.hcaptcha_token).await?;

let db_entity = PublicEntity::new(request.entity, &mut conn).await?;
let mut db_comment = None;

let mut new_comment = request.comment;
new_comment.entity_id = db_entity.id;

let db_comment = PublicComment::new(new_comment, &mut conn).await?;
if let Some(mut comment) = request.comment {
comment.entity_id = db_entity.id;
db_comment = Some(PublicComment::new(comment, &mut conn).await?);
}

Ok(AppJson(PublicNewEntityResponse {
entity: db_entity,
Expand All @@ -334,8 +352,19 @@ pub struct NewCommentRequest {
async fn viewer_new_comment(
DbConn(mut conn): DbConn,
State(state): State<AppState>,
token: MapUserTokenClaims,
Json(request): Json<NewCommentRequest>,
) -> Result<AppJson<PublicComment>, AppError> {
if !token.perms.can_add_comment {
return Err(AppError::Forbidden);
}

let target_entity = PublicEntity::get(request.comment.entity_id, &mut conn).await?;

if !is_token_allowed_for_family(&token, &target_entity.family_id) {
return Err(AppError::Forbidden);
}

check_captcha(state, request.hcaptcha_token).await?;
let db_comment = PublicComment::new(request.comment, &mut conn).await?;
Ok(AppJson(db_comment))
Expand Down Expand Up @@ -371,8 +400,16 @@ async fn viewer_fetch_entity(
Path(id): Path<Uuid>,
Json(request): Json<FetchEntityRequest>,
) -> Result<AppJson<FetchedEntity>, AppError> {
if !token.perms.can_access_entity {
return Err(AppError::Forbidden);
}

let entity = PublicEntity::get(id, &mut conn).await?;

if !is_token_allowed_for_family(&token, &entity.family_id) {
return Err(AppError::Forbidden);
}

let can_read_entity = (token.perms.families_policy.allow_all
|| token
.perms
Expand Down Expand Up @@ -463,7 +500,7 @@ async fn viewer_fetch_entity(
.collect();

if !can_read_entity && filtered_children.is_empty() {
return Err(AppError::Unauthorized);
return Err(AppError::Forbidden);
}

let comments = match token.perms.can_access_comments {
Expand Down
8 changes: 8 additions & 0 deletions backend/src/api/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ pub struct BootstrapResponse {
categories: Vec<Category>,
allowed_categories: Vec<Uuid>,
allowed_tags: Vec<Uuid>,
can_list_entities: bool,
can_access_entity: bool,
can_add_entity: bool,
can_access_comments: bool,
can_add_comment: bool,
tags: Vec<Tag>,
cartography_init_config: CartographyInitConfig,
}
Expand Down Expand Up @@ -177,7 +181,11 @@ async fn bootstrap(
categories,
allowed_categories,
allowed_tags,
can_list_entities: perms.can_list_entities,
can_access_entity: perms.can_access_entity,
can_add_entity: perms.can_add_entity,
can_access_comments: perms.can_access_comments,
can_add_comment: perms.can_add_comment,
tags,
cartography_init_config: dyn_config.cartography_init.clone(),
};
Expand Down
5 changes: 5 additions & 0 deletions backend/src/models/access_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ pub struct Permissions {
pub categories_policy: PermissionPolicy,
pub tags_policy: PermissionPolicy,
pub geographic_restrictions: Option<MultiPolygon>,

pub can_list_entities: bool,
pub can_access_entity: bool,
pub can_access_comments: bool,
pub can_add_entity: bool,
pub can_add_comment: bool,
}

#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/viewer/CommentAddForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,15 @@
</template>

<script setup lang="ts">
import type { EntityOrCommentData, Family, FormField, PublicEntity, PublicNewComment } from '~/lib'
import type { EntityOrCommentData, Family, FormField, PublicEntity, PublicNewComment, ViewerSearchedCachedEntity } from '~/lib'
import { isValidRichText, isValidText } from '~/lib/validation'
import state from '~/lib/viewer-state'

const formVisible = ref(false)

const props = defineProps<{
family: Family
entity: PublicEntity
entity: PublicEntity | ViewerSearchedCachedEntity
}>()

const processingRequest = ref(false)
Expand Down
37 changes: 25 additions & 12 deletions frontend/components/viewer/EntityAddForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
v-for="page in Array.from({ length: entityPageCount+1 }, (_, i) => i)"
:key="`EntityPage${page}`"
class="flex grow flex-col gap-4"
@submit.prevent="curr_page += 1"
@submit.prevent="include_comment || curr_page < lastNonCaptchaPage ? curr_page += 1 : onSave()"
>
<div
v-if="curr_page == page"
Expand All @@ -34,6 +34,13 @@
<FormAdresses
v-model:locations="editedEntity!.locations"
/>

<AdminInputSwitchField
v-if="state.permissions?.can_add_comment"
id="include_comment"
label="Ajouter un commentaire"
v-model:="include_comment"
/>
</template>
<template v-else>
<FormDynamicField
Expand All @@ -53,9 +60,9 @@
@click="curr_page -= 1"
/>
<Button
:label="curr_page == entityPageCount ? 'Suivant' : 'Suivant'"
:label="include_comment || curr_page < entityPageCount ? 'Suivant' : 'Sauvegarder'"
type="submit"
outlined
:outlined="include_comment || curr_page < entityPageCount"
:disabled="!isEntityPageValid(page)"
/>
</span>
Expand All @@ -64,10 +71,10 @@

<!-- Comment Form Pages -->
<form
v-for="page in Array.from({ length: commentPageCount+2 }, (_, i) => i)"
v-for="page in Array.from({ length: commentPageCount + 2 }, (_, i) => i)"
:key="`CommentPage${page}`"
class="flex grow flex-col gap-4 max-w-[30rem]"
@submit.prevent="curr_page < (entityPageCount + commentPageCount + 1) ? curr_page += 1 : onSave()"
@submit.prevent="curr_page < lastNonCaptchaPage ? curr_page += 1 : onSave()"
>
<div
v-if="curr_page == entityPageCount + 1 + page"
Expand Down Expand Up @@ -110,9 +117,9 @@
@click="curr_page -= 1"
/>
<Button
:label="curr_page == (entityPageCount + commentPageCount + 1) ? 'Sauvegarder' : 'Suivant'"
:label="curr_page == lastNonCaptchaPage ? 'Sauvegarder' : 'Suivant'"
type="submit"
:outlined="curr_page != (entityPageCount + commentPageCount + 1)"
:outlined="curr_page != lastNonCaptchaPage"
:loading="processingRequest"
:disabled="processingRequest || !isCommentPageValid(page)"
/>
Expand Down Expand Up @@ -185,6 +192,10 @@ const curr_page = ref(0)
const entityPageCount = ref(0)
const commentPageCount = ref(0)

const include_comment = ref(!!state.permissions?.can_add_comment)
const lastNonCaptchaPage = computed(() => include_comment.value ? entityPageCount.value + commentPageCount.value + 1 : entityPageCount.value)
const captchaPage = computed(() => entityPageCount.value + commentPageCount.value + 2)

const entityFieldValid = ref(
props.family.entity_form.fields
.reduce((acc, field) => {
Expand Down Expand Up @@ -236,7 +247,7 @@ watch(
watch(
() => formVisible.value,
(__, _) => {
curr_page.value = Math.min(curr_page.value, entityPageCount.value + commentPageCount.value + 1)
curr_page.value = Math.min(curr_page.value, lastNonCaptchaPage.value)
},
)

Expand Down Expand Up @@ -293,7 +304,7 @@ function hCaptchaError() {

async function onSave() {
if (state.hasSafeModeEnabled) {
curr_page.value += 1
curr_page.value = captchaPage.value
}
else {
await realOnSave(null)
Expand All @@ -305,15 +316,17 @@ async function realOnSave(token: string | null) {
try {
await state.client.createEntity({
entity: editedEntity.value,
comment: editedComment.value,
comment: include_comment.value ? editedComment.value : null,
hcaptcha_token: token,
})
formVisible.value = false
toast.add({ severity: 'success', summary: 'Succès', detail: 'Entité et commentaire ajoutés avec succès', life: 3000 })
toast.add({ severity: 'success', summary: 'Succès',
detail: include_comment.value ? 'Entité et commentaire ajoutés avec succès' : 'Entité ajoutée avec succès', life: 3000 })
reset_refs()
}
catch {
toast.add({ severity: 'error', summary: 'Erreur', detail: 'Erreur lors de l\'ajout de l\'entité ou du commentaire', life: 3000 })
toast.add({ severity: 'error', summary: 'Erreur',
detail: include_comment.value ? 'Erreur lors de l\'ajout de l\'entité ou du commentaire' : 'Erreur lors de l\'ajout de l\'entité', life: 3000 })
}
processingRequest.value = false
}
Expand Down
14 changes: 9 additions & 5 deletions frontend/components/viewer/FullResult.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<div class="flex flex-col md:items-end gap-8">
<div class="flex flex-row-reverse md:flex-row gap-2">
<Button
v-if="state.permissions?.can_access_entity"
label="Voir en détail"
class="flex-auto md:flex-initial whitespace-nowrap"
@click="changeActiveEntity(props.entity)"
Expand All @@ -62,6 +63,11 @@
<AppIcon icon-name="eye" />
</template>
</Button>
<ViewerCommentAddForm
v-if="state.permissions?.can_add_comment && !state.permissions.can_access_entity"
:family="state.activeFamily"
:entity="props.entity"
/>
</div>
</div>
</div>
Expand All @@ -71,18 +77,16 @@
</template>

<script setup lang="ts">
import type { ViewerPaginatedCachedEntities } from '~/lib'
import type { ViewerSearchedCachedEntity } from '~/lib'
import state from '~/lib/viewer-state'

type ReceivedEntity = ViewerPaginatedCachedEntities['entities'][0]

const props = defineProps<{
entity: ReceivedEntity
entity: ViewerSearchedCachedEntity
}>()

const locations = computed(() => props.entity.locations.map(loc => [loc.x, loc.y]))

function changeActiveEntity(entity: ReceivedEntity) {
function changeActiveEntity(entity: ViewerSearchedCachedEntity) {
state.selectEntity(entity.entity_id)
}
</script>
Expand Down
3 changes: 3 additions & 0 deletions frontend/components/viewer/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
</Button>

<Button
v-if="state.permissions?.can_add_entity"
label="Ajouter"
severity="info"
@click="openAddModal()"
Expand Down Expand Up @@ -161,6 +162,7 @@
</Button>

<Button
v-if="state.permissions?.can_add_entity"
label="Ajouter"
severity="info"
@click="openAddModal()"
Expand Down Expand Up @@ -353,6 +355,7 @@
</Dialog>

<ViewerEntityAddForm
v-if="state.permissions?.can_add_entity"
ref="entityAddForm"
:family="state.activeFamily"
:categories="
Expand Down
9 changes: 8 additions & 1 deletion frontend/lib.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type Category = api.components['schemas']['Category']
export type Tag = api.components['schemas']['Tag']

export type PublicEntity = api.components['schemas']['PublicEntity']
export type PublicListedEntity = api.components['schemas']['ListedEntity']
export type PublicListedEntity = api.components['schemas']['PublicListedEntity']
export type PublicNewEntity = api.components['schemas']['PublicNewEntity']
export type PublicNewEntityRequest = api.components['schemas']['PublicNewEntityRequest']
export type PublicNewEntityResponse = api.components['schemas']['PublicNewEntityResponse']
Expand Down Expand Up @@ -71,6 +71,13 @@ export type AccessToken = api.components['schemas']['AccessToken']
permissions: Permissions & { geographic_restrictions: null | [number, number][][] }
}
export type AccessTokenStats = api.components['schemas']['AccessTokenStats']
export type PublicPermissions = {
can_list_entities: boolean
can_access_entity: boolean
can_add_entity: boolean
can_access_comments: boolean
can_add_comment: boolean
}

export type User = api.components['schemas']['User']
export type NewOrUpdatedUser = api.components['schemas']['NewOrUpdatedUser']
Expand Down
Loading