AWS SES preview
This commit is contained in:
@@ -2,11 +2,13 @@ import { betterAuth } from 'better-auth';
|
||||
import { kyselyAdapter } from '@better-auth/kysely-adapter';
|
||||
import { Kysely, PostgresDialect } from 'kysely';
|
||||
import { Pool } from 'pg';
|
||||
import { sendEmail, verificationEmailHtml, resetPasswordEmailHtml } from './email';
|
||||
|
||||
const db = new Kysely({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
connectionString: process.env.DATABASE_URL ||
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
'postgresql://labwise:labwise_dev_pw@localhost:5432/labwise_db',
|
||||
}),
|
||||
}),
|
||||
@@ -16,7 +18,35 @@ export const auth = betterAuth({
|
||||
database: kyselyAdapter(db, { type: 'postgres' }),
|
||||
|
||||
baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3001',
|
||||
secret: process.env.BETTER_AUTH_SECRET || 'dev-secret-change-in-production-min32chars!!',
|
||||
secret:
|
||||
process.env.BETTER_AUTH_SECRET ||
|
||||
'dev-secret-change-in-production-min32chars!!',
|
||||
|
||||
rateLimit: {
|
||||
enabled: false, // TODO: re-enable in production
|
||||
},
|
||||
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
sendResetPassword: async ({ user, url }) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Reset your LabWise password',
|
||||
html: resetPasswordEmailHtml(url),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async ({ user, url }) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Verify your LabWise email',
|
||||
html: verificationEmailHtml(url),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
socialProviders: {
|
||||
google: {
|
||||
|
||||
47
server/src/auth/email.ts
Normal file
47
server/src/auth/email.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
|
||||
|
||||
const ses = new SESClient({ region: process.env.AWS_REGION || 'us-east-1' });
|
||||
const FROM = process.env.FROM_EMAIL || 'noreply@labwise.wahwa.com';
|
||||
|
||||
export async function sendEmail({
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
}: {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
}) {
|
||||
await ses.send(
|
||||
new SendEmailCommand({
|
||||
Source: FROM,
|
||||
Destination: { ToAddresses: [to] },
|
||||
Message: {
|
||||
Subject: { Data: subject },
|
||||
Body: { Html: { Data: html } },
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function verificationEmailHtml(url: string) {
|
||||
return `
|
||||
<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px">
|
||||
<h2 style="color:#2d5a4a;margin-bottom:8px">Verify your email</h2>
|
||||
<p style="color:#555;margin-bottom:24px">Click the button below to verify your email address and activate your LabWise account.</p>
|
||||
<a href="${url}" style="display:inline-block;background:#5a9584;color:white;padding:12px 28px;border-radius:6px;text-decoration:none;font-weight:600">Verify Email</a>
|
||||
<p style="color:#aaa;font-size:12px;margin-top:32px">If you didn't create a LabWise account, you can ignore this email.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function resetPasswordEmailHtml(url: string) {
|
||||
return `
|
||||
<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px">
|
||||
<h2 style="color:#2d5a4a;margin-bottom:8px">Reset your password</h2>
|
||||
<p style="color:#555;margin-bottom:24px">Click the button below to reset your LabWise password. This link expires in 1 hour.</p>
|
||||
<a href="${url}" style="display:inline-block;background:#5a9584;color:white;padding:12px 28px;border-radius:6px;text-decoration:none;font-weight:600">Reset Password</a>
|
||||
<p style="color:#aaa;font-size:12px;margin-top:32px">If you didn't request this, you can ignore this email.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -108,3 +108,14 @@ CREATE TABLE IF NOT EXISTS protocols (
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS protocols_user_id_idx ON protocols(user_id);
|
||||
|
||||
-- User profile — lab defaults pre-filled on chemical entries
|
||||
CREATE TABLE IF NOT EXISTS user_profile (
|
||||
user_id TEXT NOT NULL PRIMARY KEY REFERENCES "user"("id") ON DELETE CASCADE,
|
||||
pi_first_name TEXT NOT NULL,
|
||||
bldg_code TEXT NOT NULL,
|
||||
lab TEXT NOT NULL,
|
||||
contact TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
@@ -6,10 +6,10 @@ import { auth } from './auth/auth';
|
||||
import { authRateLimiter, apiRateLimiter } from './auth/rateLimiter';
|
||||
import chemicalsRouter from './routes/chemicals';
|
||||
import protocolsRouter from './routes/protocols';
|
||||
import profileRouter from './routes/profile';
|
||||
import path from 'path';
|
||||
|
||||
const app = express();
|
||||
console.log(process.env.BETTER_AUTH_URL, process.env.BETTER_AUTH_SECRET);
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || path.join(__dirname, '../uploads');
|
||||
|
||||
@@ -38,11 +38,10 @@ app.use(express.json({ limit: '1mb' }));
|
||||
app.use('/api', apiRateLimiter);
|
||||
app.use('/api/chemicals', chemicalsRouter);
|
||||
app.use('/api/protocols', protocolsRouter);
|
||||
app.use('/api/profile', profileRouter);
|
||||
|
||||
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`LabWise API running on http://localhost:${PORT}`);
|
||||
console.log('BETTER_AUTH_URL:', process.env.BETTER_AUTH_URL);
|
||||
console.log('BETTER_AUTH_SECRET:', process.env.BETTER_AUTH_SECRET);
|
||||
});
|
||||
|
||||
46
server/src/routes/profile.ts
Normal file
46
server/src/routes/profile.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../auth/middleware';
|
||||
import { pool } from '../db/pool';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM user_profile WHERE user_id = $1',
|
||||
[req.user!.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Profile not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', requireAuth, async (req, res) => {
|
||||
const { pi_first_name, bldg_code, lab, contact } = req.body;
|
||||
if (!pi_first_name || !bldg_code || !lab) {
|
||||
return res.status(400).json({ error: 'pi_first_name, bldg_code, and lab are required' });
|
||||
}
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO user_profile (user_id, pi_first_name, bldg_code, lab, contact)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
pi_first_name = EXCLUDED.pi_first_name,
|
||||
bldg_code = EXCLUDED.bldg_code,
|
||||
lab = EXCLUDED.lab,
|
||||
contact = EXCLUDED.contact,
|
||||
updated_at = NOW()
|
||||
RETURNING *`,
|
||||
[req.user!.id, pi_first_name, bldg_code, lab, contact || null]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user