Skip to content

Commit ab93e64

Browse files
scheidtdavJerryVincentjona159
authored
Feat/mailer (#612)
* feat: add draft for port of user registration to resource route * feat: partly implement refresh token * docs: simplify contributing and add info about api routes and shared logic * feat(api): finalize user registration endpoint * fix(tests): get the tests to run be reconfiguring build steps * docs(db): readd db setup and seed scripts with README info for it * fix: wrong import of utils * refactor: remove leftover custom server stuff * fix(tests): add missing refresh token table * fix(tests): reenable remaining tests for registration * fix(ci): remove playwright and use correct node version * fix(ci): run the tests with a postgres container * feat(tests): add coverage report * fix(build): reorganize server modules to correctly split client/ server * fix(build): miss an import * fix(build): remove leftovers from custom server implementation * chore(deps): bump react-router dependencies * chore(deps): update react-router * feat/user me api (#559) * feat(api): add api routes for /users/me * fix(tests): api me PUT * feat(api): add delete me endpoint * feat(api): add root route (#560) * start * new commit * tested docs * added a route * Added API Docs * modified * removed unsupported packages * updated * Modified * script generation without using ts-node. * modified * fix: update package-lock.json * Updated (#575) * Updated README * Updated README * Removed duplicate Documentation section (#576) * Updated README * Updated README * Removed duplicate section. * Update README.md * Feat/api email and password (#561) * feat(api): add email-confirmation endpoint * feat(api): add request password reset * feat(api): add password reset * feat(api): implement resend email confirmation (without sending yet) * feat/api auth (#562) * feat(api): add email-confirmation endpoint * feat(api): add request password reset * feat(api): add password reset * feat(api): implement resend email confirmation (without sending yet) * feat(api): add sign-in, sign-out and refresh-routes to api * feat(api): implement refresh endpoint --------- Co-authored-by: jona159 <[email protected]> * feat(api): boxes for user endpoints (#573) * feat/api misc (#571) * feat(api): boxes for user endpoints * feat(api): add tags and stats route scaffold * feat(api): implement tags route * refactor: remove unnecessary imports * feat(api): implement statistics route --------- Co-authored-by: jona159 <[email protected]> * feat(api): add route and test files * feat: add test code * feat: add dummy sensors to devices and implement getting them back * feat: prefer dev server in no production envs and hide dev in prod * feat(docs): start adding docs to route * feat: finish up to the point where we need measurements * fix: api routes without need for measurements * fix: stats call * fix: remaining tests * fix: frontend issue from changing the service implementation * feat: add react mail * feat: add nodemailer and setup for sending mails * feat: send password reset mails * refactor: fix lint issues with mails * feat: implement email confirmation mail * feat: add mail for new users * fix: rewrite package lock * fix: rewrite package lock * fix: packages * feat: add email to new devices being created * feat: send mail when email requested to change * feat: set email language properly * feat: implement base mail for all devices and migrate device specifics * fix: move ts-ignore * fix: use example vars for test * feat: use ethereal.email for testing * fix: remove conflicting test * fix: rewrite package-lock.json * refactor: remove unused comment * refactor: remove mailhog platform * feat: remove [email protected] and matthias mail address --------- Co-authored-by: JerryVincent <[email protected]> Co-authored-by: Jerry Vincent <[email protected]> Co-authored-by: jona159 <[email protected]>
1 parent 6c4ced0 commit ab93e64

21 files changed

+11944
-4856
lines changed

.env.example

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,10 @@ MYBADGES_SERVERADMIN_USERNAME = ""
2424
MYBADGES_SERVERADMIN_PASSWORD = ""
2525
MYBADGES_ISSUERID_OSEM = ""
2626
MYBADGES_CLIENT_ID = ""
27-
MYBADGES_CLIENT_SECRET = ""
27+
MYBADGES_CLIENT_SECRET = ""
28+
29+
SMTP_HOST = "localhost"
30+
SMTP_PORT = "1025"
31+
SMTP_SECURE = "false"
32+
SMTP_USERNAME = "ignored"
33+
SMTP_PASSWORD = "ignored"

app/components/device-detail/device-detail-box.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ export default function DeviceDetailBox() {
107107

108108
const [sensors, setSensors] = useState<SensorWithLatestMeasurement[]>();
109109
useEffect(() => {
110-
const sortedSensors = [...data.sensors as any].sort(
111-
(a, b) => (a.id as unknown as number) - (b.id as unknown as number),
110+
const sortedSensors = [...(data.sensors as any)].sort(
111+
(a, b) => (a.id as unknown as number) - (b.id as unknown as number)
112112
);
113113
setSensors(sortedSensors);
114114
}, [data]);
@@ -373,13 +373,13 @@ export default function DeviceDetailBox() {
373373
?.split(",")
374374
.includes(tag)
375375
? "bg-green-100 dark:bg-dark-green"
376-
: "",
376+
: ""
377377
)}
378378
onClick={(event) => {
379379
event.stopPropagation();
380380

381381
const currentParams = new URLSearchParams(
382-
searchParams.toString(),
382+
searchParams.toString()
383383
);
384384

385385
// Safely retrieve and parse the current tags
@@ -395,7 +395,7 @@ export default function DeviceDetailBox() {
395395
if (updatedTags.length > 0) {
396396
currentParams.set(
397397
"tags",
398-
updatedTags.join(","),
398+
updatedTags.join(",")
399399
);
400400
} else {
401401
currentParams.delete("tags");
@@ -494,7 +494,7 @@ export default function DeviceDetailBox() {
494494
id={sensor.id}
495495
value={sensor.id}
496496
defaultChecked={sensorIds.has(
497-
sensor.id,
497+
sensor.id
498498
)}
499499
/>
500500
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
@@ -560,7 +560,7 @@ export default function DeviceDetailBox() {
560560
id={sensor.id}
561561
value={sensor.id}
562562
defaultChecked={sensorIds.has(
563-
sensor.id,
563+
sensor.id
564564
)}
565565
/>
566566
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
@@ -603,7 +603,7 @@ export default function DeviceDetailBox() {
603603
</Card>
604604
</Link>
605605
);
606-
},
606+
}
607607
)}
608608
</div>
609609
</div>

app/lib/mail.server.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { render } from '@react-email/components'
2+
import * as dotenv from 'dotenv'
3+
import nodemailer from 'nodemailer'
4+
import type SMTPTransport from 'nodemailer/lib/smtp-transport'
5+
dotenv.config()
6+
7+
/**
8+
* Interface for our configuration.
9+
* Note these variables can possibly be undefined,
10+
* as varibales may be skipped or there is no .env file at all
11+
*/
12+
interface Config {
13+
SMTP_HOST: string | undefined
14+
SMTP_PORT: number | undefined
15+
SMTP_SECURE: boolean | undefined
16+
SMTP_USERNAME: string | undefined
17+
SMTP_PASSWORD: string | undefined
18+
}
19+
20+
/**
21+
* Processes environment variables and
22+
* builds a configuration object from it.
23+
* @returns {Config} A mailer configuration
24+
*/
25+
const getConfig = (): Config => {
26+
const config = {
27+
SMTP_HOST: process.env.SMTP_HOST,
28+
SMTP_PORT: process.env.SMTP_PORT
29+
? Number(process.env.SMTP_PORT)
30+
: undefined,
31+
SMTP_SECURE: process.env.SMTP_SECURE
32+
? Boolean(JSON.parse(process.env.SMTP_SECURE))
33+
: undefined,
34+
SMTP_USERNAME: process.env.SMTP_USERNAME,
35+
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
36+
}
37+
38+
// check the config for missing entries and throw an error if necessary
39+
for (const [key, value] of Object.entries(config)) {
40+
if (value === undefined) {
41+
throw new Error(`Missing key ${key} in config.env`)
42+
}
43+
}
44+
45+
return config
46+
}
47+
48+
const config = getConfig()
49+
50+
class OSEMTransporter {
51+
private static _instance: nodemailer.Transporter | null = null
52+
private constructor() {}
53+
public static async getInstance(): Promise<nodemailer.Transporter> {
54+
if (this._instance !== null) return this._instance
55+
56+
if (process.env.TEST) {
57+
return await new Promise((resolve, reject) => {
58+
nodemailer.createTestAccount((err, account) => {
59+
if (err) reject(err)
60+
else {
61+
this._instance = nodemailer.createTransport({
62+
host: 'smtp.ethereal.email',
63+
port: 587,
64+
secure: false,
65+
auth: {
66+
user: account.user,
67+
pass: account.pass,
68+
},
69+
})
70+
resolve(this._instance)
71+
}
72+
})
73+
})
74+
} else {
75+
const transportOptions: SMTPTransport.Options = {
76+
host: config.SMTP_HOST,
77+
port: config.SMTP_PORT,
78+
secure: config.SMTP_SECURE,
79+
}
80+
if (
81+
config.SMTP_USERNAME !== 'ignored' &&
82+
config.SMTP_PASSWORD !== 'ignored'
83+
) {
84+
transportOptions['auth'] = {
85+
user: config.SMTP_USERNAME,
86+
pass: config.SMTP_PASSWORD,
87+
}
88+
}
89+
this._instance = nodemailer.createTransport(transportOptions)
90+
return this._instance
91+
}
92+
}
93+
}
94+
void OSEMTransporter.getInstance() // eagerly initialize the transporter
95+
96+
export interface MailAttachment {
97+
filename: string
98+
content: string
99+
}
100+
101+
export const sendMail = async (mailConfig: {
102+
recipientAddress: string
103+
recipientName?: string
104+
subject?: string
105+
body: React.ReactElement
106+
attachments?: MailAttachment[]
107+
}) => {
108+
try {
109+
const mailHtml = await render(mailConfig.body)
110+
111+
await (
112+
await OSEMTransporter.getInstance()
113+
).sendMail({
114+
from: '"openSenseMap 🌍" <[email protected]>',
115+
to: mailConfig.recipientName
116+
? `"${mailConfig.recipientName}" <${mailConfig.recipientAddress}>`
117+
: mailConfig.recipientAddress,
118+
subject: mailConfig.subject ?? 'openSenseMap',
119+
html: mailHtml,
120+
attachments: mailConfig.attachments,
121+
})
122+
} catch (err) {
123+
console.error(err)
124+
throw err
125+
}
126+
}

0 commit comments

Comments
 (0)