diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 6ce6b6d57..03f7d6dae 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -7,6 +7,7 @@ use App\Models\TeamInvitation; use App\Models\User; use App\Providers\RouteServiceProvider; use Illuminate\Auth\Events\Verified; +use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\Request; @@ -98,23 +99,39 @@ class Controller extends BaseController { $token = request()->get('token'); if ($token) { - $decrypted = Crypt::decryptString($token); - $email = str($decrypted)->before('@@@'); - $password = str($decrypted)->after('@@@'); + try { + $decrypted = Crypt::decryptString($token); + } catch (DecryptException) { + return redirect()->route('login')->with('error', 'Invalid credentials.'); + } + + if (! str_contains($decrypted, '@@@')) { + return redirect()->route('login')->with('error', 'Invalid credentials.'); + } + + [$email, $password] = explode('@@@', $decrypted, 2); + $email = Str::lower($email); $user = User::whereEmail($email)->first(); if (! $user) { return redirect()->route('login'); } + + $invitation = TeamInvitation::whereEmail($email)->first(); + if (! $invitation || ! $invitation->isValid()) { + return redirect()->route('login')->with('error', 'Invitation has expired or been revoked.'); + } + if (Hash::check($password, $user->password)) { - $invitation = TeamInvitation::whereEmail($email); - if ($invitation->exists()) { - $team = $invitation->first()->team; - $user->teams()->attach($team->id, ['role' => $invitation->first()->role]); - $invitation->delete(); - } else { - $team = $user->teams()->first(); + $team = $invitation->team; + if (! $user->teams()->where('team_id', $team->id)->exists()) { + $user->teams()->attach($team->id, ['role' => $invitation->role]); } + $invitation->delete(); + Auth::login($user); + $user->forceFill([ + 'password' => Hash::make(Str::random(64)), + ])->save(); session(['currentTeam' => $team]); return redirect()->route('dashboard'); diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index d1fb5e387..92685d8bf 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1620,7 +1620,7 @@ "garage": { "documentation": "https://garagehq.deuxfleurs.fr/documentation/?utm_source=coolify.io", "slogan": "Garage is an S3-compatible distributed object storage service designed for self-hosting.", - "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF8zMl9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=", + "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF82NF9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=", "tags": [ "object", "storage", @@ -1666,7 +1666,7 @@ "gitea-runner": { "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", "slogan": "Gitea Actions runner for docker", - "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC42JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC43JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", "tags": [ "gitea", "actions", @@ -2007,6 +2007,24 @@ "minversion": "0.0.0", "port": "80" }, + "healthchecks": { + "documentation": "https://healthchecks.io/docs?utm_source=coolify.io", + "slogan": "Healthchecks is a cron job monitoring service. It listens for HTTP requests and email messages from your cron jobs and scheduled tasks.", + "compose": "c2VydmljZXM6CiAgaGVhbHRoY2hlY2tzOgogICAgaW1hZ2U6ICdoZWFsdGhjaGVja3MvaGVhbHRoY2hlY2tzOnY0LjInCiAgICBjb250YWluZXJfbmFtZTogaGVhbHRoY2hlY2tzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9IRUFMVEhDSEVDS1NfODAwMAogICAgICAtIERCPXNxbGl0ZQogICAgICAtIERCX05BTUU9L2RhdGEvaGMuc3FsaXRlCiAgICAgIC0gJ0FMTE9XRURfSE9TVFM9JHtBTExPV0VEX0hPU1RTOi0qfScKICAgICAgLSAnUkVHSVNUUkFUSU9OX09QRU49JHtSRUdJU1RSQVRJT05fT1BFTjotVHJ1ZX0nCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LUZhbHNlfScKICAgICAgLSAnU0VDUkVUX0tFWT0ke1NFQ1JFVF9LRVk6PyR7U0VSVklDRV9QQVNTV09SRF82NF9IRUFMVEhDSEVDS1N9fScKICAgICAgLSAnU0lURV9ST09UPSR7U0VSVklDRV9VUkxfSEVBTFRIQ0hFQ0tTfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi1maXhtZS1lbWFpbC1hZGRyZXNzLWhlcmV9JwogICAgICAtICdFTUFJTF9IT1NUPSR7RU1BSUxfSE9TVDotbXktc210cC1zZXJ2ZXItaGVyZS5jb219JwogICAgICAtICdFTUFJTF9QT1JUPSR7RU1BSUxfUE9SVDotNDY1fScKICAgICAgLSAnRU1BSUxfSE9TVF9VU0VSPSR7RU1BSUxfSE9TVF9VU0VSOi1teV91c2VybmFtZX0nCiAgICAgIC0gJ0VNQUlMX0hPU1RfUEFTU1dPUkQ9JHtFTUFJTF9IT1NUX1BBU1NXT1JEOi1teXBhc3N3b3JkfScKICAgICAgLSAnRU1BSUxfVVNFX1NTTD0ke0VNQUlMX1VTRV9TU0w6LVRydWV9JwogICAgICAtICdFTUFJTF9VU0VfVExTPSR7RU1BSUxfVVNFX1RMUzotRmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnaGVhbHRoY2hlY2tzLWRhdGE6L2RhdGEnCg==", + "tags": [ + "monitoring", + "status", + "performance", + "web", + "services", + "applications", + "real-time" + ], + "category": "monitoring", + "logo": "svgs/healthchecks.webp", + "minversion": "0.0.0", + "port": "80000" + }, "heimdall": { "documentation": "https://github.com/linuxserver/Heimdall?utm_source=coolify.io", "slogan": "Heimdall is a dashboard for managing and organizing your server applications.", diff --git a/templates/service-templates.json b/templates/service-templates.json index 699b97e55..26aed5826 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1620,7 +1620,7 @@ "garage": { "documentation": "https://garagehq.deuxfleurs.fr/documentation/?utm_source=coolify.io", "slogan": "Garage is an S3-compatible distributed object storage service designed for self-hosting.", - "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF8zMl9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=", + "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF82NF9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=", "tags": [ "object", "storage", @@ -1666,7 +1666,7 @@ "gitea-runner": { "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", "slogan": "Gitea Actions runner for docker", - "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC42JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC43JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", "tags": [ "gitea", "actions", @@ -2007,6 +2007,24 @@ "minversion": "0.0.0", "port": "80" }, + "healthchecks": { + "documentation": "https://healthchecks.io/docs?utm_source=coolify.io", + "slogan": "Healthchecks is a cron job monitoring service. It listens for HTTP requests and email messages from your cron jobs and scheduled tasks.", + "compose": "c2VydmljZXM6CiAgaGVhbHRoY2hlY2tzOgogICAgaW1hZ2U6ICdoZWFsdGhjaGVja3MvaGVhbHRoY2hlY2tzOnY0LjInCiAgICBjb250YWluZXJfbmFtZTogaGVhbHRoY2hlY2tzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fSEVBTFRIQ0hFQ0tTXzgwMDAKICAgICAgLSBEQj1zcWxpdGUKICAgICAgLSBEQl9OQU1FPS9kYXRhL2hjLnNxbGl0ZQogICAgICAtICdBTExPV0VEX0hPU1RTPSR7QUxMT1dFRF9IT1NUUzotKn0nCiAgICAgIC0gJ1JFR0lTVFJBVElPTl9PUEVOPSR7UkVHSVNUUkFUSU9OX09QRU46LVRydWV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi1GYWxzZX0nCiAgICAgIC0gJ1NFQ1JFVF9LRVk9JHtTRUNSRVRfS0VZOj8ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfSEVBTFRIQ0hFQ0tTfX0nCiAgICAgIC0gJ1NJVEVfUk9PVD0ke1NFUlZJQ0VfRlFETl9IRUFMVEhDSEVDS1N9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LWZpeG1lLWVtYWlsLWFkZHJlc3MtaGVyZX0nCiAgICAgIC0gJ0VNQUlMX0hPU1Q9JHtFTUFJTF9IT1NUOi1teS1zbXRwLXNlcnZlci1oZXJlLmNvbX0nCiAgICAgIC0gJ0VNQUlMX1BPUlQ9JHtFTUFJTF9QT1JUOi00NjV9JwogICAgICAtICdFTUFJTF9IT1NUX1VTRVI9JHtFTUFJTF9IT1NUX1VTRVI6LW15X3VzZXJuYW1lfScKICAgICAgLSAnRU1BSUxfSE9TVF9QQVNTV09SRD0ke0VNQUlMX0hPU1RfUEFTU1dPUkQ6LW15cGFzc3dvcmR9JwogICAgICAtICdFTUFJTF9VU0VfU1NMPSR7RU1BSUxfVVNFX1NTTDotVHJ1ZX0nCiAgICAgIC0gJ0VNQUlMX1VTRV9UTFM9JHtFTUFJTF9VU0VfVExTOi1GYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICdoZWFsdGhjaGVja3MtZGF0YTovZGF0YScK", + "tags": [ + "monitoring", + "status", + "performance", + "web", + "services", + "applications", + "real-time" + ], + "category": "monitoring", + "logo": "svgs/healthchecks.webp", + "minversion": "0.0.0", + "port": "80000" + }, "heimdall": { "documentation": "https://github.com/linuxserver/Heimdall?utm_source=coolify.io", "slogan": "Heimdall is a dashboard for managing and organizing your server applications.", diff --git a/tests/Feature/InvitationLinkHandlingTest.php b/tests/Feature/InvitationLinkHandlingTest.php new file mode 100644 index 000000000..4668a73b6 --- /dev/null +++ b/tests/Feature/InvitationLinkHandlingTest.php @@ -0,0 +1,109 @@ +withoutMiddleware([DecideWhatToDoWithUser::class, CheckForcePasswordReset::class]); + Once::flush(); + Config::set('app.maintenance.driver', 'file'); + Config::set('cache.default', 'array'); + Config::set('session.driver', 'array'); + + if (! InstanceSettings::find(0)) { + $settings = new InstanceSettings; + $settings->id = 0; + $settings->saveQuietly(); + } +}); + +function createInvitationLinkFixture(array $invitationAttributes = []): array +{ + $team = Team::factory()->create(); + $password = 'temporary-password-123'; + $user = User::factory()->create([ + 'email' => $invitationAttributes['email'] ?? 'invitee@example.com', + 'password' => Hash::make($password), + 'force_password_reset' => true, + 'email_verified_at' => null, + ]); + $token = Crypt::encryptString("{$user->email}@@@{$password}"); + $link = route('auth.link', ['token' => $token]); + + $invitation = TeamInvitation::create(array_merge([ + 'team_id' => $team->id, + 'uuid' => (string) new Cuid2(32), + 'email' => $user->email, + 'role' => 'member', + 'link' => $link, + 'via' => 'link', + ], $invitationAttributes)); + + return [$team, $user, $password, $token, $invitation]; +} + +it('accepts a valid magic link invitation only once and rotates the temporary password', function () { + [$team, $user, $password, $token] = createInvitationLinkFixture(); + + $this->get(route('auth.link', ['token' => $token])) + ->assertRedirect(route('dashboard')); + + $this->assertAuthenticatedAs($user); + $this->assertDatabaseMissing('team_invitations', ['email' => $user->email]); + expect($user->teams()->where('team_id', $team->id)->exists())->toBeTrue(); + + $user->refresh(); + expect(Hash::check($password, $user->password))->toBeFalse(); + + auth()->logout(); + session()->flush(); + + $this->get(route('auth.link', ['token' => $token])) + ->assertRedirect(route('login')); + + $this->assertGuest(); +}); + +it('rejects a magic link when the invitation was revoked', function () { + [, $user, , $token, $invitation] = createInvitationLinkFixture(); + $invitation->delete(); + + $this->get(route('auth.link', ['token' => $token])) + ->assertRedirect(route('login')); + + $this->assertGuest(); + expect($user->teams()->where('personal_team', false)->exists())->toBeFalse(); +}); + +it('rejects a magic link when the invitation expired', function () { + [, $user, , $token, $invitation] = createInvitationLinkFixture(); + $invitation->forceFill([ + 'created_at' => now()->subDays(config('constants.invitation.link.expiration_days') + 1), + 'updated_at' => now()->subDays(config('constants.invitation.link.expiration_days') + 1), + ])->save(); + + $this->get(route('auth.link', ['token' => $token])) + ->assertRedirect(route('login')); + + $this->assertGuest(); + $this->assertDatabaseMissing('team_invitations', ['id' => $invitation->id]); +}); + +it('rejects a malformed magic link token', function () { + $this->get(route('auth.link', ['token' => 'not-a-valid-token'])) + ->assertRedirect(route('login')); + + $this->assertGuest(); +}); diff --git a/tests/Feature/LinkLoginEmailVerificationTest.php b/tests/Feature/LinkLoginEmailVerificationTest.php index 036584e1e..1e3e32f6d 100644 --- a/tests/Feature/LinkLoginEmailVerificationTest.php +++ b/tests/Feature/LinkLoginEmailVerificationTest.php @@ -4,8 +4,10 @@ use App\Http\Middleware\CheckForcePasswordReset; use App\Http\Middleware\DecideWhatToDoWithUser; use App\Models\InstanceSettings; use App\Models\Team; +use App\Models\TeamInvitation; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Once; @@ -15,6 +17,10 @@ uses(RefreshDatabase::class); beforeEach(function () { $this->withoutMiddleware([DecideWhatToDoWithUser::class, CheckForcePasswordReset::class]); Once::flush(); + Config::set('app.maintenance.driver', 'file'); + Config::set('cache.default', 'array'); + Config::set('session.driver', 'array'); + if (! InstanceSettings::find(0)) { $settings = new InstanceSettings; $settings->id = 0; @@ -34,6 +40,14 @@ describe('invitation link login', function () { $user->teams()->attach($team->id, ['role' => 'member']); $token = Crypt::encryptString("{$user->email}@@@{$password}"); + TeamInvitation::create([ + 'team_id' => $team->id, + 'uuid' => 'email-verification-test-invitation', + 'email' => $user->email, + 'role' => 'member', + 'link' => route('auth.link', ['token' => $token]), + 'via' => 'link', + ]); $this->get(route('auth.link', ['token' => $token])); @@ -52,8 +66,17 @@ describe('invitation link login', function () { $user->teams()->attach($team->id, ['role' => 'member']); $token = Crypt::encryptString("{$user->email}@@@{$password}"); + TeamInvitation::create([ + 'team_id' => $team->id, + 'uuid' => 'email-verification-login-test-invitation', + 'email' => $user->email, + 'role' => 'member', + 'link' => route('auth.link', ['token' => $token]), + 'via' => 'link', + ]); - $this->get(route('auth.link', ['token' => $token])); + $this->get(route('auth.link', ['token' => $token])) + ->assertRedirect(route('dashboard')); expect(auth()->id())->toBe($user->id); });