Skip to content

Commit 8f74b16

Browse files
committed
Add senderEmail and senderName to Organisation schema, refactor how org data is loaded
1 parent e189fc3 commit 8f74b16

19 files changed

+161
-197
lines changed

app/lib/auth.server.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { mailjetSend } from "./email.server";
88
import { prisma, throwErrorResponse } from "./prisma.server";
99
import { requireAccessToProgram } from "./program.server";
1010
import { createUser } from "./user.server";
11+
import { getOrg } from "./organisation.server";
1112

1213
const sessionSecret = process.env.SESSION_SECRET;
1314
if (!sessionSecret) {
@@ -248,6 +249,7 @@ export async function logout(request: Request) {
248249
}
249250

250251
export async function sendPasswordResetLink(user: User) {
252+
const org = await getOrg();
251253
const reset = await prisma.userPasswordReset
252254
.upsert({
253255
where: {
@@ -279,8 +281,12 @@ export async function sendPasswordResetLink(user: User) {
279281
Messages: [
280282
{
281283
From: {
282-
Email: "registrations@certificates.unternehmertum.de",
283-
Name: "UnternehmerTUM Certificates",
284+
Email:
285+
org.senderEmail ??
286+
"email-not-configured@example.com",
287+
Name:
288+
org.senderName ??
289+
"Please configure in organisation settings",
284290
},
285291
To: [
286292
{
@@ -289,8 +295,8 @@ export async function sendPasswordResetLink(user: User) {
289295
},
290296
],
291297
Subject: `Reset your password`,
292-
TextPart: `Dear ${user.firstName} ${user.lastName},\n\nTo reset your password for UnternehmerTUM Certificates, please click on the following link:\n${resetUrl}\n\nIf you haven't requested this password reset, please ignore or report this email.\n\nThank you!`,
293-
HTMLPart: `<p>Dear ${user.firstName} ${user.lastName},</p><p>To reset your password for UnternehmerTUM Certificates, please click on the following link:<br /><a href="${resetUrl}">${resetUrl}</a></p><p>If you haven't requested this password reset, please ignore or report this email.</p><p>Thank you!</p>`,
298+
TextPart: `Dear ${user.firstName} ${user.lastName},\n\nTo reset your password for ${org.name} Certificates, please click on the following link:\n${resetUrl}\n\nIf you haven't requested this password reset, please ignore or report this email.\n\nThank you!`,
299+
HTMLPart: `<p>Dear ${user.firstName} ${user.lastName},</p><p>To reset your password for ${org.name} Certificates, please click on the following link:<br /><a href="${resetUrl}">${resetUrl}</a></p><p>If you haven't requested this password reset, please ignore or report this email.</p><p>Thank you!</p>`,
294300
},
295301
],
296302
}).catch((/*error*/) => {

app/lib/organisation.server.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { Organisation, Prisma } from "@prisma/client";
2+
import { prisma } from "./prisma.server";
3+
4+
export type PublicOrganisation = Prisma.OrganisationGetPayload<{
5+
select: {
6+
name: true;
7+
imprintUrl: true;
8+
privacyUrl: true;
9+
};
10+
}>;
11+
12+
let org: Organisation | null = null;
13+
let publicOrg: PublicOrganisation | null = null;
14+
15+
export async function getOrg(): Promise<Organisation> {
16+
if (org === null) {
17+
// not cached yet
18+
19+
org = await prisma.organisation.findUnique({
20+
where: {
21+
id: 1,
22+
},
23+
});
24+
25+
if (org === null) {
26+
// not found in database
27+
org = {
28+
id: 1,
29+
name: "(Configure Organisation)",
30+
imprintUrl: null,
31+
privacyUrl: null,
32+
senderEmail: null,
33+
senderName: null,
34+
};
35+
}
36+
}
37+
38+
return org;
39+
}
40+
41+
export async function getPublicOrg(): Promise<PublicOrganisation> {
42+
if (publicOrg === null) {
43+
// not cached yet
44+
45+
publicOrg = await prisma.organisation.findUnique({
46+
where: {
47+
id: 1,
48+
},
49+
select: {
50+
name: true,
51+
imprintUrl: true,
52+
privacyUrl: true,
53+
},
54+
});
55+
56+
if (publicOrg === null) {
57+
// not found in database
58+
publicOrg = {
59+
name: "(Configure Organisation)",
60+
imprintUrl: null,
61+
privacyUrl: null,
62+
};
63+
}
64+
}
65+
66+
return publicOrg;
67+
}

app/lib/user.server.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { domain } from "./config.server";
1515
import { mailjetSend } from "./email.server";
1616
import { ensureFolderExists, readFileIfExists } from "./fs.server";
1717
import { prisma, throwErrorResponse } from "./prisma.server";
18+
import { getOrg } from "./organisation.server";
1819

1920
const __dirname = fileURLToPath(new URL(".", import.meta.url));
2021
const userPhotoDir = resolve(__dirname, "../../storage/user/photos");
@@ -32,6 +33,7 @@ export const createUser = async (user: RegisterForm) => {
3233
verifyCode,
3334
},
3435
});
36+
3537
await sendVerificationEmail(newUser);
3638
return { id: newUser.id, email: emailLowerCase };
3739
};
@@ -76,6 +78,7 @@ export const createUserInvitation = async (
7678
};
7779

7880
export const sendVerificationEmail = async (user: User) => {
81+
const org = await getOrg();
7982
// @todo test for user-enumeration vulnerability
8083
const verificationUrl = `${domain}/user/verify/${user.id}/${user.verifyCode}`;
8184

@@ -84,8 +87,8 @@ export const sendVerificationEmail = async (user: User) => {
8487
Messages: [
8588
{
8689
From: {
87-
Email: "registrations@certificates.unternehmertum.de",
88-
Name: "UnternehmerTUM Certificates",
90+
Email: org.senderEmail ?? "email-not-configured@example.com",
91+
Name: org.senderName ?? "Please configure in organisation settings",
8992
},
9093
To: [
9194
{
@@ -94,8 +97,8 @@ export const sendVerificationEmail = async (user: User) => {
9497
},
9598
],
9699
Subject: `Please verify your email`,
97-
TextPart: `Dear ${user.firstName} ${user.lastName},\n\nTo complete your sign up for UnternehmerTUM Certificates, please click on the following link:\n${verificationUrl}\n\nIf you haven't signed up yourself, please ignore or report this email.\n\nThank you!`,
98-
HTMLPart: `<p>Dear ${user.firstName} ${user.lastName},</p><p>To complete your sign up for UnternehmerTUM Certificates, please click on the following link:<br /><a href="${verificationUrl}">${verificationUrl}</a></p><p>If you haven't signed up yourself, please ignore or report this email.</p><p>Thank you!</p>`,
100+
TextPart: `Dear ${user.firstName} ${user.lastName},\n\nTo complete your sign up for ${org.name} Certificates, please click on the following link:\n${verificationUrl}\n\nIf you haven't signed up yourself, please ignore or report this email.\n\nThank you!`,
101+
HTMLPart: `<p>Dear ${user.firstName} ${user.lastName},</p><p>To complete your sign up for ${org.name} Certificates, please click on the following link:<br /><a href="${verificationUrl}">${verificationUrl}</a></p><p>If you haven't signed up yourself, please ignore or report this email.</p><p>Thank you!</p>`,
99102
},
100103
],
101104
}).catch((error) => {
@@ -112,34 +115,39 @@ export const sendInvitationEmail = async (
112115
invite: UserInvitation,
113116
from: UserInvitationSender | null,
114117
) => {
118+
const org = await getOrg();
115119
// @todo dynamic org name from database
116120
const acceptUrl = `${domain}/user/accept-invite/${invite.id}/${invite.verifyCode}`;
117121

118122
const text = `Dear ${invite.firstName} ${invite.lastName},\n\n${
119123
from
120124
? `${from.firstName} ${from.lastName} is inviting you`
121125
: "You have been invited"
122-
} to become an admiminstrator for the UnternehmerTUM certificates tool.\n\nTo accept the invitation, please click on the following link:\n${acceptUrl}\n\nThank you!`;
126+
} to become an admiminstrator for the ${
127+
org.name
128+
} certificates tool.\n\nTo accept the invitation, please click on the following link:\n${acceptUrl}\n\nThank you!`;
123129
const html = `<p>Dear ${invite.firstName} ${invite.lastName},</p><p>${
124130
from
125131
? `${from.firstName} ${from.lastName} is inviting you`
126132
: "You have been invited"
127-
} to become an admiminstrator for the UnternehmerTUM certificates tool.</p><p>To accept the invitation, please click on the following link:<br /><a href="${acceptUrl}">${acceptUrl}</a></p><p>Thank you!</p>`;
133+
} to become an admiminstrator for the ${
134+
org.name
135+
} certificates tool.</p><p>To accept the invitation, please click on the following link:<br /><a href="${acceptUrl}">${acceptUrl}</a></p><p>Thank you!</p>`;
128136

129137
await mailjetSend({
130138
Messages: [
131139
{
132140
From: {
133-
Email: "invitation@certificates.unternehmertum.de",
134-
Name: "UnternehmerTUM Certificates",
141+
Email: org.senderEmail ?? "email-not-configured@example.com",
142+
Name: org.senderName ?? "Please configure in organisation settings",
135143
},
136144
To: [
137145
{
138146
Email: invite.email,
139147
Name: `${invite.firstName} ${invite.lastName}`,
140148
},
141149
],
142-
Subject: `You have been invited to UnternehmerTUM Certificates`,
150+
Subject: `You have been invited to ${org.name} Certificates`,
143151
TextPart: text,
144152
HTMLPart: html,
145153
},
@@ -165,7 +173,7 @@ export async function saveTransparentPhotoUpload(
165173

166174
const filepath = `${userPhotoDir}/${userPhoto.id}.transparent.png`;
167175
await lazyWriteFile(filepath, photo);
168-
return await lazyOpenFile(filepath);
176+
return lazyOpenFile(filepath);
169177
}
170178

171179
export async function readPhoto(userPhoto: UserPhoto) {

app/routes/_index.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121

2222
import { requireUserId, getUser, logout } from "~/lib/auth.server";
2323
import { prisma } from "~/lib/prisma.server";
24+
import { getPublicOrg } from "~/lib/organisation.server";
2425

2526
export function meta({ data }: Route.MetaArgs) {
2627
return [
@@ -40,28 +41,10 @@ export async function loader({ request }: Route.LoaderArgs) {
4041

4142
if (!user) {
4243
return await logout(request);
43-
/* return new Response(null, {
44-
status: 500,
45-
statusText: "Error while retrieving user information.",
46-
}); */
4744
}
4845

4946
// @todo parallelize DB requests instead of awaiting each one
50-
51-
let org = await prisma.organisation.findUnique({
52-
where: {
53-
id: 1,
54-
},
55-
});
56-
57-
if (!org) {
58-
org = {
59-
id: 1,
60-
name: "Unknown Organisation",
61-
imprintUrl: null,
62-
privacyUrl: null,
63-
};
64-
}
47+
const org = await getPublicOrg();
6548

6649
const certificates = await prisma.certificate.findMany({
6750
where: {

app/routes/cert.$certUuid.notify.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import slug from "slug";
55
import { requireAdmin } from "~/lib/auth.server";
66
import { domain } from "~/lib/config.server";
77
import { mailjetSend } from "~/lib/email.server";
8+
import { getOrg } from "~/lib/organisation.server";
89
import { generateCertificate } from "~/lib/pdf.server";
910
import { prisma } from "~/lib/prisma.server";
1011

@@ -34,18 +35,7 @@ export async function action({ request, params }: Route.ActionArgs) {
3435
});
3536
}
3637

37-
const org = await prisma.organisation.findUnique({
38-
where: {
39-
id: 1,
40-
},
41-
});
42-
43-
if (!org) {
44-
throw new Response(null, {
45-
status: 500,
46-
statusText: "Missing organisation",
47-
});
48-
}
38+
const org = await getOrg();
4939

5040
const social = await prisma.socialPreview.findUnique({
5141
where: {
@@ -110,8 +100,8 @@ export async function action({ request, params }: Route.ActionArgs) {
110100
// @ts-expect-error CustomId is missing from the Message type
111101
CustomId: certificate.uuid,
112102
From: {
113-
Email: "notifications@certificates.unternehmertum.de",
114-
Name: `${org.name} Certificates`,
103+
Email: org.senderEmail ?? "email-not-configured@example.com",
104+
Name: org.senderName ?? "Please configure in organisation settings",
115105
},
116106
To: [
117107
{

app/routes/org.settings.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Input } from "~/components/ui/input";
1515
import { Label } from "~/components/ui/label";
1616

1717
import { requireSuperAdmin } from "~/lib/auth.server";
18+
import { getOrg } from "~/lib/organisation.server";
1819
import { prisma } from "~/lib/prisma.server";
1920

2021
export function meta() {
@@ -44,20 +45,7 @@ export async function action({ request }: Route.ActionArgs) {
4445

4546
export async function loader({ request }: Route.LoaderArgs) {
4647
await requireSuperAdmin(request);
47-
48-
const org = await prisma.organisation.findUnique({
49-
where: {
50-
id: 1,
51-
},
52-
});
53-
54-
if (!org) {
55-
throw new Response(null, {
56-
status: 404,
57-
statusText: "Not Found",
58-
});
59-
}
60-
48+
const org = await getOrg();
6149
return { org };
6250
}
6351

app/routes/org.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { TooltipProvider } from "~/components/ui/tooltip";
1515
import { requireAdmin, getUser } from "~/lib/auth.server";
1616
import { getProgramsByAdmin } from "~/lib/program.server";
1717
import { prisma } from "~/lib/prisma.server";
18+
import { getOrg } from "~/lib/organisation.server";
1819

1920
export function meta({ data }: Route.MetaArgs) {
2021
return [{ title: `${data?.org?.name} Certificates` }];
@@ -24,20 +25,7 @@ export async function loader({ request, params }: Route.LoaderArgs) {
2425
const adminId = await requireAdmin(request);
2526
const user = await getUser(request);
2627

27-
let org = await prisma.organisation.findUnique({
28-
where: {
29-
id: 1,
30-
},
31-
});
32-
33-
if (!org) {
34-
org = {
35-
id: 1,
36-
name: "Unknown Organisation",
37-
imprintUrl: null,
38-
privacyUrl: null,
39-
};
40-
}
28+
const org = await getOrg();
4129

4230
const programs = await getProgramsByAdmin(adminId);
4331

app/routes/user.accept-invite.$inviteId.$code.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424

2525
import { createUserSessionAndRedirect } from "~/lib/auth.server";
2626
import { prisma } from "~/lib/prisma.server";
27+
import { getPublicOrg } from "~/lib/organisation.server";
2728

2829
export async function action({ request, params }: Route.ActionArgs) {
2930
if (params.inviteId && params.code) {
@@ -123,12 +124,7 @@ export async function loader({ params }: Route.LoaderArgs) {
123124
});
124125
}
125126

126-
const org = await prisma.organisation.findUnique({
127-
where: {
128-
id: 1,
129-
},
130-
});
131-
127+
const org = await getPublicOrg();
132128
return { invite, org };
133129
}
134130

app/routes/user.forgot-password.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
CardTitle,
1212
} from "~/components/ui/card";
1313
import { getUser, sendPasswordResetLink } from "~/lib/auth.server";
14+
import { getPublicOrg } from "~/lib/organisation.server";
1415
import { prisma } from "~/lib/prisma.server";
1516
import { validateEmail } from "~/lib/validators.server";
1617

@@ -65,15 +66,7 @@ export async function loader({ request }: Route.LoaderArgs) {
6566
const user = await getUser(request);
6667
if (user) return redirect("/");
6768

68-
const org = await prisma.organisation.findUnique({
69-
where: { id: 1 },
70-
select: {
71-
name: true,
72-
imprintUrl: true,
73-
privacyUrl: true,
74-
},
75-
});
76-
69+
const org = await getPublicOrg();
7770
return { org };
7871
}
7972

0 commit comments

Comments
 (0)