Skip to content

Commit 618be4b

Browse files
authored
⚖️ feat: Terms and Conditions Dialog (danny-avila#3712)
* Added UI for Terms and Conditions Modal Dialogue * Handled the logout on not accepting * Added logic for terms acceptance * Add terms and conditions modal * Fixed bug on terms and conditions modal, clicking out of it won't close it now * Added acceptance of Terms to Database * Removed unnecessary api endpoints from index.js * Added NPM script to reset terms acceptance * Added translations, markdown terms and samples * Merged terms and conditions modal feature * feat/Modal Terms and Conditions Dialog * Amendments as requested by maintainers * Reset package-lock (again)
1 parent 79f9cd5 commit 618be4b

File tree

35 files changed

+393
-8
lines changed

35 files changed

+393
-8
lines changed

api/models/schema/userSchema.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ const userSchema = mongoose.Schema(
122122
type: Date,
123123
expires: 604800, // 7 days in seconds
124124
},
125+
termsAccepted: {
126+
type: Boolean,
127+
default: false,
128+
},
125129
},
130+
126131
{ timestamps: true },
127132
);
128133

api/server/controllers/UserController.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
deleteMessages,
99
deleteUserById,
1010
} = require('~/models');
11+
const User = require('~/models/User');
1112
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
1213
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
1314
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
@@ -20,6 +21,32 @@ const getUserController = async (req, res) => {
2021
res.status(200).send(req.user);
2122
};
2223

