diff --git a/.talismanrc b/.talismanrc index 08c09668..053c12af 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,11 +1,12 @@ fileignoreconfig: -- filename: .github/workflows/nodejs.yml - checksum: bf0ae31737daf27be68b7d2c4e63e1ef14cb799f1853462fdadcf2422ddd53a2 -- filename: __tests__/test-worker.ts - checksum: 462f569a2625f2fdb0938f0abc109ed24f0241d9f5592ead16475e2efde0aa47 -- filename: package-lock.json - checksum: add9168b63b1a9219558f2be2378591402a00d394be3bd9102715e43f8278e4f -- filename: src/app/(private)/(dashboard)/admin/signaux-faibles-beta/AdminProductClientPage.tsx +- filename: src/app/api/services/actions.ts + checksum: 9dfef4033112a7c878edfbb4cffe6bf7a888144aaf3e80646a2ee19b9f923b98 +- filename: src/models/jobs/services.ts + checksum: ee599ef3117bbfd15bbf5cf7843e75a8177c3e03eb0311ebbe71a3228b19e280 +- filename: src/server/queueing/workers/create-sentry-account.ts + checksum: 3f10e3c1996e2b05dfa5276d84542b19de5a9f4dd5282255980c300f5c526e30 +version: "" +ux-faibles-beta/AdminProductClientPage.tsx checksum: 9c740513497d70e871e4abcad3a9e509ef5b1a0da9db2d0d2d336c19854303b4 - filename: src/app/(private)/(dashboard)/incubators/[id]/info-form/page.tsx checksum: 1ec9acf039a7d336e710ef7b47c385eb7080293ee2750c2cf99e9056cde7f129 diff --git a/migrations/20241120171235_add_sentry_teams_table.ts b/migrations/20241120171235_add_sentry_teams_table.ts new file mode 100644 index 00000000..4282e32e --- /dev/null +++ b/migrations/20241120171235_add_sentry_teams_table.ts @@ -0,0 +1,21 @@ +exports.up = async function (knex) { + await knex.schema.createTable("sentry_teams", (table) => { + table.uuid("id").primary().defaultTo(knex.raw("gen_random_uuid()")); // UUID pour l'identifiant unique + table.string("sentry_id").notNullable().unique(); // Identifiant de team Sentry + table.uuid("startup_id").nullable(); // UUID lié à la table startups + table.string("name").notNullable(); // Nom de l'équipe + + // Clé étrangère vers la table startups + table + .foreign("startup_id") + .references("uuid") + .inTable("startups") + .onDelete("CASCADE"); + + table.timestamps(true, true); // Champs created_at et updated_at + }); +}; + +exports.down = async function (knex) { + await knex.schema.dropTableIfExists("sentry_teams"); +}; diff --git a/src/app/(private)/(dashboard)/services/sentry/page.tsx b/src/app/(private)/(dashboard)/services/sentry/page.tsx new file mode 100644 index 00000000..e2186549 --- /dev/null +++ b/src/app/(private)/(dashboard)/services/sentry/page.tsx @@ -0,0 +1,56 @@ +import { redirect } from "next/navigation"; +import { getServerSession } from "next-auth/next"; + +import AccountDetails from "@/components/Service/AccountDetails"; +import SentryServiceForm from "@/components/Service/SentryServiceForm"; +import { getServiceAccount } from "@/lib/kysely/queries/services"; +import { sentryServiceInfoToModel } from "@/models/mapper/sentryMapper"; +import { sentryUserSchemaType } from "@/models/sentry"; +import { SERVICES } from "@/models/services"; +import config from "@/server/config"; +import { authOptions } from "@/utils/authoptions"; + +const buildLinkToSentryTeam = ( + team: sentryUserSchemaType["metadata"]["teams"][0] +) => { + return team.name ? ( + + {team.name} + + ) : ( + team.name + ); +}; + +export default async function SentryPage() { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/login"); + } + + const rawAccount = await getServiceAccount( + session.user.uuid, + SERVICES.SENTRY + ); + const service_account = rawAccount + ? sentryServiceInfoToModel(rawAccount) + : undefined; + + return ( + <> +

Compte Sentry

+ {service_account ? ( + [ + buildLinkToSentryTeam(team), + team.role, + ])} + headers={["nom", "niveau d'accès"]} + /> + ) : ( + + )} + + ); +} diff --git a/src/app/api/services/actions.ts b/src/app/api/services/actions.ts index f7d029b9..c50e6b7d 100644 --- a/src/app/api/services/actions.ts +++ b/src/app/api/services/actions.ts @@ -6,15 +6,20 @@ import { match } from "ts-pattern"; import { db } from "@/lib/kysely"; import { getUserBasicInfo, getUserStartups } from "@/lib/kysely/queries/users"; -import { matomoAccountRequestWrapperSchemaType } from "@/models/actions/service"; +import { + matomoAccountRequestWrapperSchemaType, + sentryAccountRequestWrapperSchemaType, +} from "@/models/actions/service"; import { CreateMattermostAccountDataSchema, CreateMatomoAccountDataSchema, + CreateSentryAccountDataSchema, } from "@/models/jobs/services"; import { ACCOUNT_SERVICE_STATUS, SERVICES } from "@/models/services"; import { encryptPassword } from "@/server/controllers/utils"; import { getBossClientInstance } from "@/server/queueing/client"; import { createMatomoServiceAccountTopic } from "@/server/queueing/workers/create-matomo-account"; +import { createSentryServiceAccountTopic } from "@/server/queueing/workers/create-sentry-account"; import { authOptions } from "@/utils/authoptions"; import { AuthorizationError, @@ -29,6 +34,7 @@ export const askAccountCreationForService = withErrorHandling( data, }: | matomoAccountRequestWrapperSchemaType + | sentryAccountRequestWrapperSchemaType | { service: SERVICES.MATTERMOST; data: any }) => { // create task const session = await getServerSession(authOptions); @@ -74,6 +80,41 @@ export const askAccountCreationForService = withErrorHandling( }) .execute(); }) + .with(SERVICES.SENTRY, async () => { + if (!user.primary_email) { + throw new ValidationError( + "Un email primaire est obligatoire" + ); + } + await bossClient.send( + createSentryServiceAccountTopic, + CreateSentryAccountDataSchema.parse({ + email: user.primary_email, + login: user.primary_email, + password: encryptPassword( + data.password || + crypto + .randomBytes(20) + .toString("base64") + .slice(0, -2) + ), + }), + { + retryLimit: 50, + retryBackoff: true, + } + ); + await db + .insertInto("service_accounts") + .values({ + user_id: user.uuid, + email: user.primary_email, + account_type: SERVICES.SENTRY, + status: ACCOUNT_SERVICE_STATUS.ACCOUNT_CREATION_PENDING, + }) + .execute(); + }) + .otherwise(() => { // otherwise or exhaustive should be defined otherwise function is not awaited // cf https://github.com/gvergnaud/ts-pattern/issues/163 diff --git a/src/lib/kysely/queries/services.ts b/src/lib/kysely/queries/services.ts new file mode 100644 index 00000000..8b230b8b --- /dev/null +++ b/src/lib/kysely/queries/services.ts @@ -0,0 +1,11 @@ +import { db } from "@/lib/kysely"; +import { SERVICES } from "@/models/services"; + +export async function getServiceAccount(userId: string, service: SERVICES) { + return db + .selectFrom("service_accounts") + .selectAll() + .where("user_id", "=", userId) + .where("account_type", "=", service) + .executeTakeFirst(); +} diff --git a/src/models/jobs/services.ts b/src/models/jobs/services.ts index f7f6932a..7d730b41 100644 --- a/src/models/jobs/services.ts +++ b/src/models/jobs/services.ts @@ -31,3 +31,15 @@ export const CreateMatomoAccountDataSchema = export type CreateMatomoAccountDataSchemaType = z.infer< typeof CreateMatomoAccountDataSchema >; + +export const CreateSentryAccountDataSchema = + MaintenanceWrapperDataSchema.extend({ + email: z.string().email(), // Valide que l'email est bien formaté + login: z.string().min(1, "Le nom d'utilisateur est requis"), // Valide que le nom d'utilisateur n'est pas vide + password: z + .string() + .min(6, "Le mot de passe doit contenir au moins 6 caractères"), // Valide que le mot de passe contient au moins 6 caractères + }).strict(); +export type CreateSentryAccountDataSchemaType = z.infer< + typeof CreateSentryAccountDataSchema +>; diff --git a/src/server/queueing/workers/create-sentry-account.ts b/src/server/queueing/workers/create-sentry-account.ts new file mode 100644 index 00000000..f4ef3470 --- /dev/null +++ b/src/server/queueing/workers/create-sentry-account.ts @@ -0,0 +1,43 @@ +import pAll from "p-all"; +import PgBoss from "pg-boss"; + +import { db } from "@/lib/kysely"; +import { CreateSentryAccountDataSchemaType } from "@/models/jobs/services"; +import { sentryMetadataToModel } from "@/models/mapper/sentryMapper"; +import { ACCOUNT_SERVICE_STATUS, SERVICES } from "@/models/services"; +import { sentryClient } from "@/server/config/sentry.config"; +import { decryptPassword } from "@/server/controllers/utils"; + +export const createSentryServiceAccountTopic = "create-sentry-service-account"; + +export async function createSentryServiceAccount( + job: PgBoss.Job +) { + console.log( + `Create sentry service account for ${job.data.login}`, + job.id, + job.name + ); + + // throw new Error("Account could not be created"); + const userLogin = job.data.email; + + await sentryClient.createUser({ + email: job.data.email, + password: decryptPassword(job.data.password), + userLogin, + alias: job.data.email, + teams: [], + }); + + const result = await db + .updateTable("service_accounts") + .set({ + service_user_id: userLogin, + status: ACCOUNT_SERVICE_STATUS.ACCOUNT_FOUND, + metadata: JSON.stringify(metadata), + }) + .executeTakeFirstOrThrow(); + + console.log(`the sentry account has been created for ${userLogin}`); +}