Skip to content

Commit

Permalink
✨ admin: add logging (Closes #161)
Browse files Browse the repository at this point in the history
  • Loading branch information
gwennlbh committed Apr 23, 2023
1 parent 2d1fa34 commit 84866a5
Show file tree
Hide file tree
Showing 26 changed files with 421 additions and 11 deletions.
5 changes: 5 additions & 0 deletions mail-templates/plain.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<mj-section>
<mj-column>
<mj-text>{{{text}}}</mj-text>
</mj-column>
</mj-section>
16 changes: 16 additions & 0 deletions prisma/migrations/20230423194845_add_logging/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE `Log` (
`id` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`action` VARCHAR(191) NOT NULL DEFAULT 'misc',
`level` INTEGER NOT NULL,
`message` LONGTEXT NOT NULL,
`ip` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NULL,

UNIQUE INDEX `Log_id_key`(`id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `Log` ADD CONSTRAINT `Log_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
12 changes: 12 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ model User {
emailValidations EmailValidation[]
Report Report[]
PasswordReset PasswordReset[]
Log Log[]
@@map("user")
}
Expand Down Expand Up @@ -207,3 +208,14 @@ enum PublicTransportType {
telepherique
tad
}

model Log {
id String @id @unique @default(cuid())
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
action String @default("misc")
level Int
message String @db.LongText
ip String
userId String?
}
121 changes: 121 additions & 0 deletions src/lib/server/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { User } from '@prisma/client';
import { prisma } from './prisma';
import { sendMail } from './mail';
export type LogAction =
| 'send_mail'
| 'submit_appartment'
| 'submit_appartment_edit'
| 'approve_appartment_edit'
| 'report_appartment'
| 'create_account'
| 'login'
| 'logout'
| 'delete_account'
| 'request_password_reset'
| 'use_password_reset'
| 'request_email_validation'
| 'use_email_validation'
| 'approve_appartent'
| 'change_email'
| 'change_password'
| 'archive_appartment'
| 'unarchive_appartment'
| 'delete_appartment'
| 'edit_account'
| 'delete_appartment_edit'
| 'delete_appartment_report'
| 'generate_travel_times'
| 'misc';
export const LOG_LEVELS = ['trace', 'info', 'warn', 'error', 'fatal'] as const;
export type LogLevel = (typeof LOG_LEVELS)[number];

function serialize(o: any): string {
if (typeof o === 'string') {
return o;
}
if (o instanceof Error) {
return o.message + (o.stack ? '\n' + o.stack : '(no stack trace available)');
}
try {
return JSON.stringify(o);
} catch (error) {
return o.toString();
}
}

async function _log(
level: LogLevel,
action: LogAction,
by: User | string | null,
...message: any[]
) {
let messageString = message.map(serialize).join(' ');
const consoleArgs = [
level,
typeof by === 'string' ? by : by?.email || 'unknown_user',
action,
messageString
];
if (level === 'error' || 'fatal') {
console.error(...consoleArgs);
} else if (level === 'warn') {
console.warn(...consoleArgs);
} else {
console.info(...consoleArgs);
}
let user = null;
if (by) {
user = await prisma.user.findFirst({
where:
typeof by === 'string' ? { [by.includes('@') ? 'email' : 'id']: by } : { id: by.id }
});
}
const createdLog = await prisma.log.create({
data: {
level: LOG_LEVELS.indexOf(level),
action,
message: messageString,
ip: '', // TODO
userId: user?.id ?? null
}
});
if (level === 'fatal') {
const gods = await prisma.user.findMany({
where: {
god: true
}
});
await Promise.all(
gods.map((god) =>
sendMail({
to: god.email,
subject: `Loca7: Fatal error when ${action}`,
template: 'plain',
data: {
text: `At ${new Date().toISOString()}<br><strong>${
user?.email ?? 'unknown user'
}</strong> triggered a fatal error when <strong>${action}</strong>:<br><br>${messageString.replaceAll(
'\n',
'<br>'
)}<br><br><br><br>For some context: <a href="${process.env.ORIGIN || 'http://localhost:5173'}/logs#${createdLog.createdAt.toISOString()}" target="_blank">see logs</a>.`
}
})
)
);
}
}

type LogFunction = (
action: LogAction,
by: User | string | null,
...message: any[]
) => Promise<void>;

export const log = Object.fromEntries(
LOG_LEVELS.map((level) => [
level.toLowerCase(),
((action, by, ...message) => _log(level, action, by, ...message)) as LogFunction
])
) as {
[level in LogLevel]: LogFunction;
};
7 changes: 5 additions & 2 deletions src/lib/server/mail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import mjml2html from 'mjml';
import nodemailer from 'nodemailer';
import path from 'path';
import { valueOfBooleanString } from './utils';
import { log } from './logging';

// generate:EmailTemplates
/**
Expand All @@ -15,6 +16,7 @@ export type EmailTemplates =
| 'announcement'
| 'email-changed'
| 'password-changed'
| 'plain'
| 'reset-password'
| 'validate-email';

Expand All @@ -34,7 +36,7 @@ export const mailer = nodemailer.createTransport({
: {})
});

export function sendMail({
export async function sendMail({
template,
to,
subject,
Expand All @@ -45,12 +47,13 @@ export function sendMail({
subject: string;
data: Record<string, string>;
}) {
await log.info('send_mail', null, { to, template, subject, data });
const computedSubject = Handlebars.compile(subject)(data);
const layout = readFileSync('mail-templates/_layout.mjml').toString('utf-8');
return mailer.sendMail({
from: '[email protected]',
to,
subject: computedSubject + ' @ ' + new Date().toISOString(),
subject: computedSubject,
html: mjml2html(
Handlebars.compile(
layout.replace(
Expand Down
13 changes: 13 additions & 0 deletions src/routes/account/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LuciaError } from 'lucia-auth';
import type { Actions, PageServerLoad } from './$types';
import { sendMail } from '$lib/server/mail';
import { CONTACT_EMAIL } from '$lib/constants';
import { log } from '$lib/server/logging';

export const load: PageServerLoad = async ({ locals, url }) => {
const { user, session } = await locals.validateUser();
Expand Down Expand Up @@ -33,8 +34,17 @@ export const actions: Actions = {
agencyWebsite
}
});
await log.info('edit_account', user, 'success', {
firstName,
lastName,
email,
phone,
agencyName,
agencyWebsite
});

if (email !== user.email) {
await log.info('change_email', user, 'attempt', { from: user.email, to: email });
// XXX this should be done through lucia-auth
await prisma.key.updateMany({
where: {
Expand All @@ -59,6 +69,7 @@ export const actions: Actions = {
});
}

await log.info('change_email', user, 'success', { from: user.email, to: email });
throw redirect(302, '/account');
},

Expand All @@ -74,6 +85,7 @@ export const actions: Actions = {
try {
await auth.validateKeyPassword('email', user.email, oldPassword);
await auth.updateKeyPassword('email', user.email, newPassword);
await log.info('change_password', user, 'attempt');
await sendMail({
to: user.email,
subject: 'Loca7: Votre mot de passe a été changé',
Expand All @@ -93,6 +105,7 @@ export const actions: Actions = {
throw error;
}
}
await log.info('change_password', user, 'success');
await fetch('/logout', { method: 'POST' });
},

Expand Down
5 changes: 4 additions & 1 deletion src/routes/appartements/[id]/approuver/+server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log } from '$lib/server/logging';
import { guards } from '$lib/server/lucia';
import { prisma } from '$lib/server/prisma';
import type { RequestHandler } from './$types';
Expand All @@ -22,7 +23,9 @@ export const POST: RequestHandler = async ({ params, locals, url }) => {
}
});

return new Response('Archivage effectué avec succès', {
await log.info('approve_appartent', user, 'success', { appartment: params.id });

return new Response('Appartement approuvé', {
status: 200
});
};
3 changes: 3 additions & 0 deletions src/routes/appartements/[id]/archiver/+server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log } from '$lib/server/logging';
import { guards } from '$lib/server/lucia';
import { prisma } from '$lib/server/prisma';
import type { RequestHandler } from './$types';
Expand All @@ -23,6 +24,8 @@ export const POST: RequestHandler = async ({ params, locals, url }) => {
}
});

await log.info('archive_appartment', user, 'success', { appartment: params.id });

return new Response('Archivage effectué avec succès', {
status: 200
});
Expand Down
11 changes: 11 additions & 0 deletions src/routes/appartements/[id]/generate-travel-times/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { RequestHandler } from './$types';
import { prisma } from '$lib/server/prisma';
import { openRouteService } from '$lib/server/traveltime';
import { ENSEEIHT } from '$lib/utils';
import { log } from '$lib/server/logging';

export const GET: RequestHandler = async ({ request, params, locals }) => {
const { user, session } = await locals.validateUser();
Expand All @@ -27,6 +28,11 @@ export const GET: RequestHandler = async ({ request, params, locals }) => {
);
}

await log.info('generate_travel_times', user, 'calculated foot & bike travel times', {
appartment,
newTravelTimes
});

await prisma.travelTimeToN7.update({
where: { id: appartment.travelTimeToN7.id },
data: {
Expand All @@ -36,5 +42,10 @@ export const GET: RequestHandler = async ({ request, params, locals }) => {
}
});

await log.info('generate_travel_times', user, 'added to database', {
appartment,
newTravelTimes
});

return new Response('OK');
};
12 changes: 12 additions & 0 deletions src/routes/appartements/[id]/modifier/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { copyPhotos, writePhotosToDisk } from '$lib/server/photos';
import xss from 'xss';
import { redirect } from '@sveltejs/kit';
import type { AppartmentKind } from '@prisma/client';
import { log } from '$lib/server/logging';

export const load: PageServerLoad = async ({ locals, params, url }) => {
const { user, session } = await locals.validateUser();
Expand Down Expand Up @@ -99,6 +100,8 @@ export const actions: Actions = {
}
});

const creatingOwner = !newOwner;

if (!newOwner) {
newOwner = await prisma.user.create({
data: {
Expand All @@ -124,6 +127,15 @@ export const actions: Actions = {
}
}
});
await log.info(
'submit_appartment_edit',
user,
'appartment owner changed from',
ownerEmail,
'to',
newOwner?.email + (creatingOwner ? ' (new account)' : ''),
'(effective immediately)'
);
}

const edit = await prisma.appartmentEdit.create({
Expand Down
8 changes: 7 additions & 1 deletion src/routes/appartements/[id]/publier/+server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log } from '$lib/server/logging';
import { guards } from '$lib/server/lucia';
import { prisma } from '$lib/server/prisma';
import type { RequestHandler } from './$types';
Expand All @@ -21,7 +22,7 @@ export const POST: RequestHandler = async ({ params, locals, url }) => {
guards.isAdmin(user, session, url);
}

await prisma.appartment.update({
const newAppartment = await prisma.appartment.update({
where: { id: params.id },
data: {
// Only approve if the user is an admin, else leave it as it was before
Expand All @@ -31,6 +32,11 @@ export const POST: RequestHandler = async ({ params, locals, url }) => {
}
});

await log.info('unarchive_appartment', user, 'success', {
before: appartment,
after: newAppartment
});

return new Response('Archivage effectué avec succès', {
status: 200
});
Expand Down
3 changes: 3 additions & 0 deletions src/routes/appartements/[id]/signaler/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ReportReason } from '@prisma/client';
import { redirect } from '@sveltejs/kit';
import xss from 'xss';
import type { Actions } from './$types';
import { log } from '$lib/server/logging';

export const actions: Actions = {
default: async ({ locals, request, url }) => {
Expand Down Expand Up @@ -33,6 +34,8 @@ export const actions: Actions = {
}
});

await log.info('report_appartment', user, { reason, message, appartmentId });

throw redirect(301, `/appartements/${appartmentId}#reportSubmitted`);
}
};
Loading

0 comments on commit 84866a5

Please sign in to comment.