Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions client/packages/cli/src/commands/auth/email/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { HttpClientResponse, Path } from '@effect/platform';
import { Effect, Schema } from 'effect';
import { ProjectInfo } from '../../../context/projectInfo.ts';
import { BadArgsError } from '../../../errors.ts';
import { readLocalEmailFile } from '../../../old.js';
import { UI } from '../../../ui/index.ts';
import { getEmailPathToWrite } from '../../../util/findConfigCandidates.ts';
import { writeTypescript } from '../../../lib/pullSchema.ts';
import { promptOk } from '../../../lib/ui.ts';
import { getEmailTemplateStatus, type EmailTemplateInfo } from './status.ts';
import type { authEmailPullDef, OptsFromCommand } from '../../../index.ts';
import { InstantHttp } from '../../../lib/http.ts';
import { getAppName } from '../../../util/getAppName.ts';
import type { EmailConfig } from '../../../lib/email.ts';

export const authEmailPullCmd = Effect.fn(function* (
opts: OptsFromCommand<typeof authEmailPullDef>,
) {
yield* pullEmail(opts.file);
});

export type EmailConfig = (typeof EmailConfig.Type)['authEmail'];

type WriteEmailTemplateOpts = {
emailPath?: string;
confirmOverwrite?: boolean;
};

const pullEmail = (emailPath?: string) =>
Effect.gen(function* () {
yield* Effect.log('Pulling email template...');

const info = yield* getEmailTemplateStatus;

const emailConfig = info
? infoToEmailConfig(info)
: yield* getDefaultEmailTemplate;

yield* writeEmailTemplate(emailConfig, { emailPath });
});

export const writeEmailTemplate = (
emailConfig: EmailConfig,
{ emailPath, confirmOverwrite = true }: WriteEmailTemplateOpts = {},
) =>
Effect.gen(function* () {
const prevEmailFile = yield* Effect.tryPromise({
try: () => readLocalEmailFile(emailPath),
catch: (e) =>
BadArgsError.make({
message: `Error reading local email file: ${e}`,
}),
});

const shortEmailPath =
prevEmailFile?.path ?? emailPath ?? getEmailPathToWrite();

if (prevEmailFile && confirmOverwrite) {
const shouldContinue = yield* promptOk({
promptText: `This will overwrite your local ${shortEmailPath} file, OK to proceed?`,
modifyOutput: UI.modifiers.yPadding,
inline: true,
});
if (!shouldContinue) {
yield* Effect.log('Cancelled email pull');
return;
}
}

const path = yield* Path.Path;
const { pkgDir } = yield* ProjectInfo;
const fullEmailPath = shortEmailPath.startsWith('/')
? shortEmailPath
: path.join(pkgDir, shortEmailPath);

const typescriptFile = yield* generateEmailTypescriptFile(emailConfig);

yield* writeTypescript(fullEmailPath, typescriptFile);
yield* Effect.log('Wrote email template to ' + shortEmailPath);
});

const DefaultEmailTemplateSchema = Schema.Struct({
subject: Schema.String,
body: Schema.String,
'sender-email': Schema.String.pipe(Schema.optional),
});

export const getDefaultEmailTemplate = Effect.gen(function* () {
const http = yield* InstantHttp;
const template = yield* http
.get('/dash/default-email-template')
.pipe(
Effect.flatMap(
HttpClientResponse.schemaBodyJson(DefaultEmailTemplateSchema),
),
);

const appName = yield* getAppName;

return {
subject: template.subject,
senderName: appName,
senderEmail: template['sender-email'],
body: template.body,
};
});

const infoToEmailConfig = (info: EmailTemplateInfo) => ({
subject: info.subject,
senderName: info.name,
senderEmail: info.email,
body: info.body,
});

const generateEmailTypescriptFile = Effect.fn(function* (
emailConfig: EmailConfig,
) {
const senderEmail = emailConfig.senderEmail
? JSON.stringify(emailConfig.senderEmail)
: 'undefined';

return `
// We provide a few dynamic variables for you to use in your email:
// {code}, the magic code e.g. 123456
// {app_title}, your app's title, i.e. test-fresh
// {user_email}, the user's email address, e.g. happyuser@gmail.com
// {expiration}, the magic code expiration, e.g. 10 minutes
// Note: {code} is required in both the subject and body.
const email = {
authEmail: {
subject: ${JSON.stringify(emailConfig.subject)},
senderName: ${JSON.stringify(emailConfig.senderName)},
senderEmail: ${senderEmail},
body: ${toTemplateLiteral(emailConfig.body)},
},
};

export default email;
`;
});

