Skip to content

Commit 75560f6

Browse files
committed
Merge branch 'dev' into main
2 parents 25331af + 41623d8 commit 75560f6

File tree

27 files changed

+450
-188
lines changed

27 files changed

+450
-188
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { trpc } from 'src/trpc/server';
22

33
import { makePublicProcedure } from './make-public';
4+
import { setJoinRequestsAllowedProcedure } from './set-join-requests-allowed';
45

56
export const privacyRouter = trpc.router({
67
makePublic: makePublicProcedure(),
8+
setJoinRequestsAllowed: setJoinRequestsAllowedProcedure(),
79
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { isNanoID } from '@stdlib/misc';
2+
import { checkRedlockSignalAborted } from '@stdlib/redlock';
3+
import { once } from 'lodash';
4+
import type { InferProcedureOpts } from 'src/trpc/helpers';
5+
import { authProcedure } from 'src/trpc/helpers';
6+
import { z } from 'zod';
7+
8+
const baseProcedure = authProcedure.input(
9+
z.object({
10+
groupId: z.string().refine(isNanoID),
11+
12+
areJoinRequestsAllowed: z.boolean(),
13+
}),
14+
);
15+
16+
export const setJoinRequestsAllowedProcedure = once(() =>
17+
baseProcedure.mutation(setJoinRequestsAllowed),
18+
);
19+
20+
export async function setJoinRequestsAllowed({
21+
ctx,
22+
input,
23+
}: InferProcedureOpts<typeof baseProcedure>) {
24+
return await ctx.usingLocks(
25+
[[`user-lock:${ctx.userId}`], [`group-lock:${input.groupId}`]],
26+
async (signals) => {
27+
return await ctx.dataAbstraction.transaction(async (dtrx) => {
28+
// Assert agent is subscribed
29+
30+
await ctx.assertUserSubscribed({ userId: ctx.userId });
31+
32+
// Check if user has sufficient permissions
33+
34+
await ctx.assertSufficientGroupPermissions({
35+
userId: ctx.userId,
36+
groupId: input.groupId,
37+
permission: 'editGroupSettings',
38+
});
39+
40+
await ctx.dataAbstraction.hmset(
41+
'group',
42+
input.groupId,
43+
{ 'are-join-requests-allowed': input.areJoinRequestsAllowed },
44+
{ dtrx },
45+
);
46+
47+
checkRedlockSignalAborted(signals);
48+
});
49+
},
50+
);
51+
}

apps/app-server/src/websocket/groups/change-user-role.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export function registerGroupsChangeUserRole(
3939
await ctx.usingLocks(
4040
[
4141
[`user-lock:${ctx.userId}`],
42-
[`user-lock:${input.patientId}`],
42+
...(input.patientId !== ctx.userId
43+
? [[`user-lock:${input.patientId}`]]
44+
: []),
4345
[`group-lock:${input.groupId}`],
4446
],
4547
performCommunication,

apps/app-server/src/websocket/groups/join-requests/send.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,17 @@ export async function sendStep1({
5555

5656
await ctx.assertUserSubscribed({ userId: ctx.userId });
5757

58-
const [groupJoinRequestRejected, groupMemberRole] = await Promise.all([
58+
const [
59+
groupAreJoinRequestsAllowed,
60+
groupJoinRequestRejected,
61+
groupMemberRole,
62+
] = await Promise.all([
63+
ctx.dataAbstraction.hget(
64+
'group',
65+
input.groupId,
66+
'are-join-requests-allowed',
67+
),
68+
5969
ctx.dataAbstraction.hget(
6070
'group-join-request',
6171
`${input.groupId}:${ctx.userId}`,
@@ -69,6 +79,15 @@ export async function sendStep1({
6979
),
7080
]);
7181

82+
// Check if group allows join requests
83+
84+
if (!groupAreJoinRequestsAllowed) {
85+
throw new TRPCError({
86+
code: 'FORBIDDEN',
87+
message: 'This group does not allow join requests.',
88+
});
89+
}
90+
7291
// Check if user has been rejected from the group
7392

7493
if (groupJoinRequestRejected) {

apps/app-server/src/websocket/groups/remove-user.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export function registerGroupsRemoveUser(fastify: ReturnType<typeof Fastify>) {
3636
await ctx.usingLocks(
3737
[
3838
[`user-lock:${ctx.userId}`],
39-
[`user-lock:${input.patientId}`],
39+
...(input.patientId !== ctx.userId
40+
? [[`user-lock:${input.patientId}`]]
41+
: []),
4042
[`group-lock:${input.groupId}`],
4143
],
4244
performCommunication,

apps/client/components.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ declare module '@vue/runtime-core' {
99
export interface GlobalComponents {
1010
BillingFrequencyToggle: typeof import('./src/components/BillingFrequencyToggle.vue')['default']
1111
Checkbox: typeof import('./src/components/Checkbox.vue')['default']
12+
Checklist: typeof import('./src/components/Checklist.vue')['default']
1213
ColorPalette: typeof import('./src/components/ColorPalette.vue')['default']
1314
ColorSquare: typeof import('./src/components/ColorSquare.vue')['default']
1415
Combobox: typeof import('./src/components/Combobox.vue')['default']
1516
CopyBtn: typeof import('./src/components/CopyBtn.vue')['default']
1617
CustomDialog: typeof import('./src/components/CustomDialog.vue')['default']
18+
CustomInfiniteScroll: typeof import('./src/components/CustomInfiniteScroll.vue')['default']
1719
DeepBtn: typeof import('./src/components/DeepBtn.vue')['default']
1820
DeepBtnDropdown: typeof import('./src/components/DeepBtnDropdown.vue')['default']
1921
DisplayBtn: typeof import('./src/components/DisplayBtn.vue')['default']
@@ -26,6 +28,7 @@ declare module '@vue/runtime-core' {
2628
MiniSidebarBtn: typeof import('./src/components/MiniSidebarBtn.vue')['default']
2729
PageItem: typeof import('./src/components/PageItem.vue')['default']
2830
PageItemContent: typeof import('./src/components/PageItemContent.vue')['default']
31+
PassthroughComponent: typeof import('./src/components/PassthroughComponent.vue')['default']
2932
PasswordField: typeof import('./src/components/PasswordField.vue')['default']
3033
QMenuHover: typeof import('./src/components/QMenuHover.vue')['default']
3134
Radio: typeof import('./src/components/Radio.vue')['default']

apps/client/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@deepnotes/client",
33
"description": "DeepNotes",
44
"homepage": "https://deepnotes.app",
5-
"version": "1.0.13",
5+
"version": "1.0.14",
66
"author": "Gustavo Toyota <[email protected]>",
77
"dependencies": {
88
"@_ueberdosis/prosemirror-tables": "~1.1.3",

apps/client/quasar.config.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ module.exports = configure(function (ctx) {
6262
{ path: 'sodium.universal' },
6363
{ path: 'i18n.universal' },
6464
{ path: 'vue.universal' },
65-
{ path: 'disable-cache.universal' },
65+
66+
{ path: 'http-headers/disable-cache.universal' },
67+
{ path: 'http-headers/x-frame-options.universal' },
68+
{ path: 'http-headers/referrer-policy.universal' },
6669

6770
{ path: 'array-at-polyfill.client', server: false },
6871
{ path: 'logger.client', server: false },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { boot } from 'quasar/wrappers';
2+
3+
export default boot(async ({ ssrContext }) => {
4+
ssrContext?.res.setHeader('Referrer-Policy', 'no-referrer');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { boot } from 'quasar/wrappers';
2+
3+
export default boot(async ({ ssrContext }) => {
4+
ssrContext?.res.setHeader('X-Frame-Options', 'DENY');
5+
});
+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<template>
2+
<q-list>
3+
<PassthroughComponent
4+
:is="itemsWrapper"
5+
v-bind="wrapperProps"
6+
v-on="wrapperEvents"
7+
>
8+
<slot
9+
v-if="itemIds.length === 0"
10+
name="empty"
11+
></slot>
12+
13+
<q-item
14+
v-for="(itemId, itemIndex) of itemIds"
15+
:key="itemId"
16+
clickable
17+
v-ripple
18+
:style="{
19+
'background-color': selectedItemIds.has(itemId) ? '#505050' : '',
20+
}"
21+
@click="(event) => selectItem(itemId, event as any)"
22+
v-bind="props.itemProps?.(itemId, itemIndex)"
23+
>
24+
<q-item-section
25+
avatar
26+
style="padding-right: 4px"
27+
>
28+
<Checkbox
29+
:model-value="selectedItemIds.has(itemId)"
30+
style="pointer-events: none"
31+
/>
32+
</q-item-section>
33+
34+
<slot
35+
name="item"
36+
:item-id="itemId"
37+
:item-index="itemIndex"
38+
></slot>
39+
</q-item>
40+
</PassthroughComponent>
41+
</q-list>
42+
</template>
43+
44+
<script setup lang="ts">
45+
import type { QItemProps, QListProps } from 'quasar';
46+
import type { Component } from 'vue';
47+
48+
const emit = defineEmits(['select', 'unselect']);
49+
50+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
51+
interface Props extends QListProps {
52+
itemIds: string[];
53+
selectedItemIds: Set<string>;
54+
55+
itemProps?: (
56+
itemId: string,
57+
itemIndex: number,
58+
) => QItemProps & Record<string, unknown>;
59+
60+
itemsWrapper?: string | Component;
61+
wrapperProps?: Record<string, unknown>;
62+
wrapperEvents?: Record<string, unknown>;
63+
}
64+
65+
const props = defineProps<Props>();
66+
67+
let lastSelectedItemId: string;
68+
69+
function selectItem(itemId: string, event: MouseEvent) {
70+
if (event.shiftKey || internals.mobileAltKey) {
71+
const sourceItemIndex = props.itemIds.indexOf(lastSelectedItemId);
72+
const targetItemIndex = props.itemIds.indexOf(itemId);
73+
74+
if (
75+
sourceItemIndex >= 0 &&
76+
targetItemIndex >= 0 &&
77+
sourceItemIndex !== targetItemIndex
78+
) {
79+
const sign = Math.sign(targetItemIndex - sourceItemIndex);
80+
81+
const add = !props.selectedItemIds.has(itemId);
82+
83+
for (let i = sourceItemIndex; i !== targetItemIndex + sign; i += sign) {
84+
if (add) {
85+
props.selectedItemIds.add(props.itemIds[i]);
86+
emit('select', props.itemIds[i]);
87+
} else {
88+
props.selectedItemIds.delete(props.itemIds[i]);
89+
emit('unselect', props.itemIds[i]);
90+
}
91+
}
92+
}
93+
} else {
94+
if (props.selectedItemIds.has(itemId)) {
95+
props.selectedItemIds.delete(itemId);
96+
emit('unselect', itemId);
97+
} else {
98+
props.selectedItemIds.add(itemId);
99+
emit('select', itemId);
100+
}
101+
}
102+
103+
lastSelectedItemId = itemId;
104+
}
105+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<template>
2+
<q-infinite-scroll>
3+
<slot></slot>
4+
5+
<template v-slot:loading>
6+
<div class="row justify-center q-my-md">
7+
<q-circular-progress
8+
indeterminate
9+
size="md"
10+
/>
11+
</div>
12+
</template>
13+
</q-infinite-scroll>
14+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<template>
2+
<component
3+
v-if="is != null"
4+
:is="is"
5+
>
6+
<slot></slot>
7+
</component>
8+
9+
<slot v-else></slot>
10+
</template>
11+
12+
<script setup lang="ts">
13+
import type { Component } from 'vue';
14+
15+
defineProps<{
16+
is?: string | Component;
17+
}>();
18+
</script>

apps/client/src/layouts/PagesLayout/MainContent/DisplayPage/DisplayScreens/DisplayUnauthorizedScreen.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
v-if="
66
uiStore().loggedIn &&
77
!realtimeCtx.loading &&
8-
!realtimeCtx.hget('group', page.react.groupId, 'is-personal')
8+
!realtimeCtx.hget('group', page.react.groupId, 'is-personal') &&
9+
realtimeCtx.hget('group', page.react.groupId, 'are-join-requests-allowed')
910
"
1011
>
1112
<Gap style="height: 12px" />

apps/client/src/layouts/PagesLayout/MainToolbar/Notifications/Items/GroupRequestSent.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const canCancelRequest = computed(
7878
const canAcceptRequest = computed(() => {
7979
const rejected = realtimeCtx.hget(
8080
'group-join-request',
81-
`${notificationContent.value.groupId}:${authStore().userId}`,
81+
`${notificationContent.value.groupId}:${notificationContent.value.agentId}`,
8282
'rejected',
8383
);
8484

apps/client/src/layouts/PagesLayout/MainToolbar/Notifications/NotificationsPopup.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
ref="notificationsMenu"
44
anchor="bottom middle"
55
self="top middle"
6-
style="width: 250px; display: flex; flex-direction: column"
6+
style="width: 250px"
77
@before-show="onBeforeShow()"
88
>
99
<template v-if="pagesStore().notifications.items.length === 0">

0 commit comments

Comments
 (0)