mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-23 11:08:18 +03:00
feat(auth): sign in with an email address
Wires the pin-code provider, which existed and was never reachable, and makes it the only branch that can create an account. Steam now resolves an existing connection instead of minting a user from a persona, and refuses when there is no account behind it — which is an answer the interface renders rather than an implicit signup. Delivery is a small provider-neutral POST rather than a vendor SDK: configure an endpoint, a key and a from address. With none of them set it logs the code outside production so a local sign-in works, and throws in production, because a screen that says "check your email" when nothing was sent leaves someone waiting instead of telling anybody. A person who has only ever signed in by email has no connected account, and the token says so with an empty value — the same one a server-to-server caller has always carried.
This commit is contained in:
61
apps/auth/src/email.ts
Normal file
61
apps/auth/src/email.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Getting a pin code to a mailbox.
|
||||
*
|
||||
* Deliberately not tied to one mail vendor: it posts a small JSON body to
|
||||
* whatever endpoint is configured, so swapping providers is configuration and
|
||||
* not a code change. Three settings, all optional except in production —
|
||||
* `EMAIL_SEND_URL`, `EMAIL_API_KEY`, `EMAIL_FROM`.
|
||||
*/
|
||||
export interface MailerConfig {
|
||||
EMAIL_SEND_URL?: string;
|
||||
EMAIL_API_KEY?: string;
|
||||
EMAIL_FROM?: string;
|
||||
NODE_ENV?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the code, or fail loudly.
|
||||
*
|
||||
* With no mailer configured this logs the code and carries on, which is what
|
||||
* makes a local sign-in possible without a mail account. In production the
|
||||
* same situation throws instead: a signup screen that says "check your email"
|
||||
* when nothing was sent is worse than one that says it is broken, because the
|
||||
* person waits instead of telling anybody.
|
||||
*/
|
||||
export async function sendVerificationCode(
|
||||
config: MailerConfig,
|
||||
email: string,
|
||||
code: string
|
||||
): Promise<void> {
|
||||
const configured = config.EMAIL_SEND_URL && config.EMAIL_API_KEY && config.EMAIL_FROM;
|
||||
|
||||
if (!configured) {
|
||||
if (config.NODE_ENV === 'production') {
|
||||
throw new Error('Email delivery is not configured, so no sign-in code can be sent');
|
||||
}
|
||||
console.log(`[auth] sign-in code for ${email}: ${code}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(config.EMAIL_SEND_URL!, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: `Bearer ${config.EMAIL_API_KEY}`,
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: config.EMAIL_FROM,
|
||||
to: [email],
|
||||
subject: `${code} is your Nestri sign-in code`,
|
||||
text:
|
||||
`Your Nestri sign-in code is ${code}.\n\n` +
|
||||
`It expires shortly. If you did not ask to sign in, you can ignore this.`
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// The body is included because the useful part of a delivery failure is
|
||||
// always the provider's own message, and it is otherwise lost.
|
||||
throw new Error(`Sending the sign-in code failed: ${response.status} ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user