diff --git a/.env.example b/.env.example index 084e916..47df83d 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,7 @@ EMAIL_PORT= # or 465 for SSL EMAIL_TLS= # or false for SSL (see provider documentation) NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY= +MEDUSA_SECRET_API_KEY= NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo diff --git a/README.md b/README.md index 1125158..ecbbedc 100644 --- a/README.md +++ b/README.md @@ -98,13 +98,13 @@ To access admin pages follow these steps: - Register new user - Go to Profile and add Multi-Factor Authentication - Authenticate with mfa (at current time profile page prompts it again) -- update your role. look at `supabase/sql/super-admin.sql` +- update your `account.application_role` to `super_admin`. - Sign out and Sign in ## Company User - With admin account go to `http://localhost:3000/admin/accounts` -- For Create Company Account to work you need to have rows in `medreport.roles` table. For that you can sql in `supabase/sql/super-admin.sql` +- For Create Company Account to work you need to have rows in `medreport.roles` table. ## Start email server diff --git a/app/(public)/company-offer/_components/company-offer-form.tsx b/app/(public)/company-offer/_components/company-offer-form.tsx index ebcfdcf..0b50e7d 100644 --- a/app/(public)/company-offer/_components/company-offer-form.tsx +++ b/app/(public)/company-offer/_components/company-offer-form.tsx @@ -19,6 +19,8 @@ import { Label } from '@kit/ui/label'; import { Spinner } from '@kit/ui/spinner'; import { Trans } from '@kit/ui/trans'; +import { sendCompanyOfferEmail } from '../_lib/server/company-offer-actions'; + const CompanyOfferForm = () => { const [isLoading, setIsLoading] = useState(false); const router = useRouter(); @@ -34,6 +36,16 @@ const CompanyOfferForm = () => { const onSubmit = async (data: CompanySubmitData) => { setIsLoading(true); + try { + await sendCompanyOfferEmail(data, language); + router.push('/company-offer/success'); + } catch (err) { + setIsLoading(false); + if (err instanceof Error) { + console.warn('Could not send company offer email: ' + err.message); + } + console.warn('Could not send company offer email: ', err); + } const formData = new FormData(); Object.entries(data).forEach(([key, value]) => { if (value !== undefined) formData.append(key, value); diff --git a/app/(public)/company-offer/_lib/server/company-offer-actions.ts b/app/(public)/company-offer/_lib/server/company-offer-actions.ts new file mode 100644 index 0000000..a66ab42 --- /dev/null +++ b/app/(public)/company-offer/_lib/server/company-offer-actions.ts @@ -0,0 +1,25 @@ +'use server'; + +import { renderCompanyOfferEmail } from '@/packages/email-templates/src'; + +import { sendEmailFromTemplate } from '~/lib/services/mailer.service'; +import { CompanySubmitData } from '~/lib/types/company'; + +export const sendCompanyOfferEmail = async ( + data: CompanySubmitData, + language: string, +) => { + const formData = new FormData(); + Object.entries(data).forEach(([key, value]) => { + if (value !== undefined) formData.append(key, value); + }); + + sendEmailFromTemplate( + renderCompanyOfferEmail, + { + companyData: data, + language, + }, + process.env.CONTACT_EMAIL!, + ); +}; diff --git a/app/api/after-mfa/route.ts b/app/api/after-mfa/route.ts new file mode 100644 index 0000000..1bbe946 --- /dev/null +++ b/app/api/after-mfa/route.ts @@ -0,0 +1,27 @@ +import { enhanceRouteHandler } from '@/packages/next/src/routes'; +import { createAuthCallbackService } from '@/packages/supabase/src/auth-callback.service'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; + +export const POST = enhanceRouteHandler( + async () => { + try { + const supabaseClient = getSupabaseServerClient(); + const { + data: { user }, + } = await supabaseClient.auth.getUser(); + const service = createAuthCallbackService(supabaseClient); + + if (user && service.isKeycloakUser(user)) { + await service.setupMedusaUserForKeycloak(user); + } + + return new Response(null, { status: 200 }); + } catch (err) { + console.error('Error on verifying:', { err }); + return new Response(null, { status: 500 }); + } + }, + { + auth: false, + }, +); diff --git a/app/api/job/handler/send-open-jobs-emails.ts b/app/api/job/handler/send-open-jobs-emails.ts index 9f09e12..44d436e 100644 --- a/app/api/job/handler/send-open-jobs-emails.ts +++ b/app/api/job/handler/send-open-jobs-emails.ts @@ -7,10 +7,18 @@ import { sendEmailFromTemplate } from '~/lib/services/mailer.service'; export default async function sendOpenJobsEmails() { const analysisResponseIds = await getOpenJobAnalysisResponseIds(); + if (analysisResponseIds.length === 0) { + return; + } + const doctorAccounts = await getDoctorAccounts(); - const doctorEmails: string[] = doctorAccounts + const doctorEmails = doctorAccounts .map(({ email }) => email) - .filter((email): email is string => !!email); + .filter((email) => !!email); + + if (doctorEmails !== null) { + return []; + } await sendEmailFromTemplate( renderNewJobsAvailableEmail, @@ -20,4 +28,6 @@ export default async function sendOpenJobsEmails() { }, doctorEmails, ); + + return doctorAccounts.filter((email) => !!email).map(({ id }) => id); } diff --git a/app/api/job/handler/sync-analysis-groups.ts b/app/api/job/handler/sync-analysis-groups.ts index c037378..515e91b 100644 --- a/app/api/job/handler/sync-analysis-groups.ts +++ b/app/api/job/handler/sync-analysis-groups.ts @@ -41,7 +41,7 @@ export default async function syncAnalysisGroups() { try { console.info('Getting latest public message id'); - const lastCheckedDate = await getLastCheckedDate(); + // const lastCheckedDate = await getLastCheckedDate(); never used? const latestMessage = await getLatestPublicMessageListItem(); if (!latestMessage) { diff --git a/app/api/job/handler/sync-analysis-results.ts b/app/api/job/handler/sync-analysis-results.ts index b631045..3be7639 100644 --- a/app/api/job/handler/sync-analysis-results.ts +++ b/app/api/job/handler/sync-analysis-results.ts @@ -1,3 +1,6 @@ +import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; + import { readPrivateMessageResponse } from '~/lib/services/medipost/medipostPrivateMessage.service'; type ProcessedMessage = { @@ -16,6 +19,8 @@ type GroupedResults = { export default async function syncAnalysisResults() { console.info('Syncing analysis results'); + const supabase = getSupabaseServerClient(); + const api = createUserAnalysesApi(supabase); const processedMessages: ProcessedMessage[] = []; const excludedMessageIds: string[] = []; @@ -25,6 +30,12 @@ export default async function syncAnalysisResults() { processedMessages.push(result as ProcessedMessage); } + await api.sendAnalysisResultsNotification({ + hasFullAnalysisResponse: result.hasFullAnalysisResponse, + hasPartialAnalysisResponse: result.hasAnalysisResponse, + analysisOrderId: result.analysisOrderId, + }); + if (!result.messageId) { console.info('No more messages to process'); break; diff --git a/app/api/job/handler/sync-connected-online.ts b/app/api/job/handler/sync-connected-online.ts index 80e89c9..2255479 100644 --- a/app/api/job/handler/sync-connected-online.ts +++ b/app/api/job/handler/sync-connected-online.ts @@ -81,21 +81,19 @@ export default async function syncConnectedOnline() { }); } - let clinics; - let services; - let serviceProviders; - let jobTitleTranslations; // Filter out "Dentas Demo OÜ" in prod or only sync "Dentas Demo OÜ" in any other environment const isDemoClinic = (clinicId: number) => isProd ? clinicId !== 2 : clinicId === 2; - clinics = responseData.Data.T_Lic.filter(({ ID }) => isDemoClinic(ID)); - services = responseData.Data.T_Service.filter(({ ClinicID }) => + const clinics = responseData.Data.T_Lic.filter(({ ID }) => + isDemoClinic(ID), + ); + const services = responseData.Data.T_Service.filter(({ ClinicID }) => isDemoClinic(ClinicID), ); - serviceProviders = responseData.Data.T_Doctor.filter(({ ClinicID }) => + const serviceProviders = responseData.Data.T_Doctor.filter(({ ClinicID }) => isDemoClinic(ClinicID), ); - jobTitleTranslations = createTranslationMap( + const jobTitleTranslations = createTranslationMap( responseData.Data.P_JobTitleTranslations.filter(({ ClinicID }) => isDemoClinic(ClinicID), ), diff --git a/app/api/job/medipost-retry-dispatch/route.ts b/app/api/job/medipost-retry-dispatch/route.ts index 1fe75a0..a191998 100644 --- a/app/api/job/medipost-retry-dispatch/route.ts +++ b/app/api/job/medipost-retry-dispatch/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; +import { getLogger } from '@/packages/shared/src/logger'; import { retrieveOrder } from '@lib/data/orders'; import { getMedipostDispatchTries } from '~/lib/services/audit.service'; @@ -10,13 +11,17 @@ import loadEnv from '../handler/load-env'; import validateApiKey from '../handler/validate-api-key'; export const POST = async (request: NextRequest) => { + const logger = await getLogger(); + const ctx = { + api: '/job/medipost-retry-dispatch', + }; loadEnv(); const { medusaOrderId } = await request.json(); try { validateApiKey(request); - } catch (e) { + } catch { return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); } @@ -36,15 +41,15 @@ export const POST = async (request: NextRequest) => { medusaOrder, }); await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); - console.info('Successfully sent order to medipost'); + logger.info(ctx, 'Successfully sent order to medipost'); return NextResponse.json( { message: 'Successfully sent order to medipost', }, { status: 200 }, ); - } catch (e) { - console.error('Error sending order to medipost', e); + } catch (error) { + logger.error({ ...ctx, error }, 'Error sending order to medipost'); return NextResponse.json( { message: 'Failed to send order to medipost', diff --git a/app/api/job/send-open-jobs-emails/route.ts b/app/api/job/send-open-jobs-emails/route.ts index 9bf4118..a18d3c6 100644 --- a/app/api/job/send-open-jobs-emails/route.ts +++ b/app/api/job/send-open-jobs-emails/route.ts @@ -14,18 +14,19 @@ export const POST = async (request: NextRequest) => { try { validateApiKey(request); - } catch (e) { + } catch { return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); } try { - await sendOpenJobsEmails(); + const doctors = await sendOpenJobsEmails(); console.info( - 'Successfully sent out open job notification emails to doctors.', + 'Successfully sent out open job notification emails to doctors', ); await createNotificationLog({ action: NotificationAction.DOCTOR_NEW_JOBS, status: 'SUCCESS', + comment: `doctors that received email: ${doctors}`, }); return NextResponse.json( { diff --git a/app/api/job/sync-analysis-groups-store/route.ts b/app/api/job/sync-analysis-groups-store/route.ts index be54509..1def4a9 100644 --- a/app/api/job/sync-analysis-groups-store/route.ts +++ b/app/api/job/sync-analysis-groups-store/route.ts @@ -9,7 +9,7 @@ export const POST = async (request: NextRequest) => { try { validateApiKey(request); - } catch (e) { + } catch { return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); } diff --git a/app/api/job/sync-analysis-groups/route.ts b/app/api/job/sync-analysis-groups/route.ts index 83cc6d3..cb06e1c 100644 --- a/app/api/job/sync-analysis-groups/route.ts +++ b/app/api/job/sync-analysis-groups/route.ts @@ -9,7 +9,7 @@ export const POST = async (request: NextRequest) => { try { validateApiKey(request); - } catch (e) { + } catch { return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); } diff --git a/app/api/job/sync-analysis-results/route.ts b/app/api/job/sync-analysis-results/route.ts index 392be6a..0f66fbc 100644 --- a/app/api/job/sync-analysis-results/route.ts +++ b/app/api/job/sync-analysis-results/route.ts @@ -9,7 +9,7 @@ export const POST = async (request: NextRequest) => { try { validateApiKey(request); - } catch (e) { + } catch { return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); } diff --git a/app/api/job/sync-connected-online/route.ts b/app/api/job/sync-connected-online/route.ts index 065c8ec..41fb295 100644 --- a/app/api/job/sync-connected-online/route.ts +++ b/app/api/job/sync-connected-online/route.ts @@ -9,7 +9,7 @@ export const POST = async (request: NextRequest) => { try { validateApiKey(request); - } catch (e) { + } catch { return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); } diff --git a/app/api/job/test-medipost-responses/route.ts b/app/api/job/test-medipost-responses/route.ts index fc1f2e1..0634efd 100644 --- a/app/api/job/test-medipost-responses/route.ts +++ b/app/api/job/test-medipost-responses/route.ts @@ -18,7 +18,7 @@ export async function POST(request: NextRequest) { try { validateApiKey(request); - } catch (e) { + } catch { return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); } diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index a16e023..d36c96a 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -47,6 +47,11 @@ export async function GET(request: NextRequest) { const service = createAuthCallbackService(getSupabaseServerClient()); const oauthResult = await service.exchangeCodeForSession(authCode); + + if (oauthResult.requiresMultiFactorAuthentication) { + redirect(pathsConfig.auth.verifyMfa); + } + if (!('isSuccess' in oauthResult)) { return redirectOnError(oauthResult.searchParams); } diff --git a/app/auth/membership-confirmation/_components/membership-confirmation-notification.tsx b/app/auth/membership-confirmation/_components/membership-confirmation-notification.tsx index 86b8079..1931d21 100644 --- a/app/auth/membership-confirmation/_components/membership-confirmation-notification.tsx +++ b/app/auth/membership-confirmation/_components/membership-confirmation-notification.tsx @@ -25,7 +25,7 @@ const MembershipConfirmationNotification: React.FC<{ descriptionKey="account:membershipConfirmation:successDescription" buttonProps={{ buttonTitleKey: 'account:membershipConfirmation:successButton', - href: pathsConfig.app.home, + href: pathsConfig.app.selectPackage, }} /> ); diff --git a/app/auth/membership-confirmation/layout.tsx b/app/auth/membership-confirmation/layout.tsx index e3d32c7..1360b0f 100644 --- a/app/auth/membership-confirmation/layout.tsx +++ b/app/auth/membership-confirmation/layout.tsx @@ -1,5 +1,3 @@ -import { withI18n } from '~/lib/i18n/with-i18n'; - async function SiteLayout(props: React.PropsWithChildren) { return (
{feedback?.value ?? '-'}
+ {!isReadOnly && ( + + + )} +{feedback?.value ?? '-'}
- {!isReadOnly && ( - - + {order.isPackage && ( +
-
+
+
+
+
{value}
+{t('common:close')}
@@ -106,11 +104,13 @@ const ComparePackagesModal = async ({+
{t('product:healthPackageComparison.description')}
-