Repository: safeinsights/management-app
Pull request #103: Add mailgun and welcome email
Author: login: chrisbendel
Created: 2025-03-26T20:20:30Z | Merged: 2025-03-27T16:05:29Z



Diff:
diff --git a/.env.sample b/.env.sample
index 00d572e88..567ca8ba5 100644
--- a/.env.sample
+++ b/.env.sample
@@ -8,3 +8,6 @@ ALWAYS_CREATE_ECR=f
 SIMULATE_IMAGE_BUILD=t
 USE_S3_STORAGE=f
 DEBUG=none
+MAILGUN_DOMAIN=YOUR_DOMAIN
+MAILGUN_API_KEY=YOUR_KEY
+DOMAIN_NAME=localhost:4000
diff --git a/package-lock.json b/package-lock.json
index 0e2a01922..d458b9d19 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,9 +42,10 @@
                 "jsonwebtoken": "^9.0.2",
                 "kysely": "^0.27.5",
                 "kysely-ctl": "^0.11.1",
+                "mailgun.js": "^12.0.1",
                 "mantine-datatable": "^7.15.1",
                 "mantine-form-zod-resolver": "^1.1.0",
-                "next": "15.2.2",
+                "next": "^15.2.4",
                 "next-swagger-doc": "^0.4",
                 "papaparse": "^5.5.2",
                 "pg": "^8.13.3",
@@ -3595,9 +3596,9 @@
             }
         },
         "node_modules/@next/env": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.2.tgz",
-            "integrity": "sha512-yWgopCfA9XDR8ZH3taB5nRKtKJ1Q5fYsTOuYkzIIoS8TJ0UAUKAGF73JnGszbjk2ufAQDj6mDdgsJAFx5CLtYQ==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz",
+            "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==",
             "license": "MIT"
         },
         "node_modules/@next/eslint-plugin-next": {
@@ -3611,9 +3612,9 @@
             }
         },
         "node_modules/@next/swc-darwin-arm64": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz",
-            "integrity": "sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz",
+            "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==",
             "cpu": [
                 "arm64"
             ],
@@ -3627,9 +3628,9 @@
             }
         },
         "node_modules/@next/swc-darwin-x64": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz",
-            "integrity": "sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz",
+            "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==",
             "cpu": [
                 "x64"
             ],
@@ -3643,9 +3644,9 @@
             }
         },
         "node_modules/@next/swc-linux-arm64-gnu": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz",
-            "integrity": "sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz",
+            "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==",
             "cpu": [
                 "arm64"
             ],
@@ -3659,9 +3660,9 @@
             }
         },
         "node_modules/@next/swc-linux-arm64-musl": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz",
-            "integrity": "sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz",
+            "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==",
             "cpu": [
                 "arm64"
             ],
@@ -3675,9 +3676,9 @@
             }
         },
         "node_modules/@next/swc-linux-x64-gnu": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz",
-            "integrity": "sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz",
+            "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==",
             "cpu": [
                 "x64"
             ],
@@ -3691,9 +3692,9 @@
             }
         },
         "node_modules/@next/swc-linux-x64-musl": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz",
-            "integrity": "sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz",
+            "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==",
             "cpu": [
                 "x64"
             ],
@@ -3707,9 +3708,9 @@
             }
         },
         "node_modules/@next/swc-win32-arm64-msvc": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz",
-            "integrity": "sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz",
+            "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==",
             "cpu": [
                 "arm64"
             ],
@@ -3723,9 +3724,9 @@
             }
         },
         "node_modules/@next/swc-win32-x64-msvc": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.2.tgz",
-            "integrity": "sha512-52nWy65S/R6/kejz3jpvHAjZDPKIbEQu4x9jDBzmB9jJfuOy5rspjKu4u77+fI4M/WzLXrrQd57hlFGzz1ubcQ==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz",
+            "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==",
             "cpu": [
                 "x64"
             ],
@@ -8008,6 +8009,12 @@
                 "node": ">= 0.4"
             }
         },
+        "node_modules/asynckit": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+            "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+            "license": "MIT"
+        },
         "node_modules/available-typed-arrays": {
             "version": "1.0.7",
             "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -8114,6 +8121,17 @@
                 "node": ">=4"
             }
         },
