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}`);
+}