Skip to content

Commit

Permalink
Les transactions Drizzle supportent l'authentification Supabase et do…
Browse files Browse the repository at this point in the history
…nc les RLS 🎉

Permet notamment de gérer la bonne mise à jour du champ `modified_by` de la table `fiche_action`.
  • Loading branch information
farnoux committed Dec 20, 2024
1 parent 24d3169 commit 7513917
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 16 deletions.
6 changes: 4 additions & 2 deletions backend/src/auth/models/auth.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ export enum AuthRole {
ANON = 'anon', // Anonymous
}

// export type User = Pick<SupabaseUser, 'id' | 'role' | 'is_anonymous'>;

export interface AuthUser<Role extends AuthRole = AuthRole> {
id: Role extends AuthRole.AUTHENTICATED ? string : null;
role: Role;
isAnonymous: Role extends AuthRole.AUTHENTICATED ? false : true;
jwtToken: AuthJwtPayload<Role>;
}

export type AnonymousUser = AuthUser<AuthRole.ANON>;
Expand Down Expand Up @@ -49,6 +48,7 @@ export function jwtToUser(jwt: AuthJwtPayload): AuthUser {
id: jwt.sub,
role: AuthRole.AUTHENTICATED,
isAnonymous: false,
jwtToken: jwt,
};
}

Expand All @@ -57,6 +57,7 @@ export function jwtToUser(jwt: AuthJwtPayload): AuthUser {
id: null,
role: AuthRole.ANON,
isAnonymous: true,
jwtToken: jwt,
};
}

Expand All @@ -65,6 +66,7 @@ export function jwtToUser(jwt: AuthJwtPayload): AuthUser {
id: null,
role: AuthRole.SERVICE_ROLE,
isAnonymous: true,
jwtToken: jwt,
};
}

Expand Down
52 changes: 44 additions & 8 deletions backend/src/common/services/database.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,60 @@
import { AuthUser } from '@/backend/auth/models/auth.models';
import { Injectable, Logger, OnApplicationShutdown } from '@nestjs/common';
import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import { sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/postgres-js';
import { default as postgres } from 'postgres';
import ConfigurationService from '../../config/configuration.service';

@Injectable()
export default class DatabaseService implements OnApplicationShutdown {
private readonly logger = new Logger(DatabaseService.name);
public readonly db: PostgresJsDatabase;
private readonly client: postgres.Sql;

constructor(private readonly configService: ConfigurationService) {
this.logger.log(`Initializing database service`);
this.client = postgres(this.configService.get('SUPABASE_DATABASE_URL'), {
private readonly client = postgres(
this.configService.get('SUPABASE_DATABASE_URL'),
{
prepare: false,
connection: {
application_name: `Backend ${process.env.APPLICATION_VERSION}`,
},
});
}
);

public readonly db = drizzle(this.client);

constructor(private readonly configService: ConfigurationService) {
this.logger.log(`Initializing database service`);
}

this.db = drizzle(this.client);
// Taken from https://orm.drizzle.team/docs/rls
rls(user: AuthUser) {
return (async (transaction, ...rest) => {
return await this.db.transaction(async (tx) => {
// Supabase exposes auth.uid() and auth.jwt()
// https://supabase.com/docs/guides/database/postgres/row-level-security#helper-functions
try {
await tx.execute(sql`
-- auth.jwt()
select set_config('request.jwt.claims', '${sql.raw(
JSON.stringify(user.jwtToken)
)}', TRUE);
-- auth.uid()
select set_config('request.jwt.claim.sub', '${sql.raw(
user.jwtToken.sub ?? ''
)}', TRUE);
-- set local role
set local role ${sql.raw(user.jwtToken.role ?? 'anon')};
`);
return await transaction(tx);
} finally {
await tx.execute(sql`
-- reset
select set_config('request.jwt.claims', NULL, TRUE);
select set_config('request.jwt.claim.sub', NULL, TRUE);
reset role;
`);
}
}, ...rest);
}) as typeof this.db.transaction;
}

async onApplicationShutdown(signal: string) {
Expand Down
17 changes: 11 additions & 6 deletions backend/src/fiches/services/fiches-action-update.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ export default class FichesActionUpdateService {
async updateFicheAction(
ficheActionId: number,
body: UpdateFicheActionRequestType,
tokenInfo: AuthenticatedUser
user: AuthenticatedUser
) {
await this.ficheService.canWriteFiche(ficheActionId, tokenInfo);
await this.ficheService.canWriteFiche(ficheActionId, user);

this.logger.log(
`Mise à jour de la fiche action dont l'id est ${ficheActionId}`
Expand All @@ -103,7 +103,7 @@ export default class FichesActionUpdateService {
...unsafeFicheAction
} = body;

return await this.databaseService.db.transaction(async (tx) => {
return await this.databaseService.rls(user)(async (tx) => {
const existingFicheAction = await this.databaseService.db
.select()
.from(ficheActionTable)
Expand Down Expand Up @@ -136,10 +136,15 @@ export default class FichesActionUpdateService {
* Updates fiche action properties
*/

if (Object.keys(ficheAction).length > 0) {
if (Object.keys(body).length > 0) {
updatedFicheAction = await tx
.update(ficheActionTable)
.set(ficheAction)
// `modifiedBy` in the fiche_action table is automatically updated
// via a PG trigger (using the current authenticated user of `db.rls()`).
// But we also set it here so that when the updating object `ficheAction` is empty,
// the trigger can be well… triggered (corresponding to updates only affecting
// related tables, and not directly the fiche_action main table)
.set({ ...ficheAction, modifiedBy: user.id })
.where(eq(ficheActionTable.id, ficheActionId))
.returning();
}
Expand Down Expand Up @@ -335,7 +340,7 @@ export default class FichesActionUpdateService {
libresTag.map((relation) => ({
ficheId: ficheActionId,
libreTagId: relation.id,
createdBy: tokenInfo.id,
createdBy: user.id,
}))
)
.returning();
Expand Down

0 comments on commit 7513917

Please sign in to comment.