+        "node_modules/axios": {
+            "version": "1.8.4",
+            "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
+            "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
+            "license": "MIT",
+            "dependencies": {
+                "follow-redirects": "^1.15.6",
+                "form-data": "^4.0.0",
+                "proxy-from-env": "^1.1.0"
+            }
+        },
         "node_modules/axobject-query": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -8130,6 +8148,12 @@
             "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
             "license": "MIT"
         },
+        "node_modules/base-64": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
+            "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
+            "license": "MIT"
+        },
         "node_modules/base64-js": {
             "version": "1.5.1",
             "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -8657,6 +8681,18 @@
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/combined-stream": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+            "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+            "license": "MIT",
+            "dependencies": {
+                "delayed-stream": "~1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
         "node_modules/commander": {
             "version": "13.1.0",
             "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
@@ -8969,6 +9005,15 @@
             "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
             "license": "MIT"
         },
+        "node_modules/delayed-stream": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+            "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
         "node_modules/delegates": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -9361,7 +9406,6 @@
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
             "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
-            "dev": true,
             "license": "MIT",
             "dependencies": {
                 "es-errors": "^1.3.0",
@@ -10184,6 +10228,26 @@
             "dev": true,
             "license": "ISC"
         },
+        "node_modules/follow-redirects": {
+            "version": "1.15.9",
+            "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+            "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+            "funding": [
+                {
+                    "type": "individual",
+                    "url": "https://github.com/sponsors/RubenVerborgh"
+                }
+            ],
+            "license": "MIT",
+            "engines": {
+                "node": ">=4.0"
+            },
+            "peerDependenciesMeta": {
+                "debug": {
+                    "optional": true
+                }
+            }
+        },
         "node_modules/for-each": {
             "version": "0.3.5",
             "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -10216,6 +10280,21 @@
                 "url": "https://github.com/sponsors/isaacs"
             }
         },
+        "node_modules/form-data": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+            "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+            "license": "MIT",
+            "dependencies": {
+                "asynckit": "^0.4.0",
+                "combined-stream": "^1.0.8",
+                "es-set-tostringtag": "^2.1.0",
+                "mime-types": "^2.1.12"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/forwarded-parse": {
             "version": "2.1.2",
             "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz",
@@ -12665,6 +12744,20 @@
                 "source-map-js": "^1.2.0"
             }
         },
+        "node_modules/mailgun.js": {
+            "version": "12.0.1",
+            "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-12.0.1.tgz",
+            "integrity": "sha512-BGIP8bewBxhmSKj79rAV3860IpvoORHOhza6MS8JGoaQUBi3Giz2P/ixqJmG5QamrFHdl+PDtirCWvjffNz6aw==",
+            "license": "MIT",
+            "dependencies": {
+                "axios": "^1.7.4",
+                "base-64": "^1.0.0",
+                "url-join": "^4.0.1"
+            },
+            "engines": {
+                "node": ">=18.0.0"
+            }
+        },
         "node_modules/make-dir": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -13066,12 +13159,12 @@
             "peer": true
         },
         "node_modules/next": {
-            "version": "15.2.2",
-            "resolved": "https://registry.npmjs.org/next/-/next-15.2.2.tgz",
-            "integrity": "sha512-dgp8Kcx5XZRjMw2KNwBtUzhngRaURPioxoNIVl5BOyJbhi9CUgEtKDO7fx5wh8Z8vOVX1nYZ9meawJoRrlASYA==",
+            "version": "15.2.4",
+            "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz",
+            "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==",
             "license": "MIT",
             "dependencies": {
-                "@next/env": "15.2.2",
+                "@next/env": "15.2.4",
                 "@swc/counter": "0.1.3",
                 "@swc/helpers": "0.5.15",
                 "busboy": "1.6.0",
@@ -13086,14 +13179,14 @@
                 "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
             },
             "optionalDependencies": {
-                "@next/swc-darwin-arm64": "15.2.2",
-                "@next/swc-darwin-x64": "15.2.2",
-                "@next/swc-linux-arm64-gnu": "15.2.2",
-                "@next/swc-linux-arm64-musl": "15.2.2",
-                "@next/swc-linux-x64-gnu": "15.2.2",
-                "@next/swc-linux-x64-musl": "15.2.2",
-                "@next/swc-win32-arm64-msvc": "15.2.2",
-                "@next/swc-win32-x64-msvc": "15.2.2",
+                "@next/swc-darwin-arm64": "15.2.4",
+                "@next/swc-darwin-x64": "15.2.4",
+                "@next/swc-linux-arm64-gnu": "15.2.4",
+                "@next/swc-linux-arm64-musl": "15.2.4",
+                "@next/swc-linux-x64-gnu": "15.2.4",
+                "@next/swc-linux-x64-musl": "15.2.4",
+                "@next/swc-win32-arm64-msvc": "15.2.4",
+                "@next/swc-win32-x64-msvc": "15.2.4",
                 "sharp": "^0.33.5"
             },
             "peerDependencies": {
@@ -17127,6 +17220,12 @@
                 "querystring": "0.2.0"
             }
         },