const toTemplateLiteral = (value: string) =>
`\`${value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\``;
85 changes: 85 additions & 0 deletions client/packages/cli/src/commands/auth/email/push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Effect, Schema } from 'effect';
import type { authEmailPushDef, OptsFromCommand } from '../../../index.ts';
import {
getVerification,
readEmailConfig,
sendSenderVerification,
} from '../../../lib/email.ts';
import { CurrentApp } from '../../../context/currentApp.ts';
import { InstantHttpAuthed, withCommand } from '../../../lib/http.ts';
import {
HttpClient,
HttpClientRequest,
HttpClientResponse,
} from '@effect/platform';
import boxen from 'boxen';
import {
formatSenderVerificationDnsRecords,
getEmailTemplateStatus,
} from './status.ts';
import chalk from 'chalk';

export const authEmailPushCmd = Effect.fn(function* (
opts: OptsFromCommand<typeof authEmailPushDef>,
) {
const emailConfig = yield* readEmailConfig(opts.file);
const { appId } = yield* CurrentApp;
const http = yield* InstantHttpAuthed;
const authEmail = emailConfig.authEmail;
const senderEmail = authEmail.senderEmail;

yield* http
.pipe(
withCommand('auth email push'),
HttpClient.mapRequestInputEffect(
HttpClientRequest.bodyJson({
'email-type': 'magic-code',
subject: authEmail.subject,
body: authEmail.body,
'sender-email': senderEmail,
'sender-name': authEmail.senderName,
}),
),
)
.post(`/dash/apps/${appId}/email_templates`)
.pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Any)));

const info = yield* getEmailTemplateStatus;

yield* Effect.log(
[
chalk.green('Email template saved!'),
'',
chalk.bold('Pushed fields:'),
` Email type: ${chalk.cyan('magic-code')}`,
` Subject: ${chalk.cyan(authEmail.subject)}`,
` Sender name: ${chalk.cyan(authEmail.senderName)}`,
` Sender email: ${chalk.cyan(senderEmail || '(default)')}`,
` Body: ${chalk.cyan(`${authEmail.body.length} characters`)}`,
].join('\n'),
);

// Check if verification email needs to be sent

if (info?.verification_verified === false) {
yield* sendSenderVerification;
yield* Effect.log(
boxen(
"We've sent a confirmation email containing a six digit code to verify the sender email address.\nUse instant-cli auth email verify <code> to complete verification.",
{
borderColor: 'yellow',
padding: { right: 1, left: 1 },
},
),
);
}

if (emailConfig.authEmail.senderEmail) {
const verification = yield* getVerification;
if (verification.verification) {
yield* Effect.log(
formatSenderVerificationDnsRecords(verification.verification),
);
}
}
});
8 changes: 8 additions & 0 deletions client/packages/cli/src/commands/auth/email/resend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Effect } from 'effect';
import { sendSenderVerification } from '../../../lib/email.ts';

export const resendEmailCmd = Effect.gen(function* () {
yield* sendSenderVerification;

yield* Effect.log('Verification email re-sent!');
});
37 changes: 37 additions & 0 deletions client/packages/cli/src/commands/auth/email/reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { HttpClientRequest, HttpClientResponse } from '@effect/platform';
import { Effect, Schema } from 'effect';
import { CurrentApp } from '../../../context/currentApp.ts';
import { InstantHttpAuthed, withCommand } from '../../../lib/http.ts';
import { getDefaultEmailTemplate, writeEmailTemplate } from './pull.ts';
import { getEmailTemplateStatus } from './status.ts';

export const authEmailResetCmd = Effect.fn(function* () {
const { appId } = yield* CurrentApp;
const http = (yield* InstantHttpAuthed).pipe(withCommand('auth email reset'));

const status = yield* getEmailTemplateStatus;

if (!status) {
yield* Effect.log(
'No email template configured. Resetting local email template.',
);
} else {
yield* http
.execute(
HttpClientRequest.del(
`/dash/apps/${appId}/email_templates/${status.id}`,
),
)
.pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Any)));
}

const defaultConfig = yield* getDefaultEmailTemplate;

yield* writeEmailTemplate(defaultConfig, {
confirmOverwrite: false,
});

yield* Effect.log(
'instant.email.ts file reset to default. To apply this change, run instant-cli auth email push',
);
});
Loading
Loading