24+
const getTermsStatusController = async (req, res) => {
25+
try {
26+
const user = await User.findById(req.user.id);
27+
if (!user) {
28+
return res.status(404).json({ message: 'User not found' });
29+
}
30+
res.status(200).json({ termsAccepted: !!user.termsAccepted });
31+
} catch (error) {
32+
logger.error('Error fetching terms acceptance status:', error);
33+
res.status(500).json({ message: 'Error fetching terms acceptance status' });
34+
}
35+
};
36+
37+
const acceptTermsController = async (req, res) => {
38+
try {
39+
const user = await User.findByIdAndUpdate(req.user.id, { termsAccepted: true }, { new: true });
40+
if (!user) {
41+
return res.status(404).json({ message: 'User not found' });
42+
}
43+
res.status(200).json({ message: 'Terms accepted successfully' });
44+
} catch (error) {
45+
logger.error('Error accepting terms:', error);
46+
res.status(500).json({ message: 'Error accepting terms' });
47+
}
48+
};
49+
2350
const deleteUserFiles = async (req) => {
2451
try {
2552
const userFiles = await getFiles({ user: req.user.id });
@@ -135,6 +162,8 @@ const resendVerificationController = async (req, res) => {
135162

136163
module.exports = {
137164
getUserController,
165+
getTermsStatusController,
166+
acceptTermsController,
138167
deleteUserController,
139168
verifyEmailController,
140169
updateUserPluginsController,

api/server/routes/user.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ const {
66
verifyEmailController,
77
updateUserPluginsController,
88
resendVerificationController,
9+
getTermsStatusController,
10+
acceptTermsController,
911
} = require('~/server/controllers/UserController');
1012

1113
const router = express.Router();
1214

1315
router.get('/', requireJwtAuth, getUserController);
16+
router.get('/terms', requireJwtAuth, getTermsStatusController);
17+
router.post('/terms/accept', requireJwtAuth, acceptTermsController);
1418
router.post('/plugins', requireJwtAuth, updateUserPluginsController);
1519
router.delete('/delete', requireJwtAuth, canDeleteAccount, deleteUserController);
1620
router.post('/verify', verifyEmailController);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useLocalize } from '~/hooks';
2+
import { OGDialog } from '~/components/ui';
3+
import DialogTemplate from '~/components/ui/DialogTemplate';
4+
import { useAuthContext } from '~/hooks';
5+
import Markdown from '~/components/Chat/Messages/Content/Markdown';
6+
import { useToastContext } from '~/Providers';
7+
import { useAcceptTermsMutation } from '~/data-provider';
8+
9+
const TermsAndConditionsModal = ({
10+
open,
11+
onOpenChange,
12+
onAccept,
13+
onDecline,
14+
title,
15+
modalContent,
16+
}: {
17+
open: boolean;
18+
onOpenChange: (isOpen: boolean) => void;
19+
onAccept: () => void;
20+
onDecline: () => void;
21+
title?: string;
22+
contentUrl?: string;
23+
modalContent?: string;
24+
}) => {
25+
const localize = useLocalize();
26+
const { showToast } = useToastContext();
27+
const acceptTermsMutation = useAcceptTermsMutation({
28+
onSuccess: () => {
29+
onAccept();
30+
onOpenChange(false);
31+
},
32+
onError: () => {
33+
showToast({ message: 'Failed to accept terms' });
34+
},
35+
});
36+
37+
const handleAccept = () => {
38+
acceptTermsMutation.mutate();
39+
};
40+
41+
const handleDecline = () => {
42+
onDecline();
43+
onOpenChange(false);
44+
};
45+
46+
const handleOpenChange = (isOpen: boolean) => {
47+
if (open && !isOpen) {
48+
return;
49+
}
50+
onOpenChange(isOpen);
51+
};
52+
53+
return (
54+
<OGDialog open={open} onOpenChange={handleOpenChange}>
55+
<DialogTemplate
56+
title={title ?? localize('com_ui_terms_and_conditions')}
57+
className="w-11/12 max-w-3xl sm:w-3/4 md:w-1/2 lg:w-2/5"
58+
showCloseButton={false}
59+
showCancelButton={false}
60+
main={
61+
<div className="max-h-[60vh] overflow-y-auto p-4">
62+
<div className="prose dark:prose-invert w-full max-w-none !text-black dark:!text-white">
63+
{modalContent ? (
64+
<Markdown content={modalContent} isLatestMessage={false} />
65+
) : (
66+
<p>{localize('com_ui_no_terms_content')}</p>
67+
)}
68+
</div>
69+
</div>
70+
}
71+
buttons={
72+
<>
73+
<button
74+
onClick={handleDecline}
75+
className="border-border-none bg-surface-500 dark:hover:bg-surface-600 inline-flex h-10 items-center justify-center rounded-lg px-4 py-2 text-sm text-white hover:bg-gray-600"
76+
>
77+
{localize('com_ui_decline')}
78+
</button>
79+
<button
80+
onClick={handleAccept}
81+
className="border-border-none bg-surface-500 inline-flex h-10 items-center justify-center rounded-lg px-4 py-2 text-sm text-white hover:bg-green-600 dark:hover:bg-green-600"
82+
>
83+
{localize('com_ui_accept')}
84+
</button>
85+
</>
86+
}
87+
/>
88+
</OGDialog>
89+
);
90+
};
91+
92+
export default TermsAndConditionsModal;

client/src/data-provider/mutations.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,3 +1128,19 @@ export const useResendVerificationEmail = (
11281128
...(options || {}),
11291129
});
11301130
};
1131+
1132+
export const useAcceptTermsMutation = (
1133+
options?: t.AcceptTermsMutationOptions,
1134+
): UseMutationResult<t.TAcceptTermsResponse, unknown, void, unknown> => {
1135+
const queryClient = useQueryClient();
1136+
return useMutation(() => dataService.acceptTerms(), {
1137+
onSuccess: (data, variables, context) => {
1138+
queryClient.setQueryData<t.TUserTermsResponse>([QueryKeys.userTerms], {
1139+
termsAccepted: true,
1140+
});
1141+
options?.onSuccess?.(data, variables, context);
1142+
},
1143+
onError: options?.onError,
1144+
onMutate: options?.onMutate,
1145+
});
1146+
};

client/src/data-provider/queries.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import type {
2828
TCheckUserKeyResponse,
2929
SharedLinkListParams,
3030
SharedLinksResponse,
31+
TUserTermsResponse,
32+
TAcceptTermsResponse,
3133
} from 'librechat-data-provider';
3234
import { findPageForConversation, addFileToCache } from '~/utils';
3335

@@ -564,3 +566,14 @@ export const useGetRandomPrompts = (
564566
},
565567
);
566568
};
569+
570+
export const useUserTermsQuery = (
571+
config?: UseQueryOptions<t.TUserTermsResponse>,
572+
): QueryObserverResult<t.TUserTermsResponse> => {
573+
return useQuery<t.TUserTermsResponse>([QueryKeys.userTerms], () => dataService.getUserTerms(), {
574+
refetchOnWindowFocus: false,
575+
refetchOnReconnect: false,
576+
refetchOnMount: false,
577+
...config,
578+
});
579+
};

