This commit is contained in:
mrKamoo
2026-09-30 16:27:32 +02:00
commit 310724708f
147 changed files with 21327 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

+161
View File
@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Manifeste de l'add-in Signatures CABM (format XML « add-in only »).
Le format XML est obligatoire pour Outlook mobile (le manifeste unifié JSON n'y est pas supporté).
{{BASE_URL}} est remplacé au build (webpack) : https://signature.beziers-mediterranee.fr/addin en production.
-->
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0"
xsi:type="MailApp">
<Id>8102c04b-a358-496c-99f3-70fac75e23b5</Id>
<Version>0.1.0.0</Version>
<ProviderName>Communauté d'agglomération Béziers Méditerranée</ProviderName>
<DefaultLocale>fr-FR</DefaultLocale>
<DisplayName DefaultValue="Signatures CABM"/>
<Description DefaultValue="Insère automatiquement la signature officielle de l'agent dans les messages et invitations."/>
<IconUrl DefaultValue="{{BASE_URL}}/assets/icon-64.png"/>
<HighResolutionIconUrl DefaultValue="{{BASE_URL}}/assets/icon-128.png"/>
<SupportUrl DefaultValue="{{SUPPORT_URL}}"/>
<AppDomains>
<AppDomain>{{ORIGIN}}</AppDomain>
<AppDomain>https://login.microsoftonline.com</AppDomain>
</AppDomains>
<Hosts>
<Host Name="Mailbox"/>
</Hosts>
<Requirements>
<Sets>
<Set Name="Mailbox" MinVersion="1.1"/>
</Sets>
</Requirements>
<FormSettings>
<Form xsi:type="ItemEdit">
<DesktopSettings>
<SourceLocation DefaultValue="{{BASE_URL}}/taskpane.html"/>
</DesktopSettings>
</Form>
</FormSettings>
<Permissions>ReadWriteItem</Permissions>
<Rule xsi:type="RuleCollection" Mode="Or">
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Edit"/>
<Rule xsi:type="ItemIs" ItemType="Appointment" FormType="Edit"/>
</Rule>
<DisableEntityHighlighting>false</DisableEntityHighlighting>
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides" xsi:type="VersionOverridesV1_0">
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1" xsi:type="VersionOverridesV1_1">
<!-- 1.5 = maximum déclaré par Outlook mobile ; les API récentes (setSignatureAsync…) y sont ouvertes par exception. -->
<Requirements>
<bt:Sets DefaultMinVersion="1.5">
<bt:Set Name="Mailbox"/>
</bt:Sets>
</Requirements>
<Hosts>
<Host xsi:type="MailHost">
<Runtimes>
<!-- Runtime HTML (OWA, nouvel Outlook, mobile) ; override JS pour Outlook classique Windows. -->
<Runtime resid="WebViewRuntime.Url">
<Override type="javascript" resid="JSRuntime.Url"/>
</Runtime>
</Runtimes>
<DesktopFormFactor>
<SupportsSharedFolders>true</SupportsSharedFolders>
<FunctionFile resid="WebViewRuntime.Url"/>
<!-- Bouton « Ma signature » dans la fenêtre de rédaction d'un message -->
<ExtensionPoint xsi:type="MessageComposeCommandSurface">
<OfficeTab id="TabDefault">
<Group id="msgComposeGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="msgComposeOpenPaneButton">
<Label resid="TaskpaneButton.Label"/>
<Supertip>
<Title resid="TaskpaneButton.Label"/>
<Description resid="TaskpaneButton.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
<!-- Même bouton dans une invitation -->
<ExtensionPoint xsi:type="AppointmentOrganizerCommandSurface">
<OfficeTab id="TabDefault">
<Group id="apptComposeGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="apptComposeOpenPaneButton">
<Label resid="TaskpaneButton.Label"/>
<Supertip>
<Title resid="TaskpaneButton.Label"/>
<Description resid="TaskpaneButton.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
<!-- Activation automatique -->
<ExtensionPoint xsi:type="LaunchEvent">
<LaunchEvents>
<LaunchEvent Type="OnNewMessageCompose" FunctionName="onNewMessageComposeHandler"/>
<LaunchEvent Type="OnNewAppointmentOrganizer" FunctionName="onNewAppointmentOrganizerHandler"/>
<LaunchEvent Type="OnMessageFromChanged" FunctionName="onMessageFromChangedHandler"/>
</LaunchEvents>
<SourceLocation resid="WebViewRuntime.Url"/>
</ExtensionPoint>
</DesktopFormFactor>
<MobileFormFactor>
<!-- OnNewAppointmentOrganizer n'est pas supporté sur mobile -->
<ExtensionPoint xsi:type="LaunchEvent">
<LaunchEvents>
<LaunchEvent Type="OnNewMessageCompose" FunctionName="onNewMessageComposeHandler"/>
<LaunchEvent Type="OnMessageFromChanged" FunctionName="onMessageFromChangedHandler"/>
</LaunchEvents>
<SourceLocation resid="WebViewRuntime.Url"/>
</ExtensionPoint>
</MobileFormFactor>
</Host>
</Hosts>
<Resources>
<bt:Images>
<bt:Image id="Icon.16x16" DefaultValue="{{BASE_URL}}/assets/icon-16.png"/>
<bt:Image id="Icon.32x32" DefaultValue="{{BASE_URL}}/assets/icon-32.png"/>
<bt:Image id="Icon.80x80" DefaultValue="{{BASE_URL}}/assets/icon-80.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="WebViewRuntime.Url" DefaultValue="{{BASE_URL}}/commands.html"/>
<bt:Url id="JSRuntime.Url" DefaultValue="{{BASE_URL}}/launchevent.js"/>
<bt:Url id="Taskpane.Url" DefaultValue="{{BASE_URL}}/taskpane.html"/>
</bt:Urls>
<bt:ShortStrings>
<bt:String id="GroupLabel" DefaultValue="Signatures CABM"/>
<bt:String id="TaskpaneButton.Label" DefaultValue="Ma signature"/>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Afficher, réinsérer ou modifier ma signature."/>
</bt:LongStrings>
</Resources>
</VersionOverrides>
</VersionOverrides>
</OfficeApp>
+5462
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "cabm-signature-addin",
"version": "0.1.0",
"private": true,
"description": "Add-in Outlook CABM : insertion automatique des signatures email",
"scripts": {
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"watch": "webpack --mode development --watch",
"typecheck": "tsc --noEmit",
"validate": "office-addin-manifest validate dist/manifest.xml",
"dev-server": "webpack serve --mode development"
},
"dependencies": {
"@azure/msal-browser": "^5.23.0"
},
"devDependencies": {
"@types/office-js": "^1.0.610",
"copy-webpack-plugin": "^13.0.1",
"html-webpack-plugin": "^5.6.8",
"office-addin-dev-certs": "^3.0.1",
"office-addin-manifest": "^3.0.1",
"ts-loader": "^9.5.2",
"typescript": "^5.8.0",
"webpack": "^5.101.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^6.0.0"
}
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<title>Signatures CABM — runtime</title>
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
</head>
<body>
<!-- Page sans interface : charge le runtime des gestionnaires d'événements (launchevent.js injecté au build). -->
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
/**
* Gestionnaires d'événements (activation automatique, sans action de l'agent).
* Limites : ~300 s sur le web / nouvel Outlook, 60 s sur mobile. event.completed() est obligatoire.
*/
import { fetchSignature } from "../shared/api";
import { getTokenSilent } from "../shared/auth";
import { readCache, writeCache } from "../shared/cache";
import { applySignature, currentItem, getComposeContext, getMailbox, notify } from "../shared/office";
async function insertSignature(): Promise<void> {
const item = currentItem();
const context = await getComposeContext(item);
const mailbox = await getMailbox(item);
console.info("[signature] événement reçu", { context, mailbox });
let html: string | null = null;
const token = await getTokenSilent();
console.info("[signature] jeton", token ? "obtenu" : "absent");
if (token) {
const res = await fetchSignature(token, context, mailbox);
if (res) {
html = res.html;
await writeCache(context, mailbox, html);
}
}
if (html === null) html = readCache(context, mailbox);
if (html === null) {
await notify(item, "Signature CABM indisponible pour le moment. Ouvrez « Ma signature » pour réessayer.");
return;
}
console.info("[signature] insertion", html.length, "caractères");
// Chaîne vide = choix explicite « aucune signature » (ex. réponse sans signature).
await applySignature(item, html);
}
function handler(event: Office.AddinCommands.Event): void {
insertSignature()
.catch((e) => console.error("[signature]", e))
.finally(() => event.completed());
}
export const onNewMessageComposeHandler = handler;
export const onNewAppointmentOrganizerHandler = handler;
export const onMessageFromChangedHandler = handler;
Office.onReady((info) => {
console.info("[signature] runtime chargé", info.host, info.platform);
});
// Association nom (manifeste) -> fonction. Indispensable avec un bundle webpack.
Office.actions.associate("onNewMessageComposeHandler", onNewMessageComposeHandler);
Office.actions.associate("onNewAppointmentOrganizerHandler", onNewAppointmentOrganizerHandler);
Office.actions.associate("onMessageFromChangedHandler", onMessageFromChangedHandler);
+42
View File
@@ -0,0 +1,42 @@
import { CONFIG } from "./config";
/** Contexte de rédaction transmis à l'API : elle choisit la variante (complète / courte). */
export type ComposeContext = "new" | "reply" | "forward" | "appointment";
export interface SignatureResponse {
html: string;
variant: "full" | "short";
template: string;
generated_at: string;
}
const TIMEOUT_MS = 8000;
export async function fetchSignature(
token: string,
context: ComposeContext,
mailbox: string | null,
): Promise<SignatureResponse | null> {
const url = new URL("/api/v1/signature", CONFIG.apiBaseUrl);
url.searchParams.set("context", context);
if (mailbox) url.searchParams.set("mailbox", mailbox);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const res = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
signal: controller.signal,
});
if (!res.ok) {
console.warn("[signature] API", res.status);
return null;
}
return (await res.json()) as SignatureResponse;
} catch (e) {
console.warn("[signature] API injoignable", e);
return null;
} finally {
clearTimeout(timer);
}
}
+65
View File
@@ -0,0 +1,65 @@
import {
BrowserCacheLocation,
createNestablePublicClientApplication,
type IPublicClientApplication,
} from "@azure/msal-browser";
import { CONFIG } from "./config";
/**
* Authentification par Nested App Authentication (NAA).
* Les jetons Exchange historiques (getUserIdentityTokenAsync) sont désactivés sur tous les tenants :
* NAA est la seule voie supportée pour obtenir un jeton Entra depuis l'add-in.
*/
let pcaPromise: Promise<IPublicClientApplication> | null = null;
function getPca(): Promise<IPublicClientApplication> {
if (!pcaPromise) {
pcaPromise = createNestablePublicClientApplication({
auth: {
clientId: CONFIG.clientId,
authority: `https://login.microsoftonline.com/${CONFIG.tenantId}`,
},
cache: { cacheLocation: BrowserCacheLocation.LocalStorage },
});
}
return pcaPromise;
}
export function isNaaSupported(): boolean {
try {
return Office.context.requirements.isSetSupported("NestedAppAuth", "1.1");
} catch {
return false;
}
}
/**
* Jeton silencieux uniquement : c'est la seule option dans un gestionnaire d'événement
* (aucune fenêtre ne peut s'ouvrir). Le consentement administrateur rend ce cas nominal.
*/
export async function getTokenSilent(): Promise<string | null> {
if (!isNaaSupported()) return null;
try {
const pca = await getPca();
const result = await pca.acquireTokenSilent({ scopes: [CONFIG.apiScope] });
return result.accessToken;
} catch (e) {
console.warn("[signature] jeton silencieux indisponible", e);
return null;
}
}
/** Jeton avec repli interactif : utilisable uniquement depuis le volet (taskpane). */
export async function getTokenInteractive(): Promise<string | null> {
const silent = await getTokenSilent();
if (silent) return silent;
if (!isNaaSupported()) return null;
try {
const pca = await getPca();
const result = await pca.acquireTokenPopup({ scopes: [CONFIG.apiScope] });
return result.accessToken;
} catch (e) {
console.error("[signature] authentification interactive échouée", e);
return null;
}
}
+36
View File
@@ -0,0 +1,36 @@
import type { ComposeContext } from "./api";
/**
* Cache de la dernière signature reçue, dans les roamingSettings de la boîte
* (persistés côté Exchange, disponibles sur tous les clients, 32 Ko max au total).
* Sert de repli si l'API ou l'authentification est indisponible.
*/
const PREFIX = "sig:";
function key(context: ComposeContext, mailbox: string | null): string {
const variant = context === "reply" || context === "forward" ? "reply" : context === "appointment" ? "appt" : "new";
return `${PREFIX}${variant}:${mailbox ?? "self"}`;
}
export function readCache(context: ComposeContext, mailbox: string | null): string | null {
try {
const v = Office.context.roamingSettings.get(key(context, mailbox));
return typeof v === "string" && v.length > 0 ? v : null;
} catch {
return null;
}
}
export function writeCache(context: ComposeContext, mailbox: string | null, html: string): Promise<void> {
return new Promise((resolve) => {
try {
const rs = Office.context.roamingSettings;
const k = key(context, mailbox);
if (rs.get(k) === html) return resolve();
rs.set(k, html);
rs.saveAsync(() => resolve());
} catch {
resolve();
}
});
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Configuration injectée au build par webpack (DefinePlugin), à partir de .env / variables d'environnement.
* Voir addin/.env.example.
*/
declare const __ADDIN_CONFIG__: {
tenantId: string;
clientId: string;
apiBaseUrl: string;
apiScope: string;
portalUrl: string;
};
export const CONFIG = __ADDIN_CONFIG__;
+83
View File
@@ -0,0 +1,83 @@
import type { ComposeContext } from "./api";
/** Promesse autour des API asynchrones Office (callback AsyncResult). */
export function officeAsync<T>(call: (cb: (r: Office.AsyncResult<T>) => void) => void): Promise<T> {
return new Promise((resolve, reject) => {
call((r) => (r.status === Office.AsyncResultStatus.Succeeded ? resolve(r.value) : reject(r.error)));
});
}
type ComposeItem = Office.MessageCompose | Office.AppointmentCompose;
export function currentItem(): ComposeItem {
return Office.context.mailbox.item as ComposeItem;
}
export function isAppointment(item: ComposeItem): item is Office.AppointmentCompose {
return item.itemType === Office.MailboxEnums.ItemType.Appointment;
}
/** new / reply / forward pour un message, appointment pour une invitation. */
export async function getComposeContext(item: ComposeItem): Promise<ComposeContext> {
if (isAppointment(item)) return "appointment";
const msg = item as Office.MessageCompose;
if (typeof msg.getComposeTypeAsync !== "function") return "new";
try {
const v = await officeAsync<{ composeType: string }>((cb) => msg.getComposeTypeAsync(cb));
switch (v.composeType) {
case Office.MailboxEnums.ComposeType.Reply:
return "reply";
case Office.MailboxEnums.ComposeType.Forward:
return "forward";
default:
return "new";
}
} catch {
return "new";
}
}
/** Adresse de la boîte émettrice (boîte partagée si l'agent a changé le champ De). */
export async function getMailbox(item: ComposeItem): Promise<string | null> {
try {
if (isAppointment(item)) {
const o = await officeAsync<Office.EmailAddressDetails>((cb) => item.organizer.getAsync(cb));
return o?.emailAddress?.toLowerCase() ?? null;
}
const f = await officeAsync<Office.EmailAddressDetails>((cb) => (item as Office.MessageCompose).from.getAsync(cb));
return f?.emailAddress?.toLowerCase() ?? null;
} catch {
return Office.context.mailbox.userProfile?.emailAddress?.toLowerCase() ?? null;
}
}
export async function applySignature(item: ComposeItem, html: string): Promise<void> {
await officeAsync<void>((cb) => item.body.setSignatureAsync(html, { coercionType: Office.CoercionType.Html }, cb));
// Neutralise la signature native d'Outlook (sinon doublon). Ignoré si non supporté.
if (typeof item.disableClientSignatureAsync === "function") {
try {
await officeAsync<void>((cb) => item.disableClientSignatureAsync(cb));
} catch {
/* non bloquant */
}
}
}
export function notify(item: ComposeItem, message: string): Promise<void> {
return new Promise((resolve) => {
try {
item.notificationMessages.replaceAsync(
"cabm_signature",
{
type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
message,
icon: "Icon.16x16",
persistent: false,
},
() => resolve(),
);
} catch {
resolve();
}
});
}
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ma signature</title>
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
<style>
:root { --ink: #1b1b1b; --muted: #5c5c5c; --line: #d6d6d6; --accent: #0b5cad; }
body { font-family: "Segoe UI", system-ui, sans-serif; color: var(--ink); margin: 0; padding: 16px; font-size: 14px; }
h1 { font-size: 18px; margin: 0 0 12px; }
.muted { color: var(--muted); font-size: 12px; }
#preview { border: 1px solid var(--line); border-radius: 6px; padding: 12px; margin: 12px 0; min-height: 60px; overflow-x: auto; background: #fff; }
button { background: var(--accent); color: #fff; border: 0; border-radius: 4px; padding: 8px 14px; font-size: 14px; cursor: pointer; }
button:disabled { opacity: .5; cursor: default; }
a { color: var(--accent); }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
</style>
</head>
<body>
<h1>Ma signature</h1>
<p class="muted" id="status">Chargement…</p>
<div id="preview" aria-live="polite"></div>
<div class="row">
<button id="apply" disabled>Insérer dans ce message</button>
<a id="portal" target="_blank" rel="noopener">Modifier mes informations</a>
</div>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
/**
* Volet « Ma signature » : aperçu, réinsertion manuelle et lien vers le portail agent.
* C'est aussi le seul endroit où l'authentification interactive (popup) est possible.
*/
import { fetchSignature } from "../shared/api";
import { getTokenInteractive } from "../shared/auth";
import { writeCache } from "../shared/cache";
import { CONFIG } from "../shared/config";
import { applySignature, currentItem, getComposeContext, getMailbox } from "../shared/office";
const $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
async function load(): Promise<void> {
const status = $("status");
const preview = $("preview");
const apply = $<HTMLButtonElement>("apply");
$<HTMLAnchorElement>("portal").href = CONFIG.portalUrl;
const item = currentItem();
const context = await getComposeContext(item);
const mailbox = await getMailbox(item);
const token = await getTokenInteractive();
if (!token) {
status.textContent = "Connexion impossible. Contactez le support informatique.";
return;
}
const res = await fetchSignature(token, context, mailbox);
if (!res) {
status.textContent = "Le service de signature ne répond pas.";
return;
}
await writeCache(context, mailbox, res.html);
status.textContent = `Modèle : ${res.template} · variante ${res.variant === "short" ? "courte" : "complète"}`;
// Aperçu isolé dans une iframe sandbox (aucun script, origine opaque) : le HTML d'un template
// ne peut pas accéder au jeton ni au contexte Office du volet.
const frame = document.createElement("iframe");
frame.setAttribute("sandbox", "");
frame.title = "Aperçu de la signature";
frame.style.cssText = "width:100%;min-height:200px;border:0";
frame.srcdoc = `<!doctype html><meta charset="utf-8"><body style="margin:0">${res.html || "<em>Aucune signature pour ce contexte.</em>"}</body>`;
preview.replaceChildren(frame);
apply.disabled = false;
apply.onclick = async () => {
apply.disabled = true;
try {
await applySignature(item, res.html);
status.textContent = "Signature insérée.";
} catch (e) {
status.textContent = "Insertion impossible.";
console.error(e);
} finally {
apply.disabled = false;
}
};
}
Office.onReady(() => {
load().catch((e) => {
console.error(e);
$("status").textContent = "Erreur inattendue.";
});
});
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2020", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"sourceMap": true,
"types": ["office-js"]
},
"include": ["src/**/*.ts"]
}
+98
View File
@@ -0,0 +1,98 @@
/* eslint-disable */
const path = require("path");
const fs = require("fs");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
/** Lecture minimale de .env (sans dépendance) ; les variables d'environnement priment. */
function loadEnv() {
const env = {};
const file = path.resolve(__dirname, ".env");
if (fs.existsSync(file)) {
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
}
}
return { ...env, ...process.env };
}
module.exports = async (_env, argv) => {
const e = loadEnv();
const baseUrl = (e.ADDIN_BASE_URL || "https://signature.beziers-mediterranee.fr/addin").replace(/\/$/, "");
const origin = new URL(baseUrl).origin;
const config = {
tenantId: e.ENTRA_TENANT_ID || "",
clientId: e.ADDIN_CLIENT_ID || "",
apiBaseUrl: e.API_BASE_URL || origin,
apiScope: e.API_SCOPE || "",
portalUrl: e.PORTAL_URL || `${origin}/me`,
};
if (argv.mode === "production" && (!config.tenantId || !config.clientId || !config.apiScope)) {
console.warn("\n[ATTENTION] ENTRA_TENANT_ID, ADDIN_CLIENT_ID ou API_SCOPE vide : renseignez addin/.env\n");
}
// Serveur de dev HTTPS (npm run dev-server) : certificat localhost approuvé par office-addin-dev-certs,
// /api et /img relayés vers le Laravel local pour éviter le contenu mixte.
let devServer;
if (process.env.WEBPACK_SERVE) {
const devCerts = require("office-addin-dev-certs");
const https = await devCerts.getHttpsServerOptions();
const backend = e.DEV_BACKEND_URL || "http://127.0.0.1:8000";
devServer = {
port: Number(new URL(baseUrl).port || 3000),
server: { type: "https", options: { ca: https.ca, key: https.key, cert: https.cert } },
headers: { "Access-Control-Allow-Origin": "*" },
static: false,
devMiddleware: { publicPath: new URL(baseUrl).pathname.replace(/\/?$/, "/") },
proxy: [{ context: ["/api", "/img"], target: backend, changeOrigin: true }],
};
}
return {
devServer,
entry: {
launchevent: "./src/launchevent/launchevent.ts",
taskpane: "./src/taskpane/taskpane.ts",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
clean: true,
},
resolve: { extensions: [".ts", ".js"] },
module: { rules: [{ test: /\.ts$/, use: "ts-loader", exclude: /node_modules/ }] },
devtool: argv.mode === "production" ? false : "source-map",
performance: { hints: false },
plugins: [
new webpack.DefinePlugin({ __ADDIN_CONFIG__: JSON.stringify(config) }),
new HtmlWebpackPlugin({
filename: "commands.html",
template: "./src/launchevent/commands.html",
chunks: ["launchevent"],
scriptLoading: "blocking",
}),
new HtmlWebpackPlugin({
filename: "taskpane.html",
template: "./src/taskpane/taskpane.html",
chunks: ["taskpane"],
}),
new CopyWebpackPlugin({
patterns: [
{ from: "assets", to: "assets" },
{
from: "manifest.xml",
to: "manifest.xml",
transform: (content) =>
content
.toString()
.replace(/\{\{BASE_URL\}\}/g, baseUrl)
.replace(/\{\{ORIGIN\}\}/g, origin)
.replace(/\{\{SUPPORT_URL\}\}/g, e.SUPPORT_URL || `${origin}/aide`),
},
],
}),
],
};
};