+        "node_modules/url-join": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+            "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+            "license": "MIT"
+        },
         "node_modules/url/node_modules/punycode": {
             "version": "1.3.2",
             "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
@@ -17293,9 +17392,9 @@
             }
         },
         "node_modules/vite": {
-            "version": "6.2.2",
-            "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
-            "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
+            "version": "6.2.3",
+            "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
+            "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
diff --git a/package.json b/package.json
index 451d18e4b..a96cdb75a 100644
--- a/package.json
+++ b/package.json
@@ -65,9 +65,10 @@
         "jsonwebtoken": "^9.0.2",
         "kysely": "^0.27.5",
         "kysely-ctl": "^0.11.1",
+        "mailgun.js": "^12.0.1",
         "mantine-datatable": "^7.15.1",
         "mantine-form-zod-resolver": "^1.1.0",
-        "next": "15.2.2",
+        "next": "^15.2.4",
         "next-swagger-doc": "^0.4",
         "papaparse": "^5.5.2",
         "pg": "^8.13.3",
diff --git a/src/app/admin/invite/admin-users.actions.test.ts b/src/app/admin/invite/admin-users.actions.test.ts
index 03bcf7400..c2c9ff75f 100644
--- a/src/app/admin/invite/admin-users.actions.test.ts
+++ b/src/app/admin/invite/admin-users.actions.test.ts
@@ -6,6 +6,11 @@ import { adminInviteUserAction } from './admin-users.actions'
 import { faker } from '@faker-js/faker'
 import { randomString } from 'remeda'
 import * as clerk from '@clerk/nextjs/server'
+import { sendWelcomeEmail } from '@/lib/mailgun'
+
+vi.mock('@/lib/mailgun', () => ({
+    sendWelcomeEmail: vi.fn(),
+}))
 
 describe('invite user Actions', async () => {
     beforeEach(() => {
@@ -14,6 +19,7 @@ describe('invite user Actions', async () => {
             org_slug: CLERK_ADMIN_ORG_SLUG,
         })
     })
+
     async function userRecordCount() {
         const c = await db
             .selectFrom('user')
@@ -63,6 +69,8 @@ describe('invite user Actions', async () => {
             .where('memberId', '=', userInvite.organizationId)
             .executeTakeFirst()
         expect(memberUser).toBeTruthy()
+
+        expect(sendWelcomeEmail).toHaveBeenCalledWith(user.email, `${user.firstName} ${user.lastName}`)
     })
 
     it('throws an error when user insert fails', async () => {
diff --git a/src/app/admin/invite/admin-users.actions.ts b/src/app/admin/invite/admin-users.actions.ts
index 7fc535d31..bdd7cad40 100644
--- a/src/app/admin/invite/admin-users.actions.ts
+++ b/src/app/admin/invite/admin-users.actions.ts
@@ -4,6 +4,7 @@ import { db } from '@/database'
 import { clerkClient } from '@clerk/nextjs/server'
 import { inviteUserSchema } from './admin-users.schema'
 import { adminAction } from '@/server/actions/wrappers'
+import { sendWelcomeEmail } from '@/lib/mailgun'
 
 export const adminInviteUserAction = adminAction(async (invite) => {
     const client = await clerkClient()
@@ -49,6 +50,9 @@ export const adminInviteUserAction = adminAction(async (invite) => {
             .returning('id')
             .executeTakeFirstOrThrow()
 
+        const fullName = `${invite.firstName} ${invite.lastName}`
+        await sendWelcomeEmail(invite.email, fullName)
+
         // Return the created user record.
         return {
             ...invite,
diff --git a/src/app/admin/invite/invite-form.tsx b/src/app/admin/invite/invite-form.tsx
index eff6ed5d3..025e60887 100644
--- a/src/app/admin/invite/invite-form.tsx
+++ b/src/app/admin/invite/invite-form.tsx
@@ -29,7 +29,6 @@ export function InviteForm() {
 
     const { mutate: inviteUser, isPending } = useMutation({
         mutationFn: adminInviteUserAction,
-
         onSettled(result, error) {
             if (error) {
                 reportError(error)
diff --git a/src/app/member/[memberIdentifier]/dashboard/page.tsx b/src/app/member/[memberIdentifier]/dashboard/page.tsx
index 27a8921a2..3438d2358 100644
--- a/src/app/member/[memberIdentifier]/dashboard/page.tsx
+++ b/src/app/member/[memberIdentifier]/dashboard/page.tsx
@@ -2,9 +2,7 @@
 
 import React from 'react'
 import { AlertNotFound } from '@/components/errors'
-
 import { getMemberFromIdentifierAction } from '@/server/actions/member.actions'
-
 import { Divider, Stack, Text, Title } from '@mantine/core'
 import { StudiesTable } from '@/app/member/[memberIdentifier]/dashboard/studies-table'
 import { siUser } from '@/server/db/queries'
diff --git a/src/lib/date.ts b/src/lib/date.ts
deleted file mode 100644
index 88272616f..000000000
--- a/src/lib/date.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import localizedFormat from 'dayjs/plugin/localizedFormat'
-import dayjs from 'dayjs'
-dayjs.extend(localizedFormat)
-
-export { dayjs }
-
-export function formatDate(date: dayjs.ConfigType) {
-    return date ? dayjs(date).format('L LT') : ''
-}
diff --git a/src/lib/mailgun.ts b/src/lib/mailgun.ts
new file mode 100644
index 000000000..5820a4f44
--- /dev/null
+++ b/src/lib/mailgun.ts
@@ -0,0 +1,53 @@
+import Mailgun from 'mailgun.js'
+import logger from '@/lib/logger'
+import { DEV_ENV, TEST_ENV } from '@/server/config'
+
+export const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY || ''
+if (!MAILGUN_API_KEY) {
+    if (DEV_ENV || TEST_ENV) {
+        console.warn('Mailgun API key not defined. Emails will not be sent in this environment.')
+    } else {
+        throw new Error('Mailgun API key must be defined')
+    }
+}
+
+export const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN || ''
+if (!MAILGUN_DOMAIN) {
+    if (DEV_ENV || TEST_ENV) {
+        console.warn('Mailgun domain not defined. Emails will not be sent in this environment.')
+    } else {
+        throw new Error('Mailgun domain must be defined')
+    }
+}
+
+const SI_EMAIL = 'Safeinsights <no-reply@safeinsights.org>'
+const mailgun = new Mailgun(FormData)
+
+export const mg = MAILGUN_API_KEY
+    ? mailgun.client({
+          username: 'api',
+          key: MAILGUN_API_KEY,
+      })
+    : null
+
+export const sendWelcomeEmail = async (emailTo: string, fullName: string) => {
+    if (!mg) {
+        console.warn('Mailgun client is not initialized. Skipping email sending.')
+        return
+    }
+
+    try {
+        await mg.messages.create(MAILGUN_DOMAIN, {
+            from: SI_EMAIL,
+            to: [emailTo],
+            subject: 'Get started with SafeInsights',
+            template: 'welcome email',
+            'h:X-Mailgun-Variables': JSON.stringify({
+                fullName,
+                resetPasswordLink: `https://${process.env.DOMAIN_NAME}/account/reset-password`,
+            }),
+        })
+    } catch (error) {
+        logger.error.log('Welcome email error: ', error)
+    }
+}
diff --git a/src/lib/roles.ts b/src/lib/roles.ts
deleted file mode 100644
index e69de29bb..000000000


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.

<details>
<summary>Files not reviewed (2)</summary>

* **.env.sample**: Language not supported
* **package.json**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/lib/mailgun.ts:41**
* The DOMAIN_NAME environment variable is used in constructing the resetPasswordLink without validation. It is recommended to verify that DOMAIN_NAME is set to avoid potential runtime errors.
```
resetPasswordLink: `https://${process.env.DOMAIN_NAME}/account/reset-password`,
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T12:52:27Z
user: login: nathanstitt
body: looks good, but check if the built-in FormData works for mg
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T14:53:05Z

Review comments:
diff_hunk: @@ -0,0 +1,47 @@
+import FormData from 'form-data'
+import Mailgun from 'mailgun.js'
+import logger from '@/lib/logger'
+import { DEV_ENV, TEST_ENV } from '@/server/config'
+
+const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY || ''
+if (!MAILGUN_API_KEY) {
+    if (DEV_ENV || TEST_ENV) {
+        console.warn('Mailgun API key not defined. Emails will not be sent in this environment.')
+    } else {
+        throw new Error('Mailgun API key must be defined')
+    }
+}
+
+const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN || ''
+
path: src/lib/mailgun.ts
user: login: Copilot
body: The MAILGUN_DOMAIN variable is not explicitly validated, which may lead to issues in production if it remains empty. Consider adding a validation similar to MAILGUN_API_KEY to ensure it's properly configured.
```suggestion
const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN || ''
if (!MAILGUN_DOMAIN) {
    if (DEV_ENV || TEST_ENV) {
        console.warn('Mailgun domain not defined. Emails will not be sent in this environment.')
    } else {
        throw new Error('Mailgun domain must be defined')
    }
}
```
created_at: 2025-03-27T12:52:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,13 +61,15 @@
         "clsx": "^2.1.1",
         "dayjs": "^1.11.13",
         "debug": "^4.4.0",
+        "form-data": "^4.0.2",
path: package.json
user: login: nathanstitt
body: are you sure we need an external lib?  nextJS (or maybe it's recent Node?) has built-in support for `FormData`, I see we're using it on the `src/app/api/job/[jobId]/results/route.test.ts` 
created_at: 2025-03-27T14:50:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,47 @@
+import FormData from 'form-data'
+import Mailgun from 'mailgun.js'
+import logger from '@/lib/logger'
+import { DEV_ENV, TEST_ENV } from '@/server/config'
+
+const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY || ''
+if (!MAILGUN_API_KEY) {
+    if (DEV_ENV || TEST_ENV) {
+        console.warn('Mailgun API key not defined. Emails will not be sent in this environment.')
+    } else {
+        throw new Error('Mailgun API key must be defined')
+    }
+}
+
+const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN || ''
+
path: src/lib/mailgun.ts
user: login: nathanstitt
body: I like this suggestion, but think we can add it all to a `mailGunDomain` in `server/config.ts`
created_at: 2025-03-27T14:52:32Z
author_association: MEMBER

PR comments:
user: login: nathanstitt
created_at: 2025-03-27T12:36:20Z
body: ### BUILD STATUS ❌



npm run build:lambda failed with status 1\n
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-27T16:03:39Z
body: 
### Total coverage

| Lines           | Branches           | Functions           | Statements           |
| --------------- | ------------------ | ------------------- | -------------------- |
| 83.30% (+0.11%) | 57.20% (+0.22%) | 75.00% (+0.29%) | 81.87% (+0.11%) | 


#### Detailed report

<details><summary>click to open the diff coverage report</summary>

|   | File | Lines | Branches | Functions | Statements |
| - | ---- | ----- | -------- | --------- | ---------- |
| ✅ | src/app/admin/invite/admin-users.actions.ts | 93.75% (+1.45%) | 50.00% | 100.00% | 94.11% (+1.26%) |,
| ✅ | src/app/member/[memberIdentifier]/study/[studyIdentifier]/review/job-review-buttons.tsx | 72.22% (+5.56%) | 33.33% (+16.67%) | 42.85% | 66.66% (+4.76%) |,
| ✅ | src/lib/paths.ts | 100.00% | 100.00% | 40.00% (+20.00%) | 100.00% |
</details>

<!-- This comment was produced by coverage-diff-action -->
author_association: CONTRIBUTOR