client/src/localization/languages/Ar.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,10 @@ export default {
485485
com_ui_terms_of_service: 'شروط الخدمة',
486486
com_ui_min_tags: 'لا يمكن إزالة المزيد من القيم، الحد الأدنى المطلوب هو {0}.',
487487
com_ui_max_tags: 'الحد الأقصى المسموح به هو {0}، باستخدام أحدث القيم.',
488+
com_ui_accept: 'أوافق',
489+
com_ui_decline: 'لا أوافق',
490+
com_ui_terms_and_conditions: 'شروط الخدمة',
491+
com_ui_no_terms_content: 'لا يوجد محتوى لشروط الخدمة',
488492
com_auth_back_to_login: 'العودة إلى تسجيل الدخول',
489493
com_endpoint_message: 'رسالة',
490494
com_endpoint_message_not_appendable: 'عدّل رسالتك أو أعد إنشاءها.',

client/src/localization/languages/Br.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ export default {
183183
com_ui_bookmarks_update_error: 'Houve um erro ao atualizar o favorito',
184184
com_ui_bookmarks_delete_error: 'Houve um erro ao excluir o favorito',
185185
com_ui_bookmarks_add_to_conversation: 'Adicionar à conversa atual',
186+
com_ui_accept: 'Eu aceito',
187+
com_ui_decline: 'Eu não aceito',
188+
com_ui_terms_and_conditions: 'Termos e Condições',
189+
com_ui_no_terms_content: 'Nenhum conteúdo de termos e condições para exibir',
186190
com_auth_error_login:
187191
'Não foi possível fazer login com as informações fornecidas. Por favor, verifique suas credenciais e tente novamente.',
188192
com_auth_error_login_rl:

client/src/localization/languages/De.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,10 @@ export default {
326326
com_ui_bookmarks_delete_error: 'Beim Löschen des Lesezeichens ist ein Fehler aufgetreten',
327327
com_ui_bookmarks_add_to_conversation: 'Zur aktuellen Konversation hinzufügen',
328328
com_ui_bookmarks_filter: 'Lesezeichen filtern...',
329+
com_ui_accept: 'Ich akzeptiere',
330+
com_ui_decline: 'Ich akzeptiere nicht',
331+
com_ui_terms_and_conditions: 'Allgemeine Geschäftsbedingungen',
332+
com_ui_no_terms_content: 'Keine Inhalte der Allgemeinen Geschäftsbedingungen zum Anzeigen',
329333
com_auth_error_login:
330334
'Anmeldung mit den angegebenen Informationen nicht möglich. Bitte überprüfe deine Anmeldedaten und versuche es erneut.',
331335
com_auth_error_login_rl:

client/src/localization/languages/Eng.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,4 +771,8 @@ export default {
771771
com_nav_lang_indonesia: 'Indonesia',
772772
com_nav_lang_hebrew: 'עברית',
773773
com_nav_lang_finnish: 'Suomi',
774+
com_ui_accept: 'I accept',
775+
com_ui_decline: 'I do not accept',
776+
com_ui_terms_and_conditions: 'Terms and Conditions',
777+
com_ui_no_terms_content: 'No terms and conditions content to display',
774778
};

0 commit comments

Comments
 (0)