Compare commits

...

332 Commits

Author SHA1 Message Date
Shift
0eb1ce5727 Empty commit for Shift 2026-01-29 04:12:41 +00:00
James Collins
3218f55a31 Merge pull request #562 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-underline-3.18.0
Bump @tiptap/extension-underline from 3.17.1 to 3.18.0
2026-01-29 14:00:50 +10:00
dependabot[bot]
be1fa8f432 Bump @tiptap/extension-underline from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-underline](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-underline) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-underline/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-underline)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-underline"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 04:00:02 +00:00
James Collins
c0c572397a Merge pull request #561 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-link-3.18.0
Bump @tiptap/extension-link from 3.17.1 to 3.18.0
2026-01-29 13:59:10 +10:00
James Collins
049d4849aa Merge pull request #560 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-superscript-3.18.0
Bump @tiptap/extension-superscript from 3.17.1 to 3.18.0
2026-01-29 13:58:58 +10:00
James Collins
e89656367f Merge pull request #559 from STEMMechanics/dependabot/composer/phpunit/phpunit-12.5.8
Bump phpunit/phpunit from 10.5.62 to 12.5.8
2026-01-29 13:58:41 +10:00
dependabot[bot]
e910ef7e84 Bump phpunit/phpunit from 10.5.62 to 12.5.8
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.62 to 12.5.8.
- [Release notes](https://github.com/sebastianbergmann/phpunit/releases)
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/12.5.8/ChangeLog-12.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.62...12.5.8)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-version: 12.5.8
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 03:58:02 +00:00
dependabot[bot]
db0e927056 Bump @tiptap/extension-link from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-link) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-link/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-link)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 03:57:20 +00:00
James Collins
cec55028d6 Merge pull request #557 from STEMMechanics/dependabot/composer/livewire/livewire-4.1.0 2026-01-29 13:57:08 +10:00
James Collins
0ae09c40ae Merge pull request #556 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-highlight-3.18.0
Bump @tiptap/extension-highlight from 3.17.1 to 3.18.0
2026-01-29 13:56:18 +10:00
James Collins
1cf2ec1ce7 Merge pull request #555 from STEMMechanics/dependabot/composer/laravel/framework-12.49.0
Bump laravel/framework from 12.48.1 to 12.49.0
2026-01-29 13:56:06 +10:00
dependabot[bot]
693df3a05b Bump @tiptap/extension-superscript from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-superscript](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-superscript) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-superscript/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-superscript)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-superscript"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 00:43:35 +00:00
dependabot[bot]
c0d1913660 Bump livewire/livewire from 3.7.6 to 4.1.0
Bumps [livewire/livewire](https://github.com/livewire/livewire) from 3.7.6 to 4.1.0.
- [Release notes](https://github.com/livewire/livewire/releases)
- [Commits](https://github.com/livewire/livewire/compare/v3.7.6...v4.1.0)

---
updated-dependencies:
- dependency-name: livewire/livewire
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 00:43:20 +00:00
dependabot[bot]
8c387280a6 Bump @tiptap/extension-highlight from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-highlight](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-highlight) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-highlight/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-highlight)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-highlight"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 00:43:11 +00:00
dependabot[bot]
a1ab7b6257 Bump laravel/framework from 12.48.1 to 12.49.0
Bumps [laravel/framework](https://github.com/laravel/framework) from 12.48.1 to 12.49.0.
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/12.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/framework/compare/v12.48.1...v12.49.0)

---
updated-dependencies:
- dependency-name: laravel/framework
  dependency-version: 12.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 00:43:07 +00:00
James Collins
36d4f08aaf Merge pull request #554 from STEMMechanics/dependabot/composer/symfony/process-7.4.5
Bump symfony/process from 7.4.4 to 7.4.5
2026-01-29 07:50:12 +10:00
dependabot[bot]
f7de4f49d3 Bump symfony/process from 7.4.4 to 7.4.5
Bumps [symfony/process](https://github.com/symfony/process) from 7.4.4 to 7.4.5.
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/8.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v7.4.4...v7.4.5)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-version: 7.4.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 21:39:53 +00:00
James Collins
06018c405d Merge pull request #553 from STEMMechanics/dependabot/composer/phpunit/phpunit-10.5.62
Bump phpunit/phpunit from 10.5.61 to 10.5.62
2026-01-28 16:24:47 +10:00
dependabot[bot]
03c17200a0 Bump phpunit/phpunit from 10.5.61 to 10.5.62
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.61 to 10.5.62.
- [Release notes](https://github.com/sebastianbergmann/phpunit/releases)
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/10.5.62/ChangeLog-10.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.61...10.5.62)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-version: 10.5.62
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 04:44:31 +00:00
519c2a0fec update to php 8.4 2026-01-28 14:43:38 +10:00
James Collins
2b8788f716 Merge pull request #552 from STEMMechanics/dependabot/npm_and_yarn/axios-1.13.4
Bump axios from 1.13.3 to 1.13.4
2026-01-28 14:33:55 +10:00
dependabot[bot]
72be071536 Bump axios from 1.13.3 to 1.13.4
Bumps [axios](https://github.com/axios/axios) from 1.13.3 to 1.13.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.3...v1.13.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 00:43:12 +00:00
37b923fbda use important on hidden for placeholder 2026-01-27 20:30:01 +10:00
github-actions[bot]
292901a02e Dependency update 2026-01-26 20:19:20 +00:00
github-actions[bot]
12a6629a4b Dependency update 2026-01-19 20:17:13 +00:00
9d9a8ed9f5 exclude vendor in tests 2026-01-15 08:56:12 +10:00
1a03fed3bd added snyk test 2026-01-15 08:54:06 +10:00
43e66b2004 path traversal in chunk unlink fix 2026-01-15 08:33:38 +10:00
8babb4c836 added unlink safeguard 2026-01-15 08:28:41 +10:00
4eb3dfbb64 fix potential path traversal 2026-01-15 08:23:03 +10:00
33d390a612 fix open redirect 2026-01-15 08:01:05 +10:00
cad78c30ae added peer dependency 2026-01-15 07:57:10 +10:00
f8acdae237 fix path traversal risk 2026-01-15 07:56:32 +10:00
github-actions[bot]
63582dc306 Dependency update 2026-01-12 20:17:47 +00:00
github-actions[bot]
c96fb95f27 Dependency update 2026-01-05 20:18:40 +00:00
1160c8b077 holiday message 2026-01-01 09:11:28 +10:00
a4b3405f8a vite update 2026-01-01 09:06:16 +10:00
9d5396ca9a tiptap and vite update 2026-01-01 09:02:08 +10:00
de1483d409 tiptap update 2026-01-01 09:01:15 +10:00
James Collins
a37386565a Merge pull request #530 from STEMMechanics/dependabot/npm_and_yarn/laravel-vite-plugin-2.0.1
Bump laravel-vite-plugin from 1.3.0 to 2.0.1
2026-01-01 08:46:08 +10:00
dependabot[bot]
ff8b6549ce Bump laravel-vite-plugin from 1.3.0 to 2.0.1
Bumps [laravel-vite-plugin](https://github.com/laravel/vite-plugin) from 1.3.0 to 2.0.1.
- [Release notes](https://github.com/laravel/vite-plugin/releases)
- [Changelog](https://github.com/laravel/vite-plugin/blob/2.x/CHANGELOG.md)
- [Upgrade guide](https://github.com/laravel/vite-plugin/blob/2.x/UPGRADE.md)
- [Commits](https://github.com/laravel/vite-plugin/compare/v1.3.0...v2.0.1)

---
updated-dependencies:
- dependency-name: laravel-vite-plugin
  dependency-version: 2.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-31 22:18:17 +00:00
github-actions[bot]
d550bc60e2 Dependency update 2025-12-31 21:02:22 +00:00
James Collins
307573a3a8 Update workflow to commit changes directly
Replaced the Create PR step with a Commit and push step to directly commit changes to the main branch.
2026-01-01 07:01:35 +10:00
James Collins
d9fc6c95f3 Update PHP version in dependency update workflow 2026-01-01 06:58:23 +10:00
James Collins
a65f0eead6 Add workflow for automated dependency updates 2026-01-01 06:56:13 +10:00
ca025ca2e8 dependency updates 2025-12-23 10:39:55 +10:00
74aef68edc remove posts reference 2025-12-23 10:34:05 +10:00
c0e595f88a dependency updates 2025-11-27 10:15:26 +10:00
aab4bc0d46 dependency updates 2025-11-26 10:10:57 +10:00
f2708e1325 dependency updates 2025-11-19 10:32:18 +10:00
James Collins
2e2b70ab7c Merge pull request #533 from STEMMechanics/dependabot/npm_and_yarn/multi-74f7c1f85a
Bump esbuild and vite
2025-11-19 10:28:45 +10:00
dependabot[bot]
e42d54554a Bump esbuild and vite
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.12 and updates ancestor dependency [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Updates `esbuild` from 0.21.5 to 0.25.12
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.12)

Updates `vite` from 5.4.21 to 7.2.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.2.2/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.12
  dependency-type: indirect
- dependency-name: vite
  dependency-version: 7.2.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 00:25:53 +00:00
6cb24f1500 dependency updates 2025-11-19 10:24:23 +10:00
f7cc086f37 added jenkins link 2025-11-19 10:23:05 +10:00
8463da7842 opacity fixes 2025-11-17 09:26:48 +10:00
James Collins
322d547c92 Merge pull request #526 from STEMMechanics/laravel12
laravel 12 upgrade
2025-11-17 09:09:10 +10:00
30104ece71 laravel 12 upgrade 2025-11-17 09:08:07 +10:00
44f359ff9c fix timings 2025-11-16 23:15:49 +10:00
20f36d519a fix timings 2025-11-16 23:14:55 +10:00
e358e9fb5d fix timings 2025-11-16 23:13:28 +10:00
b882d92328 fix timings 2025-11-16 23:08:34 +10:00
3257aa9ee9 added bot checks 2025-11-16 23:04:50 +10:00
0bcd6f5e86 added bot checks 2025-11-16 23:00:21 +10:00
75d958856a rename controller 2025-11-16 22:59:07 +10:00
71eb00d010 unsubscribe fixes 2025-11-16 22:56:16 +10:00
eab3d062f5 unsubscribe fixes 2025-11-16 22:48:17 +10:00
1afa22e2f4 logging 2025-11-16 22:19:40 +10:00
b85d039c36 fix var name 2025-11-16 22:14:04 +10:00
c1a4fd13d5 fix var name 2025-11-16 22:04:01 +10:00
9a1ffe835c fix var name 2025-11-16 22:02:32 +10:00
c3b9482d35 obsolete directives 2025-11-16 22:00:14 +10:00
bc8f9149dc fix unsubscribe link 2025-11-16 21:57:41 +10:00
c60213257b force SSL 2025-11-16 21:41:14 +10:00
6a78ba2bb2 composer updates 2025-11-16 21:12:40 +10:00
a5f7ce8393 updated subscription elements 2025-11-16 21:10:34 +10:00
4e1505c5c2 updated subscription elements 2025-11-16 19:07:07 +10:00
e967bdde71 updated footer and added about page 2025-11-16 16:20:41 +10:00
74e9e39722 updated address 2025-11-16 16:02:34 +10:00
0df4033fca package updates 2025-11-16 15:41:11 +10:00
e02770cc85 added roave/security-advisories 2025-11-16 15:32:35 +10:00
3687af2656 remove blog posts 2025-11-16 15:31:29 +10:00
b168931266 upgraded packages 2025-11-10 16:46:10 +10:00
b669dd319e fixed bad left offset of backdrop in dropdown 2025-11-10 16:43:26 +10:00
e37b9a30a4 dependency updates 2025-08-28 20:17:42 +10:00
436d4b8acf update 2025-08-28 20:12:49 +10:00
a2eb1d5d1b search bar focus and select fix 2025-08-28 20:12:31 +10:00
be4fdb2f80 updated to handle local caching 2025-08-28 20:03:30 +10:00
538f324ff4 captcha cleanup and added 2fa logins 2024-09-28 11:51:28 +10:00
59ca73519d added instructions 2024-09-28 09:23:16 +10:00
6bc2b888a4 change timer 2024-09-28 09:18:43 +10:00
be8b2d48b3 update newsletter schedule 2024-09-27 22:38:11 +10:00
5f631a5c3d remove user data 2024-09-27 22:26:58 +10:00
fea3756eab fix bad checkbox variable 2024-09-27 22:26:25 +10:00
6d8db2cd80 fix bad variable name 2024-09-27 22:23:57 +10:00
9725f4944f fix bad variable name 2024-09-27 22:23:01 +10:00
9b1b92d0cf added email subscriptions 2024-09-27 22:17:39 +10:00
b10b6b712e added email subscriptions 2024-09-27 22:16:29 +10:00
db018e9120 fix invalid tag 2024-09-27 19:58:57 +10:00
1444bc9aa4 fallback if firstname is missing 2024-09-27 19:56:32 +10:00
9e7fc79fa1 add search option to navbar slide out 2024-09-27 18:08:25 +10:00
06460d9677 update home to shipping address 2024-09-27 18:04:12 +10:00
beed9f9c11 update home to shipping address 2024-09-27 17:59:27 +10:00
38b3d5d367 positioning updates 2024-09-27 17:30:00 +10:00
ad080b19a2 fix asset links 2024-09-27 14:47:59 +10:00
274d9759b6 fix small screen layouts 2024-09-27 14:26:46 +10:00
d992570ee8 fix number formatting 2024-09-27 14:22:18 +10:00
d72c08b4c9 fix selection 2024-09-27 14:22:03 +10:00
7baea36628 fix single decimal point pricing 2024-09-27 13:39:59 +10:00
b20c79b679 updated search and added past workshop page 2024-09-27 13:33:50 +10:00
5cbebd8840 remove 2024-09-27 11:26:24 +10:00
d36979cbbd updated 2024-09-27 11:25:49 +10:00
1c28cd7902 updated 2024-09-27 11:24:55 +10:00
df19e43112 update includes 2024-09-27 11:24:47 +10:00
5a65517d2b added past index route 2024-09-27 11:19:32 +10:00
49eb388041 updated mast to support tabs 2024-09-27 11:17:49 +10:00
659ae2e3ac bts update 2024-09-27 10:27:54 +10:00
8f8d12065d change workshop table to events 2024-05-07 15:00:32 +10:00
391b17c1e7 download and close buttons in gallery 2024-05-07 08:24:17 +10:00
742da4bf17 updated tokens and emails 2024-05-06 20:13:31 +10:00
39ea570f3a email still required to be unique 2024-05-06 10:46:38 +10:00
714a15e6d0 allow empty fields by admin 2024-05-06 10:45:49 +10:00
a5e4e93edb reject emails already in use 2024-05-06 10:45:38 +10:00
5f166deee9 remove unused variable 2024-05-04 18:08:45 +10:00
2468bff5fb no decimals for KB and bytes 2024-05-04 18:08:05 +10:00
277805044a keep gallery order 2024-05-04 17:25:20 +10:00
680be0535d keep gallery order 2024-05-04 17:21:07 +10:00
b438846c3c dependency updates 2024-05-04 14:44:52 +10:00
6a76dacdae fix overlapping pages on PDF thumbnail generation 2024-05-04 14:42:23 +10:00
3358cf8dea fix default name 2024-05-04 14:21:35 +10:00
7b6e17ba40 hide hidden field 2024-05-04 14:19:25 +10:00
3e891912b0 fix issues with scripts rendering 2024-05-04 14:18:17 +10:00
53d0c46aa0 updates 2024-05-04 13:49:44 +10:00
e4d5307dfe updates 2024-05-04 13:48:21 +10:00
28aebcfe58 set default publish to now 2024-05-04 13:42:22 +10:00
cdc5d1e8d3 allow multiple components on page 2024-05-04 13:39:55 +10:00
4c7dadfab0 dull border 2024-05-03 21:00:01 +10:00
4ac1322b8c post table responsive 2024-05-02 11:47:23 +10:00
822838fe29 locations table responsive 2024-05-02 11:42:58 +10:00
fb4c8c240b fix navbar z-index on search bar 2024-05-02 11:39:12 +10:00
30e308466b update title size based on length 2024-05-01 21:06:40 +10:00
e1dcc7452b support null file list 2024-05-01 21:01:49 +10:00
76690962c5 add workshop duplication 2024-05-01 20:57:20 +10:00
629ae22b78 add videos file list 2024-05-01 20:27:23 +10:00
1e1df33711 dont generate variants if it already exists 2024-05-01 18:49:50 +10:00
f835d4a21b support stopmotionstudiomobile extension 2024-05-01 18:40:30 +10:00
1219c9a02e added updating thumbnails 2024-05-01 18:39:33 +10:00
bbffddf9ae cleanup file type 2024-05-01 18:25:18 +10:00
74cc11e124 check element exists first 2024-05-01 18:14:49 +10:00
b020f5cbb6 remove file from form 2024-05-01 18:03:49 +10:00
09bd9a22c9 fix user creation validation 2024-05-01 17:43:34 +10:00
ebf97a8242 fix search data 2024-05-01 14:44:50 +10:00
6d51cc1395 CSS for ul 2024-05-01 12:09:58 +10:00
ededb36856 dependency updates 2024-05-01 07:11:19 +10:00
b7ee043bf3 reverse order in search 2024-04-30 21:44:42 +10:00
48131d3064 added shortcuts 2024-04-30 21:35:28 +10:00
d4f3e24c33 added shortcuts 2024-04-30 21:31:52 +10:00
0f253e1047 added cancelled status 2024-04-30 15:37:41 +10:00
4d2f6de3c8 added cancelled status 2024-04-30 15:36:19 +10:00
346c945937 delete temp files 2024-04-30 08:34:56 +10:00
4391abaabb progress uploads 2024-04-30 07:40:08 +10:00
50169e9905 responsive forms 2024-04-29 22:19:10 +10:00
7a6a3ec2d8 save workshop files in workshop updates 2024-04-29 22:14:13 +10:00
5d715b096f cleanup filelist display 2024-04-29 22:12:08 +10:00
c696a8bd2e refactor file uploading and add media picker errors 2024-04-29 22:00:34 +10:00
d3bf78d5a8 added empty button 2024-04-29 20:16:24 +10:00
cd08c9a5c8 correct spacing for narrow images 2024-04-28 18:44:06 +10:00
bd8f453aea bypass password for admin 2024-04-28 18:36:03 +10:00
c719da2933 string starts with fix 2024-04-28 18:28:53 +10:00
a5184be13d added filetype 2024-04-28 18:25:31 +10:00
47a2afff86 added filetype 2024-04-28 18:24:56 +10:00
b8db85abca added getFileTypeAttribute() 2024-04-28 18:23:36 +10:00
67df8f85ff generic thumbnail for password protected files 2024-04-28 18:15:02 +10:00
6bb45c38a9 generic thumbnail for password protected files 2024-04-28 18:12:12 +10:00
02dbacbdc0 incorrect filename 2024-04-28 18:04:09 +10:00
bea4f8db18 add cmd to regenerate thumbnails 2024-04-28 18:00:33 +10:00
3919a3ce1c expand on thumbnail variants 2024-04-28 17:55:09 +10:00
d1b94f9183 ensure there is a thumbnail variant before returning url 2024-04-28 17:32:01 +10:00
bb440497eb dependency updates 2024-04-28 17:27:00 +10:00
4b87c5e112 remove debug 2024-04-28 17:25:39 +10:00
a2def6abc0 added media status and retry loading thumbnails 2024-04-28 17:25:10 +10:00
a5be12aee3 media picker responsive 2024-04-28 17:06:09 +10:00
e2fed71896 add responsive table 2024-04-28 16:26:02 +10:00
befebc44cb fix image distortion 2024-04-27 21:51:32 +10:00
b4456d7771 menu floating on mobile 2024-04-27 21:51:23 +10:00
c3350823dc table cleanup 2024-04-25 20:31:09 +10:00
96ba9edf6a fix text wrapping in pagination 2024-04-25 19:57:10 +10:00
39609edc9e better file password implementation 2024-04-25 18:40:40 +10:00
c702253837 change button name 2024-04-25 17:54:07 +10:00
bad020924d support for passworded files 2024-04-25 17:48:47 +10:00
a57be26b75 incorrect email 2024-04-25 15:11:10 +10:00
1d63186fd5 bugfix 2024-04-25 14:50:06 +10:00
6e9d14728d remove time 2024-04-25 14:41:18 +10:00
7c9dab2cf0 fix 2024-04-25 14:40:23 +10:00
885a909e57 dont create user is some cases 2024-04-25 14:35:22 +10:00
a26b669daa record extra register details in log 2024-04-25 14:27:18 +10:00
3bd3b30609 record extra register details in log 2024-04-25 14:27:09 +10:00
1201a6f0e6 record extra details in honeypot 2024-04-25 14:21:34 +10:00
8a32fc41a8 record extra details in honeypot 2024-04-25 14:16:16 +10:00
6131b378b8 pass previous query values to paginate 2024-04-25 14:09:11 +10:00
2a80a1ad62 remove rules page 2024-04-24 23:30:09 +10:00
4e0e7f6b7b added missing pages 2024-04-24 23:25:47 +10:00
7bc62d5600 margin on small screens 2024-04-24 22:59:14 +10:00
6208676207 update selectors 2024-04-24 22:34:28 +10:00
70f9d736d1 add gap between buttons 2024-04-24 22:10:43 +10:00
09c49ab279 variable checking 2024-04-24 22:07:33 +10:00
6ac8fb4f89 variable checking 2024-04-24 22:05:57 +10:00
3a2735f00d further bot checking 2024-04-24 22:01:29 +10:00
7b89fad650 check value 2024-04-24 21:48:15 +10:00
9bc3e5e963 check $post 2024-04-24 21:48:09 +10:00
ea10ead824 added honeypot 2024-04-24 21:41:47 +10:00
4a4b42bed0 fix missing view 2024-04-24 21:01:02 +10:00
1053fbc797 fix missing view 2024-04-24 20:50:43 +10:00
988dbd4edc cleanup 2024-04-24 19:39:33 +10:00
4c9d1667b6 added 2024-04-24 18:07:17 +10:00
2b924fac4b removed from project 2024-04-24 18:06:38 +10:00
18e0a2afd2 don't include .idea 2024-04-24 18:04:43 +10:00
558a432960 dependency updates 2024-04-24 18:01:55 +10:00
f46dbd887b fix workshop order 2024-04-24 17:56:40 +10:00
50de666304 add responsive 2024-04-24 07:18:17 +10:00
d592f8bd19 bugfixes 2024-04-24 07:16:04 +10:00
d2b69061b5 added thumbnails and download link 2024-04-24 07:11:03 +10:00
b24fa48d85 only need lg images 2024-04-23 21:41:04 +10:00
28b2ffa4a3 only need lg images 2024-04-23 21:38:45 +10:00
4a45c0f505 add gzip and expires 2024-04-23 21:38:36 +10:00
f7e7ed9d7a use medium image size 2024-04-23 21:17:42 +10:00
232f737a10 fix up the sort order on the workshops page 2024-04-23 20:51:10 +10:00
feecd0d7f5 dont show private events on front page 2024-04-23 19:33:16 +10:00
2d872ae289 update display 2024-04-23 19:31:47 +10:00
d6b6cb49cf add private event support 2024-04-23 19:13:42 +10:00
735d39f52e fix responsive design 2024-04-23 19:02:39 +10:00
46faf195a7 add default age 2024-04-23 17:04:26 +10:00
caff5c8160 support price of 0 2024-04-23 16:54:44 +10:00
39d5b29ed3 add double click support to media picker 2024-04-23 16:47:08 +10:00
b4f3fa6d07 html entity support 2024-04-23 15:22:26 +10:00
eae8af936b support restarting upload 2024-04-23 15:01:17 +10:00
257e241aea add url features 2024-04-23 14:34:58 +10:00
2587ea624a incorrect variable name 2024-04-23 14:23:05 +10:00
8cd19f6b8a incorrect variable name 2024-04-23 14:21:18 +10:00
8dea8e5995 detect filenames that are postfixed with a copy number indicator 2024-04-23 14:18:34 +10:00
dedcf1a379 fix padding of datetime-local inputs on webkit 2024-04-23 14:07:23 +10:00
2e0df186c6 title was not amended 2024-04-23 14:07:08 +10:00
2e39b9cb2a show the file count on uploading 2024-04-23 13:28:49 +10:00
0e19f6da87 clean filename 2024-04-23 12:33:37 +10:00
34092291dd better percentage display 2024-04-23 12:33:28 +10:00
67f2967823 support chunk uploading 2024-04-23 12:18:16 +10:00
33f0d83cf7 fix path for unknown.webp 2024-04-23 11:28:17 +10:00
3a35c28f1d dependency updates 2024-04-23 11:19:59 +10:00
2a1db0b088 return unknown.webp file if the requested file does not exist 2024-04-23 11:14:54 +10:00
79fb978127 show correct thumbnail when uploading file 2024-04-23 11:14:35 +10:00
9e2cfd8abc dont allow escape key on progress window 2024-04-23 11:08:00 +10:00
88889ca329 use thumbnails 2024-04-23 10:28:29 +10:00
aed29200a7 fix incrementing and automatic titling 2024-04-23 10:21:14 +10:00
d6b03826c1 added filenameToTitle 2024-04-23 10:20:55 +10:00
0a99b1789b auto select added files 2024-04-23 10:16:15 +10:00
80bc35bf4c show new name 2024-04-23 10:16:05 +10:00
f0ae8cd7ad update the value on load 2024-04-23 09:37:06 +10:00
6dbc9f0281 bugfix with file lists 2024-04-23 09:28:42 +10:00
88b1dcdfae bugfix with array types 2024-04-23 09:20:36 +10:00
ff811d961f hide files title if no files and not editing 2024-04-23 09:10:25 +10:00
633e0557f6 dont parse array 2024-04-23 08:39:46 +10:00
bdf8846278 handle arrays 2024-04-23 08:38:16 +10:00
f0d3e739f8 remove invalid method 2024-04-23 08:14:58 +10:00
00afc7a1d3 support collections 2024-04-23 08:12:56 +10:00
71f048fb00 keep files in order! 2024-04-23 07:59:57 +10:00
fd72954165 order by title 2024-04-23 07:54:02 +10:00
da90616aa0 use correct logo 2024-04-23 07:53:56 +10:00
566092a567 use correct logo 2024-04-23 07:51:16 +10:00
ef5e35229c update media entry 2024-04-23 07:48:35 +10:00
005b29ff5a use new thumbnails 2024-04-23 02:54:16 +10:00
2203731ed8 fix trailing / 2024-04-23 02:45:59 +10:00
f59265cc00 update background image url 2024-04-23 02:23:50 +10:00
0eab9ba2ce expand space 2024-04-23 02:20:24 +10:00
e2f18a87b8 only italic on update of file 2024-04-23 02:18:05 +10:00
27941b2017 only show rename info on update of file 2024-04-23 02:17:19 +10:00
b60e368cb9 json support for store 2024-04-23 01:53:06 +10:00
a1d966327f update mail logo 2024-04-23 01:39:45 +10:00
07ab627af4 fix non defined variable in create 2024-04-22 20:44:22 +10:00
b84f78b00b no decimal places 2024-04-22 20:34:01 +10:00
d6a84e2496 change env to config 2024-04-22 20:32:36 +10:00
3ea9cbda9e removal dark mode 2024-04-22 19:59:03 +10:00
363d8cfcdb update configuration 2024-04-22 19:53:10 +10:00
32f7770e75 refactor assets 2024-04-22 18:53:40 +10:00
53df7d2fbe refactor assets 2024-04-22 18:42:19 +10:00
550bdd8249 make timestamps nullable 2024-04-22 18:26:33 +10:00
55462d0437 remove extra seed 2024-04-22 18:26:25 +10:00
5b7da699bd updated to laravel 11 2024-04-22 18:16:33 +10:00
5fbca80a3c disable Intervention\Image\ImageServiceProvider::class 2024-04-04 20:59:59 +10:00
4fa1410790 dependency updates 2024-04-04 20:33:31 +10:00
411fc5f37f dependency updates 2024-04-04 20:22:09 +10:00
f7c20719f7 dependency updates 2024-04-04 20:18:43 +10:00
e5a7aeede8 Merge pull request #431 from STEMMechanics/dependabot/npm_and_yarn/typescript-eslint/parser-6.19.1 2024-01-26 19:48:06 +10:00
dependabot[bot]
cfbf3a4033 Bump @typescript-eslint/parser from 6.19.0 to 6.19.1
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.19.0 to 6.19.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-26 09:47:25 +00:00
89cda7c415 Merge pull request #436 from STEMMechanics/dependabot/composer/doctrine/dbal-3.8.0 2024-01-26 19:47:21 +10:00
e211436675 Merge pull request #435 from STEMMechanics/dependabot/npm_and_yarn/dotenv-16.4.1 2024-01-26 19:47:09 +10:00
d1be96a1df Merge pull request #434 from STEMMechanics/dependabot/composer/laravel/sail-1.27.2 2024-01-26 19:46:59 +10:00
dependabot[bot]
7e87312d99 Bump laravel/sail from 1.27.1 to 1.27.2
Bumps [laravel/sail](https://github.com/laravel/sail) from 1.27.1 to 1.27.2.
- [Release notes](https://github.com/laravel/sail/releases)
- [Changelog](https://github.com/laravel/sail/blob/1.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/sail/compare/v1.27.1...v1.27.2)

---
updated-dependencies:
- dependency-name: laravel/sail
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-26 09:45:36 +00:00
dependabot[bot]
73c277acfb Bump dotenv from 16.3.1 to 16.4.1
Bumps [dotenv](https://github.com/motdotla/dotenv) from 16.3.1 to 16.4.1.
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v16.3.1...v16.4.1)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-26 09:45:31 +00:00
0728233206 Merge pull request #430 from STEMMechanics/dependabot/npm_and_yarn/vitejs/plugin-vue-5.0.3 2024-01-26 19:45:07 +10:00
b127ce0514 Merge pull request #429 from STEMMechanics/dependabot/npm_and_yarn/knip-4.2.1 2024-01-26 19:44:57 +10:00
5924222296 Merge pull request #428 from STEMMechanics/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.19.1 2024-01-26 19:44:45 +10:00
6d2c601b5d Merge pull request #427 from STEMMechanics/dependabot/composer/laravel/pint-1.13.10 2024-01-26 19:43:32 +10:00
f2fb993596 Merge pull request #426 from STEMMechanics/dependabot/composer/phpunit/phpunit-10.5.9 2024-01-26 19:43:08 +10:00
c40232099f Merge pull request #425 from STEMMechanics/dependabot/composer/intervention/image-3.3.1 2024-01-26 19:42:48 +10:00
dependabot[bot]
17d33a9ad2 Bump doctrine/dbal from 3.7.2 to 3.8.0
Bumps [doctrine/dbal](https://github.com/doctrine/dbal) from 3.7.2 to 3.8.0.
- [Release notes](https://github.com/doctrine/dbal/releases)
- [Commits](https://github.com/doctrine/dbal/compare/3.7.2...3.8.0)

---
updated-dependencies:
- dependency-name: doctrine/dbal
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-26 00:25:53 +00:00
dependabot[bot]
e380bafbb0 Bump @vitejs/plugin-vue from 4.6.2 to 5.0.3
Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 4.6.2 to 5.0.3.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@5.0.3/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:33:50 +00:00
dependabot[bot]
4c63e68625 Bump knip from 4.2.0 to 4.2.1
Bumps [knip](https://github.com/webpro/knip/tree/HEAD/packages/knip) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/webpro/knip/releases)
- [Commits](https://github.com/webpro/knip/commits/4.2.1/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:33:39 +00:00
dependabot[bot]
8b5d150fe2 Bump @typescript-eslint/eslint-plugin from 6.19.0 to 6.19.1
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.19.0 to 6.19.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:33:30 +00:00
dependabot[bot]
10808f3622 Bump laravel/pint from 1.13.9 to 1.13.10
Bumps [laravel/pint](https://github.com/laravel/pint) from 1.13.9 to 1.13.10.
- [Release notes](https://github.com/laravel/pint/releases)
- [Changelog](https://github.com/laravel/pint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/laravel/pint/compare/v1.13.9...v1.13.10)

---
updated-dependencies:
- dependency-name: laravel/pint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:27:26 +00:00
dependabot[bot]
37ba8f0f29 Bump phpunit/phpunit from 10.5.8 to 10.5.9
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.8 to 10.5.9.
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/10.5.9/ChangeLog-10.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.8...10.5.9)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:27:18 +00:00
dependabot[bot]
51fb0150d9 Bump intervention/image from 3.3.0 to 3.3.1
Bumps [intervention/image](https://github.com/Intervention/image) from 3.3.0 to 3.3.1.
- [Commits](https://github.com/Intervention/image/compare/3.3.0...3.3.1)

---
updated-dependencies:
- dependency-name: intervention/image
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:27:11 +00:00
James Collins
03dd12cea1 Merge pull request #423 from STEMMechanics/dependabot/npm_and_yarn/vitest-1.2.1
Bump vitest from 0.34.6 to 1.2.1
2024-01-22 12:08:26 +10:00
dependabot[bot]
56ae719a2e Bump vitest from 0.34.6 to 1.2.1
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 0.34.6 to 1.2.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.2.1/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 02:07:55 +00:00
James Collins
68b7e50b9e Merge pull request #422 from STEMMechanics/dependabot/npm_and_yarn/eslint-plugin-jsdoc-48.0.2
Bump eslint-plugin-jsdoc from 46.10.1 to 48.0.2
2024-01-22 12:07:27 +10:00
James Collins
386e2009c4 Merge pull request #421 from STEMMechanics/dependabot/npm_and_yarn/knip-4.2.0
Bump knip from 2.43.0 to 4.2.0
2024-01-22 12:07:15 +10:00
James Collins
3da55bca81 Merge pull request #420 from STEMMechanics/dependabot/npm_and_yarn/vite-5.0.12
Bump vite from 4.5.1 to 5.0.12
2024-01-22 12:06:58 +10:00
dependabot[bot]
5d8f1457d4 Bump vite from 4.5.1 to 5.0.12
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.1 to 5.0.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:42:15 +00:00
James Collins
39d9ebc549 Merge pull request #419 from STEMMechanics/dependabot/composer/intervention/image-3.3.0
Bump intervention/image from 2.7.2 to 3.3.0
2024-01-22 11:41:32 +10:00
James Collins
b2f3664909 Merge pull request #418 from STEMMechanics/dependabot/composer/phpunit/phpunit-10.5.8
Bump phpunit/phpunit from 10.5.7 to 10.5.8
2024-01-22 11:41:15 +10:00
James Collins
06c2f0e3d0 Merge pull request #417 from STEMMechanics/dependabot/npm_and_yarn/vite-4.5.2
Bump vite from 4.5.1 to 4.5.2
2024-01-22 11:40:54 +10:00
dependabot[bot]
2d2392c8ae Bump intervention/image from 2.7.2 to 3.3.0
Bumps [intervention/image](https://github.com/Intervention/image) from 2.7.2 to 3.3.0.
- [Commits](https://github.com/Intervention/image/compare/2.7.2...3.3.0)

---
updated-dependencies:
- dependency-name: intervention/image
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:22:46 +00:00
dependabot[bot]
5e7061bc61 Bump vite from 4.5.1 to 4.5.2
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:20:29 +00:00
James Collins
8451a2bd4b Merge pull request #415 from STEMMechanics/dependabot/npm_and_yarn/prettier-3.2.4
Bump prettier from 3.0.3 to 3.2.4
2024-01-22 11:11:23 +10:00
James Collins
dd135f10ec Merge pull request #413 from STEMMechanics/dependabot/composer/square/square-34.0.1.20240118
Bump square/square from 32.0.0.20231018 to 34.0.1.20240118
2024-01-22 11:11:00 +10:00
dependabot[bot]
247eb03cea Bump eslint-plugin-jsdoc from 46.10.1 to 48.0.2
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 46.10.1 to 48.0.2.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v46.10.1...v48.0.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:10:49 +00:00
dependabot[bot]
19c16cfbf1 Bump square/square from 32.0.0.20231018 to 34.0.1.20240118
Bumps [square/square](https://github.com/square/square-php-sdk) from 32.0.0.20231018 to 34.0.1.20240118.
- [Release notes](https://github.com/square/square-php-sdk/releases)
- [Changelog](https://github.com/square/square-php-sdk/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/square-php-sdk/compare/32.0.0.20231018...34.0.1.20240118)

---
updated-dependencies:
- dependency-name: square/square
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:10:22 +00:00
dependabot[bot]
456687edc5 Bump knip from 2.43.0 to 4.2.0
Bumps [knip](https://github.com/webpro/knip/tree/HEAD/packages/knip) from 2.43.0 to 4.2.0.
- [Release notes](https://github.com/webpro/knip/releases)
- [Commits](https://github.com/webpro/knip/commits/4.2.0/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:10:18 +00:00
James Collins
aab63bb627 Merge pull request #397 from STEMMechanics/dependabot/npm_and_yarn/unocss-0.58.3
Bump unocss from 0.57.7 to 0.58.3
2024-01-22 11:06:09 +10:00
dependabot[bot]
efb3cac129 Bump unocss from 0.57.7 to 0.58.3
Bumps [unocss](https://github.com/unocss/unocss) from 0.57.7 to 0.58.3.
- [Release notes](https://github.com/unocss/unocss/releases)
- [Commits](https://github.com/unocss/unocss/compare/v0.57.7...v0.58.3)

---
updated-dependencies:
- dependency-name: unocss
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:03:28 +00:00
James Collins
36314339fb Merge pull request #361 from STEMMechanics/dependabot/composer/guzzlehttp/guzzle-7.8.1
Bump guzzlehttp/guzzle from 7.8.0 to 7.8.1
2024-01-22 10:49:10 +10:00
dependabot[bot]
c6247e445f Bump phpunit/phpunit from 10.5.7 to 10.5.8
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.7 to 10.5.8.
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/10.5.8/ChangeLog-10.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.7...10.5.8)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 00:44:40 +00:00
dependabot[bot]
0d1bd4522e Bump prettier from 3.0.3 to 3.2.4
Bumps [prettier](https://github.com/prettier/prettier) from 3.0.3 to 3.2.4.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.0.3...3.2.4)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-18 00:55:39 +00:00
dependabot[bot]
0f2400ff2b Bump guzzlehttp/guzzle from 7.8.0 to 7.8.1
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.8.0 to 7.8.1.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/7.8/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.8.0...7.8.1)

---
updated-dependencies:
- dependency-name: guzzlehttp/guzzle
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-04 00:54:13 +00:00
545 changed files with 17198 additions and 72966 deletions

View File

@@ -1,3 +0,0 @@
.github/
.vscode/
vendor/

View File

@@ -1,22 +0,0 @@
module.exports = {
env: {
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-strongly-recommended",
"prettier",
"plugin:jsdoc/recommended",
"plugin:@typescript-eslint/recommended",
],
rules: {
"vue/multi-word-component-names": "off",
indent: ["off", 4, { ignoredNodes: ["ConditionalExpression"] }],
"@typescript-eslint/no-inferrable-types": "off",
},
plugins: ["jsdoc", "@typescript-eslint"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
},
};

63
.github/workflows/dependency-update.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Dependency Update
on:
schedule:
- cron: "0 20 * * 1" # Mondays 20:00 UTC
workflow_dispatch: {}
permissions:
contents: write
pull-requests: write
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
tools: composer:v2
- name: Install current deps
run: |
npm ci
composer install --no-interaction --no-progress
- name: Update deps
run: |
npm update
composer update --no-interaction --no-progress
- name: Detect changes
id: changes
run: |
if git diff --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
# - name: Create PR
# if: steps.changes.outputs.changed == 'true'
# uses: peter-evans/create-pull-request@v6
# with:
# commit-message: "Dependency update"
# title: "Dependency update"
# body: "Automated dependency update."
# branch: "bot/dependency-update"
# delete-branch: true
- name: Commit and push
if: steps.changes.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -am "Dependency update"
git push origin HEAD:main

6
.gitignore vendored
View File

@@ -15,7 +15,7 @@ app/storage/
# Laravel 5 & Lumen specific # Laravel 5 & Lumen specific
public/storage public/storage
public/hot public/hot*
# Laravel 5 & Lumen specific with changed public path # Laravel 5 & Lumen specific with changed public path
public_html/storage public_html/storage
@@ -256,3 +256,7 @@ tempCodeRunnerFile.*
### Codesniffer ### ### Codesniffer ###
phpcs.phar phpcs.phar
phpcbf.phar phpcbf.phar
### PHPStorm ###
.idea/

View File

@@ -1,14 +0,0 @@
ls:
resources/js/store:
.ts: pascalcase
"*":
.js: camelcase
.ts: camelcase
.vue: pascalcase
.dir: snakecase
.type.ts: camelcase
ignore:
- node_modules
- vendor
- public/build
- public/tinymce

View File

@@ -1,5 +0,0 @@
{
"bracketSameLine": true,
"tabWidth": 4,
"htmlWhitespaceSensitivity": "css"
}

19
.vscode/settings.json vendored
View File

@@ -1,19 +0,0 @@
{
"editor.formatOnType": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[php]": {
// "editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
"editor.defaultFormatter": "wongjn.php-sniffer"
},
"cSpell.words": ["TIMESTAMPDIFF"]
}

View File

@@ -1,7 +1,7 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p> <p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center"> <p align="center">
<a href="https://travis-ci.org/laravel/framework"><img src="https://travis-ci.org/laravel/framework.svg" alt="Build Status"></a> <a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a> <a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a> <a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a> <a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
@@ -27,27 +27,27 @@ Laravel has the most extensive and thorough [documentation](https://laravel.com/
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors ## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell). We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners ### Premium Partners
- **[Vehikl](https://vehikl.com/)** - **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)** - **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** - **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)** - **[64 Robots](https://64robots.com)**
- **[Cubet Techno Labs](https://cubettech.com)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[Many](https://www.many.co.uk)**
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
- **[DevSquad](https://devsquad.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)** - **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)** - **[OP.GG](https://op.gg)**
- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
- **[Lendio](https://lendio.com)**
## Contributing ## Contributing

File diff suppressed because it is too large Load Diff

7
api.http Normal file
View File

@@ -0,0 +1,7 @@
### Get media items
GET http://127.0.0.1:8001/media
Accept: application/json
### Get media item
GET http://127.0.0.1:8001/media/SC-After-Dark.png
Accept: application/json

View File

@@ -1,71 +0,0 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class AnalyticsConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\AnalyticsSession::class;
/**
* The default includes to include in a request.
*
* @var array
*/
protected $includes = ['requests.type','requests.path'];
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/analytics') === true);
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
return true;
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/analytics') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/analytics') === true);
}
}

View File

@@ -1,181 +0,0 @@
<?php
namespace App\Conductors;
use App\Models\Media;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\MissingAttributeException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use LogicException;
class ArticleConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Article::class;
/**
* The default sorting field
* @var string
*/
protected $sort = '-publish_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['attachments', 'user', 'gallery'];
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder): void
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/articles') === false) {
$builder
->where('publish_at', '<=', now());
}
}
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
if (Carbon::parse($model->publish_at)->isFuture() === true) {
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/articles') === false) {
return false;
}
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/articles') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/articles') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/articles') === true);
}
/**
* Transform the final model data
*
* @param array $data The model data to transform.
* @return array The transformed model.
*/
public function transformFinal(array $data): array
{
unset($data['user_id']);
return $data;
}
/**
* Include Attachments Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeAttachments(Model $model)
{
return $model->getAttachments()->map(function ($attachment) {
return MediaConductor::includeModel(request(), 'attachments', $attachment->media);
});
}
/**
* Include Gallery Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeGallery(Model $model)
{
return $model->getGallery()->map(function ($item) {
return MediaConductor::includeModel(request(), 'gallery', $item->media);
});
}
/**
* Include User Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeUser(Model $model)
{
$cacheKey = "user:{$model['user_id']}";
$user = Cache::remember($cacheKey, now()->addDays(28), function () use ($model) {
return User::find($model['user_id']);
});
return UserConductor::includeModel(request(), 'user', $user);
}
/**
* Transform the Hero field.
*
* @param mixed $value The current value.
* @return array|null The new value.
*/
public function transformHero(mixed $value): array|null
{
$cacheKey = "media:{$value}";
$media = Cache::remember($cacheKey, now()->addDays(28), function () use ($value) {
return Media::find($value);
});
return MediaConductor::includeModel(request(), 'hero', $media);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,140 +0,0 @@
<?php
namespace App\Conductors;
use App\Models\Media;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class EventConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Event::class;
/**
* The default sorting field
* @var string
*/
protected $sort = '-start_at';
/**
* The included fields
* @var string[]
*/
protected $includes = ['attachments'];
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder): void
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/events') === false) {
$builder
->where('status', '!=', 'draft')
->where('publish_at', '<=', now());
}
}
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
if (strtolower($model->status) === 'draft' || Carbon::parse($model->publish_at)->isFuture() === true) {
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/events') === false) {
return false;
}
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Include Attachments Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeAttachments(Model $model)
{
/** @var \App\Models\User */
$user = auth()->user();
return $model->getAttachments()->map(function ($attachment) use ($user) {
if ($attachment->private === false || ($user !== null && $user->hasPermission('admin/events') === true)) {
return MediaConductor::includeModel(request(), 'attachments', $attachment->media);
}
});
}
/**
* Transform the Hero field.
*
* @param mixed $value The current value.
* @return array|null The new value.
*/
public function transformHero(mixed $value): array|null
{
$cacheKey = "media:{$value}";
$media = Cache::remember($cacheKey, now()->addDays(28), function () use ($value) {
return Media::find($value);
});
return MediaConductor::includeModel(request(), 'hero', $media);
}
}

View File

@@ -1,184 +0,0 @@
<?php
namespace App\Conductors;
use App\Models\MediaJob;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class MediaConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Media::class;
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['user', 'jobs'];
/**
* The default filters to use in a request.
*
* @var array
*/
// protected $defaultFilters = [
// 'status' => 'OK'
// ];
/**
* Return an array of model fields visible to the current user.
*
* @param Model $model The model in question.
* @return array The array of field names.
*/
public function fields(Model $model): array
{
$fields = parent::fields($model);
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/media') === false) {
$fields = arrayRemoveItem($fields, ['security_data', 'storage']);
}
return $fields;
}
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder): void
{
$user = auth()->user();
if ($user === null) {
$builder->where('security_type', '')
->orWhere('security_type', 'password');
} else {
$builder->where(function ($query) use ($user) {
$query->where('security_type', '')
->orWhere('security_type', 'password')
->orWhere(function ($subquery) use ($user) {
$subquery->where('security_type', 'permission')
->whereIn('security_data', $user->permissions);
});
});
}
}
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
if (strcasecmp('permission', $model->security_type) === 0) {
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission($model->security_data) === false) {
return false;
}
} elseif ($model->security_type !== '' && strcasecmp('password', $model->security_type) !== 0) {
return false;
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
$user = auth()->user();
return ($user !== null);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && (strcasecmp($model->user_id, $user->id) === 0 ||
$user->hasPermission('admin/media') === true));
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true));
}
/**
* Transform the final model data
*
* @param array $data The model data to transform.
* @return array The transformed model.
*/
public function transformFinal(array $data): array
{
unset($data['user_id']);
return $data;
}
/**
* Include User Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeUser(Model $model)
{
$user = Cache::remember("user:{$model['user_id']}", now()->addDays(28), function () use ($model) {
return User::find($model['user_id']);
});
return UserConductor::includeModel(request(), 'user', $user);
}
/**
* Include job models in Media
*
* @param Model $model The reference model.
* @return mixed
*/
public function includeJobs(Model $model)
{
$jobs = $model->jobs()
->select(['id','created_at','updated_at','user_id','status','status_text','progress'])
->orderBy('created_at', 'desc')->get();
return $jobs;
}
}

View File

@@ -1,76 +0,0 @@
<?php
namespace App\Conductors;
use App\Models\MediaJob;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class MediaJobConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\MediaJob::class;
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['user'];
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
return false;
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
return false;
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
return false;
}
/**
* Transform the final model data
*
* @param array $data The model data to transform.
* @return array The transformed model.
*/
public function transformFinal(array $data): array
{
unset($data['user_id']);
return $data;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class ShortlinkConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Shortlink::class;
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/shortlinks') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/shortlinks') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/shortlinks') === true);
}
}

View File

@@ -1,45 +0,0 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class SubscriptionConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Subscription::class;
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && (
(strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) ||
$user->hasPermission('admin/subscriptions') === true
));
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 &&
$user->email_verified_at !== null) || $user->hasPermission('admin/subscriptions') === true));
}
}

View File

@@ -1,89 +0,0 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class UserConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\User::class;
/**
* Return the visible API fields.
*
* @param Model $model The model.
* @return string[] The fields visible.
*/
public function fields(Model $model): array
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/users') === false) {
return ['id', 'display_name'];
}
return parent::fields($model);
}
/**
* Transform the passed Model to an array
*
* @param Model $model The model to transform.
* @return array The transformed model.
*/
public function transform(Model $model): array
{
/** @var \App\Models\User */
$user = auth()->user();
$data = $model->toArray();
$limit = $this->fields($model);
if (
$user === null || (
$user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0
)
) {
$limit = ['id', 'display_name'];
} else {
$data['permissions'] = $user->permissions;
}
$data = arrayLimitKeys($data, $limit);
return $data;
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user !== null) {
return ($user->hasPermission('admin/users') === true || strcasecmp($user->id, $model->id) === 0);
}
return false;
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/users') === true);
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CleanupTempFiles extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:cleanup-temp-files';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete temporary files that are older that 1 day';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$keepTime = (1 * 24 * 60 * 60); // 1 Day
$currentTimeStamp = time();
$deletedFileCount = 0;
foreach (glob(storage_path('app/tmp/*')) as $filename) {
$fileModifiedTimeStamp = filemtime($filename);
if (($currentTimeStamp - $fileModifiedTimeStamp) > $keepTime) {
unlink($filename);
$deletedFileCount++;
}
}
$this->comment('Deleted ' . $deletedFileCount . ' files');
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\MediaJob;
use Illuminate\Console\Command;
class RemoveStaleMediaJobs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:remove-stale-media-jobs';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove media_jobs that have not been modified for 48 hours';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$threshold = now()->subHours(48);
$staleJobs = MediaJob::where('updated_at', '<=', $threshold)->get();
foreach ($staleJobs as $job) {
$job->delete();
}
$this->info(count($staleJobs) . ' stale media_jobs removed.');
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule The schedule.
* @return void
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
$schedule->command('app:cleanup-temp-files')->everyThirtyMinutes();
$schedule->command('app:remove-stale-media-jobs')->everyThirtyMinutes();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}
}

View File

@@ -1,169 +0,0 @@
<?php
namespace App\Enum;
class CurlErrorCodes extends Enum
{
public const CURLE_UNSUPPORTED_PROTOCOL = 1;
public const CURLE_FAILED_INIT = 2;
public const CURLE_URL_MALFORMAT = 3;
public const CURLE_URL_MALFORMAT_USER = 4;
public const CURLE_COULDNT_RESOLVE_PROXY = 5;
public const CURLE_COULDNT_RESOLVE_HOST = 6;
public const CURLE_COULDNT_CONNECT = 7;
public const CURLE_FTP_WEIRD_SERVER_REPLY = 8;
public const CURLE_REMOTE_ACCESS_DENIED = 9;
public const CURLE_FTP_WEIRD_PASS_REPLY = 11;
public const CURLE_FTP_WEIRD_PASV_REPLY = 13;
public const CURLE_FTP_WEIRD_227_FORMAT = 14;
public const CURLE_FTP_CANT_GET_HOST = 15;
public const CURLE_FTP_COULDNT_SET_TYPE = 17;
public const CURLE_PARTIAL_FILE = 18;
public const CURLE_FTP_COULDNT_RETR_FILE = 19;
public const CURLE_QUOTE_ERROR = 21;
public const CURLE_HTTP_RETURNED_ERROR = 22;
public const CURLE_WRITE_ERROR = 23;
public const CURLE_UPLOAD_FAILED = 25;
public const CURLE_READ_ERROR = 26;
public const CURLE_OUT_OF_MEMORY = 27;
public const CURLE_OPERATION_TIMEDOUT = 28;
public const CURLE_FTP_PORT_FAILED = 30;
public const CURLE_FTP_COULDNT_USE_REST = 31;
public const CURLE_RANGE_ERROR = 33;
public const CURLE_HTTP_POST_ERROR = 34;
public const CURLE_SSL_CONNECT_ERROR = 35;
public const CURLE_BAD_DOWNLOAD_RESUME = 36;
public const CURLE_FILE_COULDNT_READ_FILE = 37;
public const CURLE_LDAP_CANNOT_BIND = 38;
public const CURLE_LDAP_SEARCH_FAILED = 39;
public const CURLE_FUNCTION_NOT_FOUND = 41;
public const CURLE_ABORTED_BY_CALLBACK = 42;
public const CURLE_BAD_FUNCTION_ARGUMENT = 43;
public const CURLE_INTERFACE_FAILED = 45;
public const CURLE_TOO_MANY_REDIRECTS = 47;
public const CURLE_UNKNOWN_TELNET_OPTION = 48;
public const CURLE_TELNET_OPTION_SYNTAX = 49;
public const CURLE_PEER_FAILED_VERIFICATION = 51;
public const CURLE_GOT_NOTHING = 52;
public const CURLE_SSL_ENGINE_NOTFOUND = 53;
public const CURLE_SSL_ENGINE_SETFAILED = 54;
public const CURLE_SEND_ERROR = 55;
public const CURLE_RECV_ERROR = 56;
public const CURLE_SSL_CERTPROBLEM = 58;
public const CURLE_SSL_CIPHER = 59;
public const CURLE_SSL_CACERT = 60;
public const CURLE_BAD_CONTENT_ENCODING = 61;
public const CURLE_LDAP_INVALID_URL = 62;
public const CURLE_FILESIZE_EXCEEDED = 63;
public const CURLE_USE_SSL_FAILED = 64;
public const CURLE_SEND_FAIL_REWIND = 65;
public const CURLE_SSL_ENGINE_INITFAILED = 66;
public const CURLE_LOGIN_DENIED = 67;
public const CURLE_TFTP_NOTFOUND = 68;
public const CURLE_TFTP_PERM = 69;
public const CURLE_REMOTE_DISK_FULL = 70;
public const CURLE_TFTP_ILLEGAL = 71;
public const CURLE_TFTP_UNKNOWNID = 72;
public const CURLE_REMOTE_FILE_EXISTS = 73;
public const CURLE_TFTP_NOSUCHUSER = 74;
public const CURLE_CONV_FAILED = 75;
public const CURLE_CONV_REQD = 76;
public const CURLE_SSL_CACERT_BADFILE = 77;
public const CURLE_REMOTE_FILE_NOT_FOUND = 78;
public const CURLE_SSH = 79;
public const CURLE_SSL_SHUTDOWN_FAILED = 80;
public const CURLE_AGAIN = 81;
public const CURLE_SSL_CRL_BADFILE = 82;
public const CURLE_SSL_ISSUER_ERROR = 83;
public const CURLE_FTP_PRET_FAILED = 84;
public const CURLE_RTSP_CSEQ_ERROR = 85;
public const CURLE_RTSP_SESSION_ERROR = 86;
public const CURLE_FTP_BAD_FILE_LIST = 87;
public const CURLE_CHUNK_FAILED = 88;
/**
* Curl Error messages
* @var string[]
*/
public static $messages = [
1 => 'Unsupported protocol.',
2 => 'Failed initalization.',
3 => 'Invalid URL format.',
4 => 'CURLE_URL_MALFORMAT_USER.',
5 => 'Could not resolve proxy.',
6 => 'Could not resolve host.',
7 => 'Could not connect to host.',
8 => 'Invalid reply from FTP server.',
9 => 'Access denied on host.',
11 => 'Invalid pass reply from FTP server.',
13 => 'Invalid pasv reply from FTP server.',
14 => 'Invalid 227 format from FTP server.',
15 => 'Could not get FTP host.',
17 => 'Could not set type for FTP transfer.',
18 => 'Invalid partial size.',
19 => 'Could not retrieve file from FTP server.',
21 => 'Quote error.',
22 => 'HTTP server returned error.',
23 => 'File write error.',
25 => 'Upload file error.',
26 => 'File read error.',
27 => 'Out of memory.',
28 => 'File transfer timed out.',
30 => 'Invalid port for FTP server.',
31 => 'Could not use rest for FTP server.',
33 => 'File range error.',
34 => 'Invalid POST for HTTP server.',
35 => 'SSL connectio error.',
36 => 'Invalid resume download.',
37 => 'Could not read file.',
38 => 'Could not bind to LDAP.',
39 => 'LDAP search failed.',
41 => 'Function not found.',
42 => 'Aborted by callback.',
43 => 'Bad function argument.',
45 => 'Interface failed.',
47 => 'Too many redirects.',
48 => 'Unknown telnet option.',
49 => 'Telnet option syntax invalid.',
51 => 'Peer failed verification.',
52 => 'Did not receive any data.',
53 => 'SSL engine was not found.',
54 => 'SSL engine failed.',
55 => 'Send data error.',
56 => 'Receive data error.',
58 => 'SSL certificate error.',
59 => 'SSL cipher error.',
60 => 'SSL CACertificate failed.',
61 => 'Invalid content encoding.',
62 => 'Invalid LDAP url.',
63 => 'Filesize exceeded.',
64 => 'SSL Failed.',
65 => 'CURLE_SEND_FAIL_REWIND.',
66 => 'SSL engine initalization failed.',
67 => 'CURLE_LOGIN_DENIED.',
68 => 'CURLE_TFTP_NOTFOUND.',
69 => 'CURLE_TFTP_PERM.',
70 => 'CURLE_REMOTE_DISK_FULL.',
71 => 'CURLE_TFTP_ILLEGAL.',
72 => 'CURLE_TFTP_UNKNOWNID.',
73 => 'Remote file already exists.',
74 => 'No such user on FTP server.',
75 => 'Conversion failed.',
76 => 'Conversion required.',
77 => 'SSL CACertificate bad file.',
78 => 'Remove file not found.',
79 => 'SSH error.',
80 => 'SSL Shutdown failed.',
81 => 'Again.',
82 => 'SSL bad CRL file.',
83 => 'SSL issuer error.',
84 => 'FTP pret failed.',
85 => 'CURLE_RTSP_CSEQ_ERROR.',
86 => 'CURLE_RTSP_SESSION_ERROR.',
87 => 'CURLE_FTP_BAD_FILE_LIST.',
88 => 'CURLE_CHUNK_FAILED.',
];
}

View File

@@ -1,73 +0,0 @@
<?php
namespace App\Enum;
use ReflectionClass;
class Enum
{
/**
* Message list
*
* @var array<string<static>>
*/
public static $messages = [];
/**
* Caches reflections of enum subclasses.
*
* @var array<class-string<static>, ReflectionClass<static>>
*/
public static $reflectionCache = [];
/**
* Returns a reflection of the enum subclass.
*
* @return ReflectionClass<static>
*/
public static function getReflection(): ReflectionClass
{
$class = static::class;
return static::$reflectionCache[$class] ??= new ReflectionClass($class);
}
/**
* Returns the constants in the enum subclass
*
* @return array<static>
*/
public static function getConstants(): array
{
return static::getReflection()->getConstants();
}
/**
* Returns the constants values in the enum subclass
*
* @return array<static>
*/
public static function getConstantValues(): array
{
return array_values(static::getReflection()->getConstants());
}
/**
* Returns a message from the enum subclass
*
* @param integer $messageIndex The message index to retrieve.
* @param string $defaultMessage Message to use if index does not exist.
* @return string
*/
public static function getMessage(int $messageIndex, string $defaultMessage = 'Unknown'): string
{
if (array_key_exists($messageIndex, self::$messages) === true) {
return self::$messages[$messageIndex];
}
return $defaultMessage;
}
}

View File

@@ -1,165 +0,0 @@
<?php
namespace App\Enum;
class HttpResponseCodes extends Enum
{
public const HTTP_CONTINUE = 100;
public const HTTP_SWITCHING_PROTOCOLS = 101;
public const HTTP_PROCESSING = 102;
public const HTTP_OK = 200;
public const HTTP_CREATED = 201;
public const HTTP_ACCEPTED = 202;
public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
public const HTTP_NO_CONTENT = 204;
public const HTTP_RESET_CONTENT = 205;
public const HTTP_PARTIAL_CONTENT = 206;
public const HTTP_MULTI_STATUS = 207;
public const HTTP_ALREADY_REPORTED = 208;
public const HTTP_IM_USED = 226;
public const HTTP_MULTIPLE_CHOICES = 300;
public const HTTP_MOVED_PERMANENTLY = 301;
public const HTTP_FOUND = 302;
public const HTTP_SEE_OTHER = 303;
public const HTTP_NOT_MODIFIED = 304;
public const HTTP_USE_PROXY = 305;
public const HTTP_RESERVED = 306;
public const HTTP_TEMPORARY_REDIRECT = 307;
public const HTTP_PERMANENTLY_REDIRECT = 308;
public const HTTP_BAD_REQUEST = 400;
public const HTTP_UNAUTHORIZED = 401;
public const HTTP_PAYMENT_REQUIRED = 402;
public const HTTP_FORBIDDEN = 403;
public const HTTP_NOT_FOUND = 404;
public const HTTP_METHOD_NOT_ALLOWED = 405;
public const HTTP_NOT_ACCEPTABLE = 406;
public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
public const HTTP_REQUEST_TIMEOUT = 408;
public const HTTP_CONFLICT = 409;
public const HTTP_GONE = 410;
public const HTTP_LENGTH_REQUIRED = 411;
public const HTTP_PRECONDITION_FAILED = 412;
public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
public const HTTP_REQUEST_URI_TOO_LONG = 414;
public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public const HTTP_EXPECTATION_FAILED = 417;
public const HTTP_I_AM_A_TEAPOT = 418;
public const HTTP_MISDIRECTED_REQUEST = 421;
public const HTTP_UNPROCESSABLE_ENTITY = 422;
public const HTTP_LOCKED = 423;
public const HTTP_FAILED_DEPENDENCY = 424;
public const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425;
public const HTTP_UPGRADE_REQUIRED = 426;
public const HTTP_PRECONDITION_REQUIRED = 428;
public const HTTP_TOO_MANY_REQUESTS = 429;
public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
public const HTTP_INTERNAL_SERVER_ERROR = 500;
public const HTTP_NOT_IMPLEMENTED = 501;
public const HTTP_BAD_GATEWAY = 502;
public const HTTP_SERVICE_UNAVAILABLE = 503;
public const HTTP_GATEWAY_TIMEOUT = 504;
public const HTTP_VERSION_NOT_SUPPORTED = 505;
public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506;
public const HTTP_INSUFFICIENT_STORAGE = 507;
public const HTTP_LOOP_DETECTED = 508;
public const HTTP_NOT_EXTENDED = 510;
public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;
/**
* HTTP Response Messages
* @var string[]
*/
public static $statusTexts = [
100 => 'Continue.',
101 => 'Switching Protocols.',
102 => 'Processing.',
// RFC2518
200 => 'OK.',
201 => 'Created.',
202 => 'Accepted.',
203 => 'Non-Authoritative Information.',
204 => 'No Content.',
205 => 'Reset Content.',
206 => 'Partial Content.',
207 => 'Multi-Status.',
// RFC4918
208 => 'Already Reported.',
// RFC5842
226 => 'IM Used.',
// RFC3229
300 => 'Multiple Choices.',
301 => 'Moved Permanently.',
302 => 'Found.',
303 => 'See Other.',
304 => 'Not Modified.',
305 => 'Use Proxy.',
307 => 'Temporary Redirect.',
308 => 'Permanent Redirect.',
// RFC7238
400 => 'Bad Request.',
401 => 'Unauthorized.',
402 => 'Payment Required.',
403 => 'Forbidden.',
404 => 'Not Found.',
405 => 'Method Not Allowed.',
406 => 'Not Acceptable.',
407 => 'Proxy Authentication Required.',
408 => 'Request Timeout.',
409 => 'Conflict.',
410 => 'Gone.',
411 => 'Length Required.',
412 => 'Precondition Failed.',
413 => 'Payload Too Large.',
414 => 'URI Too Long.',
415 => 'Unsupported Media Type.',
416 => 'Range Not Satisfiable.',
417 => 'Expectation Failed.',
418 => 'I\'m a teapot.',
// RFC2324
421 => 'Misdirected Request.',
// RFC7540
422 => 'Unprocessable Entity.',
// RFC4918
423 => 'Locked.',
// RFC4918
424 => 'Failed Dependency.',
// RFC4918
425 => 'Reserved for WebDAV advanced collections expired proposal.',
// RFC2817
426 => 'Upgrade Required.',
// RFC2817
428 => 'Precondition Required.',
// RFC6585
429 => 'Too Many Requests.',
// RFC6585
431 => 'Request Header Fields Too Large.',
// RFC6585
451 => 'Unavailable For Legal Reasons.',
// RFC7725
500 => 'Internal Server Error.',
501 => 'Not Implemented.',
502 => 'Bad Gateway.',
503 => 'Service Unavailable.',
504 => 'Gateway Timeout.',
505 => 'HTTP Version Not Supported.',
506 => 'Variant Also Negotiates.',
// RFC2295
507 => 'Insufficient Storage.',
// RFC4918
508 => 'Loop Detected.',
// RFC5842
510 => 'Not Extended.',
// RFC2774
511 => 'Network Authentication Required.',
// RFC6585
];
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Exceptions;
use Exception;
class FileInvalidException extends Exception
{
/**
* The error code of the exception.
*
* @var int
*/
protected $code;
/**
* The error message of the exception.
*
* @var string
*/
protected $message;
/**
* Create a new exception instance.
*
* @param string $message
* @param int $code
* @return void
*/
public function __construct(string $message, int $code = 0)
{
$this->message = $message;
$this->code = $code;
parent::__construct($message, $code);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Exceptions;
use Exception;
class FileTooLargeException extends Exception
{
/**
* The error code of the exception.
*
* @var int
*/
protected $code;
/**
* The error message of the exception.
*
* @var string
*/
protected $message;
/**
* Create a new exception instance.
*
* @param string $message
* @param int $code
* @return void
*/
public function __construct(string $message, int $code = 0)
{
$this->message = $message;
$this->code = $code;
parent::__construct($message, $code);
}
}

View File

@@ -1,98 +0,0 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
use PDOException;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
class Handler extends ExceptionHandler
{
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register(): void
{
// $this->renderable(function (HttpException $e, $request) {
// if ($request->is('api/*')) {
// $message = $e->getMessage();
// if ($message === '') {
// $message = HttpResponseCodes::$statusTexts[$e->getStatusCode()];
// }
// return response()->json([
// 'message' => $message
// ], $e->getStatusCode());
// }
// });
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*') === true) {
return response()->json([
'message' => 'Resource not found'
], 404);
}
});
$this->renderable(function (PDOException $e, $request) {
if ($request->is('api/*') === true) {
return response()->json([
'message' => 'The server is currently unavailable'
], 503);
}
});
$this->reportable(function (Throwable $e) {
if ($this->shouldReport($e) === true) {
if (App::runningUnitTests() === false) {
$this->sendEmail($e);
}
}
});
}
/**
* Send email
*
* @param Throwable $exception Throwable object.
* @return void
*/
public function sendEmail(Throwable $exception)
{
try {
$e = FlattenException::createFromThrowable($exception);
$handler = new HtmlErrorRenderer(true);
$css = $handler->getStylesheet();
$content = $handler->getBody($e);
Mail::send('emails.exception', compact('css', 'content'), function ($message) {
$message
->to('webmaster@stemmechanics.com.au')
->subject('Exception Generated')
;
});
} catch (Throwable $ex) {
Log::error($ex);
}
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Filters;
use App\Models\Subscriber;
class SubscriptionFilter extends FilterAbstract
{
/**
* The model class to filter
*
* @var mixed
*/
protected $class = \App\Models\Subscription::class;
/**
* Return an array of attributes visible in the results
*
* @param array $attributes Attributes currently visible.
* @param User|null $user Current logged in user or null.
* @return mixed
*/
protected function seeAttributes(array $attributes, mixed $user)
{
if ($user?->hasPermission('admin/users') !== true) {
return ['id', 'email', 'confirmed_at'];
}
}
}

181
app/Helpers.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
namespace App;
use DateTime;
use Illuminate\Support\Facades\Log;
class Helpers
{
/**
* Get the maximum upload size in bytes.
*/
public static function getMaxUploadSize(): int
{
return min(
self::stringToBytes(ini_get('post_max_size')),
self::stringToBytes(ini_get('upload_max_filesize'))
);
}
public static function stringToBytes(string $val): int
{
if (empty($val)) {
$val = 0;
}
$val = trim($val);
$last = strtolower($val[strlen($val) - 1]);
$val = floatval($val);
switch ($last) {
case 'g':
$val *= (1024 * 1024 * 1024); //1073741824
break;
case 'm':
$val *= (1024 * 1024); //1048576
break;
case 'k':
$val *= 1024;
break;
}
return $val;
}
public static function bytesToString(int|float|string $bytes): string
{
if (!is_numeric($bytes)) {
return '0 bytes';
}
$units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
for ($i = 0; $bytes > 1024; $i++) {
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
}
public static function arrayToString(array $array, string $separator = ','): string
{
return implode($separator, array_map(function ($item) use ($separator) {
if (str_contains($item, $separator)) {
return '"' . str_replace('"', '\\"', $item) . '"';
} else {
return $item;
}
}, $array));
}
public static function stringToArray(string $string, string $separator = ','): array
{
return array_map(function ($item) {
// Remove quotes and unescape any escaped quotes within the string
return str_replace('\\"', '"', trim($item, '"'));
}, explode($separator, $string));
}
public static function timestampNoSeconds(string $timestamp): string
{
if(empty($timestamp)) {
return '';
}
$datetime = new DateTime($timestamp);
return $datetime->format('Y-m-d\TH:i');
}
public static function isUnderAge(mixed $ages): bool
{
if(!is_string($ages)) {
return true;
}
preg_match('/\d+/', $ages, $matches);
if (empty($matches)) {
return true;
}
$firstNumber = $matches[0];
return ($firstNumber <= 8);
}
public static function createTimeDurationStr(string $startStr, string $endStr): array
{
try {
$start = new DateTime($startStr);
$end = new DateTime($endStr);
if ($start->format('Y-m-d') === $end->format('Y-m-d')) {
return [
$start->format('l j M Y'),
$start->format('g:i a') . ' - ' . $end->format('g:i a')
];
} else {
return [
$start->format('D j/m/Y') . ' - ' . $end->format('D j/m/Y')
];
}
} catch(\Exception $e) {
return ['Error parsing date'];
}
}
public static function matchesMimeType(string $mimeType, string|array $patterns): bool
{
if (is_string($patterns)) {
$patterns = [$patterns];
}
foreach ($patterns as $pattern) {
$pattern = str_replace('\*', '.*', preg_quote($pattern, '/'));
$regex = '/^' . $pattern . '$/';
if (preg_match($regex, $mimeType) === 1) {
return true;
}
}
return false;
}
public static function findMatchingMimeTypeKey(string $mimeType, array $patterns): string|bool
{
$match = '';
foreach ($patterns as $key => $value) {
$keys = explode(',', $key);
foreach($keys as $key) {
$pattern = str_replace('\*', '.*', preg_quote($key, '/'));
$regex = '/^' . $pattern . '$/';
if (preg_match($regex, $mimeType) === 1) {
if($match !== $mimeType) {
$match = $key;
}
}
}
}
if($match !== '') {
return $match;
}
return false;
}
public static function cleanFileName(string $name): string
{
$name = strtolower($name);
$name = mb_ereg_replace('/^\.+/', '', $name);
$name = mb_ereg_replace("([\s_])", '-', $name);
$name = mb_ereg_replace("([^\w\s\d\-_.])", '', $name);
$name = mb_ereg_replace("([\.]{2,})", '', $name);
$name = mb_ereg_replace("([\-]{2,})", '-', $name);
return $name;
}
public static function filenameToTitle(string $filename): string
{
$title = pathinfo($filename, PATHINFO_FILENAME);
$title = str_replace(['-', '_', '.'], ' ', $title);
$title = ucwords($title);
return $title;
}
}

View File

@@ -1,78 +0,0 @@
<?php
/* Array Helper Functions */
/**
* Remove an item from an array.
*
* @param array $arr The array to check.
* @param string|array $item The item or items to remove.
* @return array The filtered array.
*/
function arrayRemoveItem(array $arr, string|array $item): array
{
$filteredArr = $arr;
if (is_string($item) === true) {
$item = [$item];
}
foreach ($item as $str) {
$filteredArr = array_filter($arr, function ($item) use ($str) {
return $item !== $str;
});
}
return $filteredArr;
}
/**
* Return an array with specified the keys
*
* @param array $arr The array to filter.
* @param string|array $keys The keys to keep.
* @return array The filtered array.
*/
function arrayLimitKeys(array $arr, array $keys): array
{
return array_intersect_key($arr, array_flip($keys));
}
/**
* Return an array value or default value if it does not exist
*
* @param string $key The key value to return if exists.
* @param array $arr The array to check.
* @param mixed $value The value to return if key does not exist.
* @return mixed
*/
function arrayDefaultValue(string $key, array $arr, mixed $value): mixed
{
if (array_key_exists($key, $arr) === true) {
return $arr[$key];
}
return $value;
}
/**
* Return if an item exists in an array, case insensitive
*
* @param string $val The value to check.
* @param array $arr The array to check.
* @return boolean
*/
function existsInArray(string $val, array $arr): bool
{
$exists = false;
foreach ($arr as $el) {
if (strcasecmp($val, $el) === 0) {
$exists = true;
break;
}
}
return $exists;
}

View File

@@ -1,92 +0,0 @@
<?php
/* Temp File Helper Functions */
/**
* Generate a temporary file path.
*
* @param string $extension The file extension to use.
* @param string $part The file part number.
* @return string The filtered array.
*/
function generateTempFilePath(string $extension = '', string $part = ''): string
{
$temporaryDir = storage_path('app/tmp');
if (is_dir($temporaryDir) === false) {
mkdir($temporaryDir, 0777, true);
}
return $temporaryDir . DIRECTORY_SEPARATOR . uniqid('upload_', true) . ($extension !== '' ? ".{$extension}" : '') .
($part !== '' ? ".part-{$part}" : '');
}
/**
* Get Temp file information
*
* @param string $filePath The temp file name.
* @return array The temp file name details.
*/
function tempFileInfo(string $filePath): array
{
$part = '';
// Extract the part if it's present
if (preg_match('/\.part-(\d+)$/', $filePath, $matches) !== false) {
$part = $matches[1];
$filePath = substr($filePath, 0, -strlen($matches[0]));
}
$info = pathinfo($filePath);
$directory = $info['dirname'];
$name = $info['filename'];
$extension = '';
// If there's an extension, separate it
if (isset($info['extension']) === true) {
$extension = $info['extension'];
}
return [
'dirname' => $directory,
'basename' => $name . ($extension !== '' ? ".{$extension}" : ''),
'filename' => $name,
'extension' => $extension,
'part' => $part,
];
}
/**
* Check a temporary file exists.
*
* @param string $dir The file parent directory.
* @param string $name The file name.
* @param string $extension The file extension to use.
* @param string $part The file part number.
* @return boolean If the file exists.
*/
function tempFileExists(string $dir, string $name, string $extension = '', string $part = ''): bool
{
$filename = constructTempFileName($dir, $name, $extension, $part);
$exists = file_exists($filename);
return $exists;
}
/**
* Construct the temp file name based on the information
*
* @param string $dir The file parent directory.
* @param string $name The file name.
* @param string $extension The file extension to use.
* @param string $part The file part number.
* @return string The file path.
*/
function constructTempFileName(string $dir, string $name, string $extension = '', string $part = ''): string
{
$filename = $dir . DIRECTORY_SEPARATOR . $name . ($extension !== '' ? ".{$extension}" : '') .
($part !== "" ? ".part-{$part}" : '');
return $filename;
}

View File

@@ -1,27 +0,0 @@
<?php
/* Type Value Helper Functions */
/**
* Is value true
*
* @param mixed $value Value to check.
* @return boolean
*/
function isTrue(mixed $value): bool
{
if (is_bool($value) === true && $value === true) {
return true;
}
if (is_numeric($value) === true && intval($value) === 1) {
return true;
}
if (is_string($value) === true && in_array(strtolower($value), ['true', '1'], true) === true) {
return true;
}
return false;
}

View File

@@ -0,0 +1,240 @@
<?php
namespace App\Http\Controllers;
use App\Helpers;
use App\Jobs\SendEmail;
use App\Mail\UserEmailUpdateRequest;
use App\Models\User;
use App\Providers\QRCodeProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use RobThree\Auth\Algorithm;
use RobThree\Auth\TwoFactorAuth;
class AccountController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Display the specified resource.
*/
public function show(User $user)
{
return view('account', compact('user'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request)
{
$user = auth()->user();
$validator = Validator::make($request->all(), [
'firstname' => 'required',
'surname' => 'required',
'email' => ['required', 'email', 'unique:users,email,' . $user->id],
'phone' => 'required',
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
'billing_postcode' => 'required_with:billing_address,billing_city,billing_country,billing_state',
'billing_country' => 'required_with:billing_address,billing_city,billing_postcode,billing_state',
'billing_state' => 'required_with:billing_address,billing_city,billing_postcode,billing_country',
], [
'firstname.required' => __('validation.custom_messages.firstname_required'),
'surname.required' => __('validation.custom_messages.surname_required'),
'email.required' => __('validation.custom_messages.email_required'),
'email.email' => __('validation.custom_messages.email_invalid'),
'phone.required' => __('validation.custom_messages.phone_required'),
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
'billing_city.required' => __('validation.custom_messages.billing_city_required'),
'billing_postcode.required' => __('validation.custom_messages.billing_postcode_required'),
'billing_country.required' => __('validation.custom_messages.billing_country_required'),
'billing_state.required' => __('validation.custom_messages.billing_state_required'),
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
$userData = $request->all();
$newEmail = $userData['email'];
unset($userData['email']);
if (strtolower($user->email) !== strtolower($newEmail)) {
$user->tokens()->where('type', 'email-update')->delete();
$token = $user->tokens()->create([
'type' => 'email-update',
'data' => [
'email' => $newEmail,
],
'expires_at' => now()->addMinutes(30),
]);
dispatch(new SendEmail($user->email, new UserEmailUpdateRequest($token->id, $user->email, $newEmail)))->onQueue('mail');
}
$userData['subscribed'] = ($request->get('subscribed', false) === 'on');
$user->update($userData);
$user->save();
session()->flash('message', 'Your account details have been saved');
session()->flash('message-title', 'Details updated');
session()->flash('message-type', 'success');
return redirect()->back();
}
/**
* Remove the specified resource from storage.
*/
public function destroy()
{
/** @var User $user */
$user = auth()->user();
auth()->logout();
$user->delete();
session()->flash('message', 'Your account has been deleted');
session()->flash('message-title', 'Account Deleted');
session()->flash('message-type', 'success');
return redirect()->route('index');
}
public static function getTFAInstance()
{
$tfa = new TwoFactorAuth(new QRCodeProvider(), 'STEMMechanics', 6, 30, Algorithm::Sha512);
$tfa->ensureCorrectTime();
return $tfa;
}
public function show_tfa()
{
$user = auth()->user();
if ($user->tfa_secret === null) {
$tfa = self::getTFAInstance();
$secret = $tfa->createSecret();
return response()->json([
'secret' => $secret,
]);
} else {
abort(404);
}
}
public function show_tfa_image(Request $request)
{
$user = auth()->user();
if ($user->tfa_secret === null && $request->has('secret')) {
$tfa = self::getTFAInstance();
$qrCodeProvider = new QRCodeProvider();
$qrCode = $qrCodeProvider->getQRCodeImage(
$tfa->getQRText($user->email, $request->get('secret')),
200
);
return response()->stream(function () use ($qrCode) {
echo $qrCode;
}, 200, ['Content-Type' => $qrCodeProvider->getMimeType()]);
} else {
abort(404);
}
}
public function post_tfa(Request $request)
{
$user = auth()->user();
if ($user->tfa_secret === null && $request->has('secret') && $request->has('code')) {
$secret = $request->get('secret');
$code = $request->get('code');
$tfa = self::getTFAInstance();
if ($tfa->verifyCode($secret, $code, 4)) {
$user->tfa_secret = $secret;
$user->save();
$codes = $user->generateBackupCodes();
return response()->json([
'success' => true,
'codes' => $codes
]);
} else {
return response()->json([
'success' => false,
]);
}
} else {
abort(403);
}
}
public function destroy_tfa(Request $request)
{
$user = auth()->user();
if ($user->tfa_secret !== null) {
$user->tfa_secret = null;
$user->save();
$user->backupCodes()->delete();
return response()->json([
'success' => true,
]);
} else {
abort(403);
}
}
public function post_tfa_reset_backup_codes(Request $request)
{
$user = auth()->user();
if ($user->tfa_secret !== null) {
$codes = $user->generateBackupCodes();
return response()->json([
'success' => true,
'codes' => $codes
]);
} else {
abort(403);
}
}
}

View File

@@ -1,121 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\AnalyticsConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\AnalyticsRequest;
use App\Models\AnalyticsItemRequest;
use App\Models\AnalyticsSession;
use Illuminate\Http\Request;
class AnalyticsController extends ApiController
{
/**
* AnalyticsController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only([
'index',
'update',
'delete'
]);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if ($request->user() !== null && $request->user()?->hasPermission('admin/analytics') === true) {
$request->rename([
'type' => 'requests.type',
'path' => 'requests.path'
]);
list($collection, $total) = AnalyticsConductor::request($request);
return $this->respondAsResource(
$collection,
['resourceName' => 'session',
'isCollection' => true,
'appendData' => ['total' => $total]
]
);
}//end if
return $this->respondForbidden();
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\AnalyticsSession $session The analytics session.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, AnalyticsSession $session)
{
if ($request->user() !== null && $request->user()?->hasPermission('admin/analytics') === true) {
$session->load(['requests' => function ($query) {
$query->select('session_id', 'type', 'path', 'created_at');
}
]);
foreach ($session->requests as $requestItem) {
$requestItem->makeHidden('session_id');
}
return $this->respondAsResource(
$session,
['resourceName' => 'session']
);
}
return $this->respondForbidden();
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\AnalyticsRequest $request The user request.
* @return \Illuminate\Http\Response
*/
public function store(AnalyticsRequest $request)
{
if (AnalyticsConductor::creatable() === true) {
$analytics = null;
$user = $request->user();
$data = [
'type' => $request->input('type'),
'path' => $request->input('path', ''),
'useragent' => $request->userAgent(),
'ip' => $request->ip()
];
if (
$user !== null &&
$user->hasPermission('admin/analytics') === true &&
$request->has('session') === true
) {
$data['session_id'] = $request->input('session_id');
$analytics = AnalyticsItemRequest::create($data);
} else {
$analytics = AnalyticsItemRequest::create($data);
}
return $this->respondAsResource(
AnalyticsConductor::model($request, $analytics),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}//end if
}
}

View File

@@ -1,243 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\JsonResponse;
use App\Enum\HttpResponseCodes;
use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class ApiController extends Controller
{
/**
* Resource name
* @var string
*/
protected $resourceName = '';
/**
* Return generic json response with the given data.
*
* @param array $data Response data.
* @param integer $respondCode Response status code.
* @param array $headers Response headers.
* @return JsonResponse
*/
public function respondJson(
array $data,
int $respondCode = HttpResponseCodes::HTTP_OK,
array $headers = []
): JsonResponse {
return response()->json($data, $respondCode, $headers);
}
/**
* Return forbidden message
*
* @param string $message Response message.
* @return JsonResponse
*/
public function respondForbidden(
string $message = 'You do not have permission to access the resource.'
): JsonResponse {
return response()->json(['message' => $message], HttpResponseCodes::HTTP_FORBIDDEN);
}
/**
* Return forbidden message
*
* @param string $message Response message.
* @return JsonResponse
*/
public function respondNotFound(string $message = 'The resource was not found.'): JsonResponse
{
return response()->json(['message' => $message], HttpResponseCodes::HTTP_NOT_FOUND);
}
/**
* Return too large message
*
* @param string $message Response message.
* @return JsonResponse
*/
public function respondTooLarge(string $message = 'The request entity is too large.'): JsonResponse
{
return response()->json(['message' => $message], HttpResponseCodes::HTTP_REQUEST_ENTITY_TOO_LARGE);
}
/**
* Return no content.
*
* @return JsonResponse
*/
public function respondNoContent(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_NO_CONTENT);
}
/**
* Return no content
*
* @return JsonResponse
*/
public function respondNotImplemented(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_NOT_IMPLEMENTED);
}
/**
* Return created.
*
* @return JsonResponse
*/
public function respondCreated(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_CREATED);
}
/**
* Return accepted.
*
* @return JsonResponse
*/
public function respondAccepted(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_ACCEPTED);
}
/**
* Return server error.
*
* @return JsonResponse
*/
public function respondServerError(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR);
}
/**
* Return single error message
*
* @param string $message Error message.
* @param integer $responseCode Resource code.
* @return JsonResponse
*/
public function respondError(
string $message,
int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY
): JsonResponse {
return response()->json([
'message' => $message
], $responseCode);
}
/**
* Return formatted errors
*
* @param array $errors Error messages.
* @param integer $responseCode Resource code.
* @return JsonResponse
*/
public function respondWithErrors(
array $errors,
int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY
): JsonResponse {
$keys = array_keys($errors);
$error = $errors[$keys[0]];
if (count($keys) > 1) {
$additional_errors = (count($keys) - 1);
$error .= sprintf(' (and %d more %s', $additional_errors, Str::plural('error', $additional_errors));
}
return response()->json([
'message' => $error,
'errors' => $errors
], $responseCode);
}
/**
* Return resource data
*
* @param array|Model|Collection $data Resource data.
* @param array $options Respond options.
* @param callable|null $validationFn Optional validation function to check the data before responding.
* @return JsonResponse
*/
protected function respondAsResource(
mixed $data,
array $options = [],
$validationFn = null
): JsonResponse {
$isCollection = ($options['isCollection'] ?? false);
$appendData = ($options['appendData'] ?? null);
$resourceName = ($options['resourceName'] ?? '');
$transformResourceName = ($options['transformResourceName'] ?? true);
$respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK);
if ($data === null || ($data instanceof Collection && $data->count() === 0)) {
$validationData = [];
if (array_key_exists('appendData', $options) === true) {
$validationData = $options['appendData'];
}
if ($validationFn === null || $validationFn($validationData) === true) {
return $this->respondNotFound();
}
}
if (empty($resourceName) === true) {
$resourceName = $this->resourceName;
}
if (empty($resourceName) === true) {
$resourceName = get_class($this);
$resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1));
$resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller'));
$resourceName = strtolower($resourceName);
}
$dataArray = [];
if ($data instanceof Collection) {
$dataArray = $data->toArray();
} elseif (is_array($data) === true) {
$dataArray = $data;
} elseif ($data instanceof Model) {
$dataArray = $data->toArray();
}
$resource = [];
if ($isCollection === true) {
$resource = [$transformResourceName === true ? Str::plural($resourceName) : $resourceName => $dataArray];
} else {
$resource = [$transformResourceName === true ? Str::singular($resourceName) : $resourceName => $dataArray];
}
if ($appendData !== null) {
$resource += $appendData;
}
return response()->json($resource, $respondCode);
}
/**
* Get the Controller Model Class name.
*
* @return string
*/
public function getModelClass(): string
{
$controllerClass = static::class;
$modelName = 'App\\Models\\' . Str::replaceLast('Controller', '', Str::afterLast($controllerClass, '\\'));
if (class_exists($modelName) === false) {
return $modelName;
}
return $modelName;
}
}

View File

@@ -1,139 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor;
use App\Conductors\ArticleConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\ArticleRequest;
use App\Models\Media;
use App\Models\Article;
use App\Traits\HasAttachments;
use App\Traits\HasGallery;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ArticleController extends ApiController
{
use HasAttachments;
use HasGallery;
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only([
'store',
'update',
'delete'
]);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = ArticleConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Article $article The article model.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Article $article)
{
if (ArticleConductor::viewable($article) === true) {
return $this->respondAsResource(ArticleConductor::model($request, $article));
}
return $this->respondForbidden();
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\ArticleRequest $request The user request.
* @return \Illuminate\Http\Response
*/
public function store(ArticleRequest $request)
{
if (ArticleConductor::creatable() === true) {
$article = Article::create($request->except(['attachments', 'gallery']));
if ($request->has('attachments') === true) {
$article->addAttachments($request->get('attachments'));
}
if ($request->has('gallery') === true) {
$article->galleryAddMany($request->get('gallery'));
}
return $this->respondAsResource(
ArticleConductor::model($request, $article),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}//end if
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\ArticleRequest $request The article update request.
* @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response
*/
public function update(ArticleRequest $request, Article $article)
{
if (ArticleConductor::updatable($article) === true) {
if ($request->has('attachments') === true) {
$article->deleteAttachments();
$article->addAttachments($request->get('attachments'));
}
if ($request->has('gallery') === true) {
$article->gallery()->delete();
$article->galleryAddMany($request->get('gallery'));
}
$article->update($request->except(['attachments', 'gallery']));
return $this->respondAsResource(ArticleConductor::model($request, $article));
}
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response
*/
public function destroy(Article $article)
{
if (ArticleConductor::destroyable($article) === true) {
$article->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
}

View File

@@ -1,105 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\AuthLoginRequest;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\JsonResponse;
class AuthController extends ApiController
{
/**
* Resource name
* @var string
*/
protected $resourceName = 'user';
/**
* ApplicationController constructor.
*/
public function __construct()
{
// $this->middleware('auth:sanctum')
// ->only(['me']);
}
/**
* Current User details
*
* @param Request $request Current request data.
* @return JsonResponse
*/
public function me(Request $request): JsonResponse
{
$user = $request->user()->makeVisible(['permissions']);
return $this->respondAsResource($user);
}
/**
* Login user with supplied creditials
*
* @param App\Http\Controllers\Api\AuthLoginRequest $request Created request data.
* @return JsonResponse|void
*/
public function login(AuthLoginRequest $request)
{
$user = User::where('email', '=', $request->input('email'))->first();
if (
$user !== null &&
strlen($user->password) > 0 &&
Hash::check($request->input('password'), $user->password) === true
) {
if ($user->email_verified_at === null) {
return $this->respondWithErrors([
'email' => 'Email address has not been verified.'
]);
}
if ($user->disabled === true) {
return $this->respondWithErrors([
'email' => 'Account has been disabled.'
]);
}
$token = $user->createToken('user_token')->plainTextToken;
$user->logins()->create([
'token' => $token,
'login' => now(),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent()
]);
return $this->respondAsResource(
$user->makeVisible(['permissions']),
['appendData' => ['token' => $token]]
);
}//end if
return $this->respondWithErrors([
'email' => 'Invalid email or password',
'password' => 'Invalid email or password',
]);
}
/**
* Logout current user
*
* @param Request $request Current request data.
* @return JsonResponse
*/
public function logout(Request $request): JsonResponse
{
$user = $request->user();
$user->logins()->where('token', $user->currentAccessToken())->update(['logout' => now()]);
$user->currentAccessToken()->delete();
return $this->respondNoContent();
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\ContactSendRequest;
use App\Jobs\SendEmailJob;
use App\Mail\Contact;
class ContactController extends ApiController
{
/**
* Send the request to the site admin by email
*
* @param \App\Http\Requests\User\ContactSendRequest $request Request data.
* @return \Illuminate\Http\Response
*/
public function send(ContactSendRequest $request)
{
dispatch((new SendEmailJob(
config('contact.contact_address'),
new Contact(
$request->input('name'),
$request->input('email'),
$request->input('content')
)
)))->onQueue('mail');
return $this->respondCreated();
}
}

View File

@@ -1,234 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Models\Event;
use App\Conductors\EventConductor;
use App\Conductors\MediaConductor;
use App\Conductors\UserConductor;
use App\Http\Requests\EventRequest;
use App\Models\Media;
use App\Models\User;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class EventController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy', 'userAdd', 'userUpdate', 'userDelete']);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = EventConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Event $event)
{
if (EventConductor::viewable($event) === true) {
return $this->respondAsResource(EventConductor::model($request, $event));
}
return $this->respondForbidden();
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\EventRequest $request The request.
* @return \Illuminate\Http\Response
*/
public function store(EventRequest $request)
{
if (EventConductor::creatable() === true) {
$event = Event::create($request->except(['attachments']));
if ($request->has('attachments') === true) {
$event->addAttachments($request->get('attachments'));
}
return $this->respondAsResource(
EventConductor::model($request, $event),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\EventRequest $request The endpoint request.
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function update(EventRequest $request, Event $event)
{
if (EventConductor::updatable($event) === true) {
if ($request->has('attachments') === true) {
$event->deleteAttachments();
$event->addAttachments($request->get('attachments'));
}
$event->update($request->except(['attachments']));
return $this->respondAsResource(EventConductor::model($request, $event));
}
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function destroy(Event $event)
{
if (EventConductor::destroyable($event) === true) {
$event->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
/**
* List users of Event
* @param Request $request The HTTP request.
* @param Event $event Event model.
* @return JsonResponse
*/
public function userList(Request $request, Event $event): JsonResponse
{
$authUser = $request->user();
$eventUsers = $event->users;
if ($authUser !== null) {
$isAdmin = $authUser->hasPermission('admin/events');
$isEventUser = $eventUsers->contains($authUser->id);
if ($isAdmin === true || $isEventUser === true) {
if ($isAdmin === false) {
$eventUsers = $eventUsers->filter(function ($user) use ($authUser) {
return $user->id === $authUser->id;
});
}
return $this->respondAsResource(
UserConductor::collection($request, $eventUsers),
[
'isCollection' => true,
'resourceName' => 'users'
]
);
}
return $this->respondNotFound();
}//end if
return $this->respondForbidden();
}
/**
* Add user to Event
* @param Request $request The HTTP request.
* @param Event $event Event model.
* @return JsonResponse
*/
public function userAdd(Request $request, Event $event): JsonResponse
{
$authUser = $request->user();
if ($authUser !== null && $authUser->hasPermission('admin/events') === true) {
if ($request->has("users") === true) {
$eventUsers = $event->users()->pluck('user_id')->toArray(); // Get the current users in the event
$requestedUsers = $request->input("users"); // Get the requested users
$usersToAdd = array_diff($requestedUsers, $eventUsers); // Users to add
$usersToRemove = array_diff($eventUsers, $requestedUsers); // Users to remove
// Add missing users
foreach ($usersToAdd as $userToAdd) {
if (User::find($userToAdd) !== null) {
$event->users()->attach($userToAdd);
}
}
// Remove extra users
foreach ($usersToRemove as $userToRemove) {
$event->users()->detach($userToRemove);
}
return $this->respondNoContent();
}//end if
return $this->respondWithErrors(['users' => 'The user list was not found']);
}//end if
return $this->respondForbidden();
}
/**
* Update user
* @param Request $request The HTTP request.
* @param Event $event Event model.
* @return void
*/
public function userUpdate(Request $request, Event $event): void
{
// only admin/events permitted
}
/**
* Delete user from event
*
* @param Request $request The HTTP request.
* @param Event $event Event model.
* @param User $user User model.
* @return JsonResponse
*/
public function userDelete(Request $request, Event $event, User $user): JsonResponse
{
$authUser = $request->user();
if ($authUser !== null && $authUser->hasPermission('admin/events') === true) {
$eventUsers = $event->users;
if ($eventUsers->find($user->id) !== null) {
$eventUsers->detach($user->id);
return $this->respondNoContent();
} else {
return $this->respondNotFound();
}
}
return $this->respondForbidden();
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Models\Media;
use Illuminate\Http\Request;
class InfoController extends ApiController
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$info = [
"version" => "1.0.0",
"max_upload_size" => Media::getMaxUploadSize()
];
return $this->respondJson($info);
}
}

View File

@@ -1,165 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
class LogController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['show']);
}
/**
* Display the specified resource.
*
* @param Request $request The log request.
* @param string $name The log name.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, string $name)
{
if ($request->user()?->hasPermission('logs/' . $name) === true) {
switch (strtolower($name)) {
case 'discord':
$data = [];
$log = $request->get('log');
if ($log === null) {
$log = ['output', 'error'];
} else {
$log = explode(',', strtolower($log));
}
$lines = intval($request->get('lines', 50));
if ($lines > 100) {
$lines = 100;
} elseif ($lines < 0) {
$lines = 1;
}
$before = $request->get('before');
if ($before !== null) {
$before = preg_split(
"/([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/",
$before,
-1,
(PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)
);
if (count($before) !== 6) {
$before = null;
}
}
$after = $request->get('after');
if ($after !== null) {
$after = preg_split(
"/([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/",
$after,
-1,
(PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)
);
if (count($after) !== 6) {
$after = null;
}
}
$logFiles = [
[
'name' => 'output',
'path' => '/home/discordbot/.pm2/logs/stemmech-discordbot-out-0.log'
],[
'name' => 'error',
'path' => '/home/discordbot/.pm2/logs/stemmech-discordbot-error-0.log'
]
];
foreach ($logFiles as $logFile) {
if (in_array($logFile['name'], $log) === true) {
$logContent = '';
if (file_exists($logFile['path']) === true) {
$logContent = file_get_contents($logFile['path']);
}
$logArray = preg_split(
// phpcs:ignore Generic.Files.LineLength.TooLong
"/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}: (?:(?!\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}: )[\s\S])*)/",
$logContent,
-1,
(PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)
);
$logContent = '';
$logLineCount = 0;
$logLineSkip = false;
foreach (array_reverse($logArray) as $logLine) {
$lineDate = preg_split(
"/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2}): /",
$logLine,
-1,
(PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)
);
if (count($lineDate) >= 6) {
$logLineSkip = false;
// Is line before
if (
$before !== null && (
$lineDate[0] > $before[0] ||
$lineDate[1] > $before[1] ||
$lineDate[2] > $before[2] ||
$lineDate[3] > $before[3] ||
$lineDate[4] > $before[4] ||
$lineDate[5] > $before[5]
)
) {
$logLineSkip = true;
continue;
}
// Is line after
if (
$after !== null && (
$after[0] > $lineDate[0] ||
$after[1] > $lineDate[1] ||
$after[2] > $lineDate[2] ||
$after[3] > $lineDate[3] ||
$after[4] > $lineDate[4] ||
$after[5] > $lineDate[5]
)
) {
$logLineSkip = true;
continue;
}
$logLineCount += 1;
}//end if
if ($logLineCount > $lines) {
break;
}
if ($logLineSkip === false) {
$logContent .= $logLine;
}
}//end foreach
$data[$logFile['name']] = $logContent;
}//end if
}//end foreach
return $this->respondJson([
'log' => $data
]);
}//end switch
}//end if
return $this->respondForbidden();
}
}

View File

@@ -1,443 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor;
use App\Conductors\MediaJobConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\MediaRequest;
use App\Models\Media;
use App\Models\MediaJob;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Laravel\Sanctum\PersonalAccessToken;
class MediaController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy']);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = MediaConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
],
function ($options) {
return $options['total'] === 0;
}
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Media $medium The request media.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Media $medium)
{
if (MediaConductor::viewable($medium) === true) {
return $this->respondAsResource(MediaConductor::model($request, $medium));
}
return $this->respondForbidden();
}
/**
* Store a new media resource
*
* @param \App\Http\Requests\MediaRequest $request The uploaded media.
* @return \Illuminate\Http\Response
*/
public function store(MediaRequest $request)
{
// allowed to create a media item
if (MediaConductor::creatable() === false) {
return $this->respondForbidden();
}
// check for file
$file = $request->file('file');
if ($file === null) {
return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']);
}
return $this->storeOrUpdate($request, null);
}
/**
* Update the media resource in storage.
*
* @param \App\Http\Requests\MediaRequest $request The update request.
* @param \App\Models\Media $medium The specified media.
* @return \Illuminate\Http\Response
*/
public function update(MediaRequest $request, Media $medium)
{
// allowed to update a media item
if (MediaConductor::updatable($medium) === false) {
return $this->respondForbidden();
}
return $this->storeOrUpdate($request, $medium);
}
/**
* Store a new media resource
*
* @param \App\Http\Requests\MediaRequest $request The uploaded media.
* @param \App\Models\Media|null $medium The specified media.
* @return \Illuminate\Http\Response
*/
public function storeOrUpdate(MediaRequest $request, Media|null $medium)
{
$file = $request->file('file');
if ($file !== null) {
// validate file object
if ($file->isValid() !== true) {
switch ($file->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $this->respondTooLarge();
case UPLOAD_ERR_PARTIAL:
return $this->respondWithErrors([$file => 'The file upload was interrupted.']);
default:
return $this->respondWithErrors(
[$file => 'An error occurred uploading the file to the server.']
);
}
}
if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge();
}
}
// create/get media job
$mediaJob = null;
$data = [];
if ($request->missing('job_id') === true) {
/** @var \App\Models\User */
$user = auth()->user();
$mediaJob = new MediaJob();
$mediaJob->user_id = $user->id;
if ($medium !== null) {
$mediaJob->media_id = $medium->id;
}
if ($request->has('title') === true || $file !== null) {
$data['title'] = $request->get('title', '');
}
if ($request->has('name') === true || $file !== null) {
$data['name'] = (
$request->has('chunk') === true ? $request->get('name', '') : $file->getClientOriginalName()
);
}
if ($file !== null) {
$data['size'] = $request->has('chunk') === true ? intval($request->get('size', 0)) : $file->getSize();
$data['mime_type'] = (
$request->has('chunk') === true ? $request->get('mime_type', '') : $file->getMimeType()
);
}
if ($request->has('storage') === true || $file !== null) {
$data['storage'] = $request->get('storage', '');
}
if ($request->has('security_type') === true || $file !== null) {
$data['security']['type'] = $request->get('security_type', '');
$data['security']['data'] = $request->get('security_data', '');
if ($data['security']['type'] === '') {
$data['security']['data'] = '';
}
if ($medium === null || strcasecmp($data['security']['type'], $medium->security_type) !== 0) {
if ($request->has('storage') === false) {
$mime_type = $request->get('mime_type', $medium === null ? '' : $medium->mime_type);
$data['storage'] = Media::recommendedStorage($mime_type, $data['security']['type']);
}
}
}
if (
array_key_exists('storage', $data) === true && (
array_key_exists('security', $data) === true &&
array_key_exists('type', $data['security']) === true
) &&
array_key_exists('mime_type', $data) === true &&
$data['mime_type'] !== ""
) {
$error = Media::verifyStorage($data['mime_type'], $data['security']['type'], $data['storage']);
// Log::error($data['mime_type'] . ' - ' . $data['security']['type'] . ' - ' . $data['storage']);
switch ($error) {
case Media::STORAGE_VALID:
break;
case Media::STORAGE_MIME_MISSING:
return $this->respondWithErrors(['mime_type' => 'The file type is required.']);
case Media::STORAGE_NOT_FOUND:
return $this->respondWithErrors(['storage' => 'Storage was not found.']);
case Media::STORAGE_INVALID_SECURITY:
return $this->respondWithErrors(
['storage' => 'Storage invalid for this security requirement.']
);
default:
return $this->respondWithErrors(['storage' => 'Storage verification error occurred.']);
}
}
if ($request->has('transform') === true) {
$transform = [];
foreach (explode(',', $request->get('transform', '')) as $value) {
if (is_string($value) === true) {
if (preg_match('/^rotate-(-?\d+)$/', $value, $matches) !== false) {
$transform['rotate'] = $matches[1];
} elseif (preg_match('/^flip-([vh]|vh|hv)$/', $value, $matches) !== false) {
$transform['flip'] = $matches[1];
} elseif (preg_match('/^crop-(\d+)-(\d+)$/', $value, $matches) !== false) {
$transform['crop'] = ['width' => $matches[1], 'height' => $matches[2]];
} elseif (preg_match('/^crop-(\d+)-(\d+)-(\d+)-(\d+)$/', $value, $matches) !== false) {
$transform['crop'] = [
'width' => $matches[1],
'height' => $matches[2],
'x' => $matches[3],
'y' => $matches[4]
];
}
}
}
if (count($transform) > 0) {
$data['transform'] = $transform;
}
}//end if
$mediaJob->setStatusWaiting();
} else {
$mediaJob = MediaJob::find($request->get('job_id'));
if ($mediaJob === null || $mediaJob->exists() === false) {
$this->respondNotFound();
}
$data = json_decode($mediaJob->data, true);
if ($data === null) {
Log::error(`{$mediaJob->id} contains no data`);
return $this->respondServerError();
}
if (array_key_exists('name', $data) === false) {
Log::error(`{$mediaJob->id} data does not contain the name key`);
return $this->respondServerError();
}
}//end if
if ($mediaJob === null) {
Log::error(`media job does not exist`);
return $this->respondServerError();
}
// save uploaded file
if ($file !== null) {
if ($data['name'] === '') {
Log::error(`filename does not exist`);
return $this->respondServerError();
}
$temporaryFilePath = generateTempFilePath(
pathinfo($data['name'], PATHINFO_EXTENSION),
$request->get('chunk', '')
);
copy($file->path(), $temporaryFilePath);
if ($request->has('chunk') === true) {
$data['chunks'][$request->get('chunk', '1')] = $temporaryFilePath;
$data['chunk_count'] = $request->get('chunk_count', 1);
} else {
$data['file'] = $temporaryFilePath;
}
}
$mediaJob->data = json_encode($data, true);
$mediaJob->save();
$mediaJob->process();
return $this->respondAsResource(
MediaJobConductor::model($request, $mediaJob),
['resourceName' => 'media_job', 'respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Media $medium Specified media file.
* @return \Illuminate\Http\Response
*/
public function destroy(Media $medium)
{
if (MediaConductor::destroyable($medium) === true) {
$medium->delete();
return $this->respondNoContent();
}
return $this->respondForbidden();
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Media $media Specified media.
* @return \Illuminate\Http\Response
*/
public function download(Request $request, Media $media): Response
{
$headers = [];
/* Check file exists */
if (Storage::disk($media->storage)->exists($media->name) === false) {
return $this->respondNotFound();
}
$updated_at = Carbon::parse(Storage::disk($media->storage)->lastModified($media->name));
$headerPragma = 'no-cache';
$headerCacheControl = 'max-age=0, must-revalidate';
$headerExpires = $updated_at->toRfc2822String();
/* construct user if can */
$user = $request->user();
if ($user === null && $request->has('token') === true) {
$accessToken = PersonalAccessToken::findToken(urldecode($request->input('token')));
if (
$accessToken !== null && (config('sanctum.expiration') === null ||
$accessToken->created_at->lte(now()->subMinutes(config('sanctum.expiration'))) === false)
) {
$user = $accessToken->tokenable;
}
}
if ($media->security_type === '') {
/* no security */
$headerPragma = 'public';
$headerExpires = $updated_at->addMonth()->toRfc2822String();
} elseif (strcasecmp('password', $media->security_type) === 0) {
/* password */
if (
($user === null || $user->hasPermission('admin/media') === false) &&
($request->has('password') === false || $request->get('password') !== $media->security_data)
) {
return $this->respondForbidden();
}
} elseif (strcasecmp('permission', $media->security_type) === 0) {
/* permission */
if (
$user === null || (
$user->hasPermission('admin/media') === false &&
$user->hasPermission($media->security_data) === false
)
) {
return $this->respondForbidden();
}
}//end if
// deepcode ignore InsecureHash: Browsers expect Etag to be a md5 hash
$headerEtag = md5($updated_at->format('U'));
$headerLastModified = $updated_at->toRfc2822String();
$headers = [
'Cache-Control' => $headerCacheControl,
'Content-Disposition' => sprintf('inline; filename="%s"', basename($media->name)),
'Etag' => $headerEtag,
'Expires' => $headerExpires,
'Last-Modified' => $headerLastModified,
'Pragma' => $headerPragma,
];
$server = request()->server;
$requestModifiedSince = $server->has('HTTP_IF_MODIFIED_SINCE') &&
$server->get('HTTP_IF_MODIFIED_SINCE') === $headerLastModified;
$requestNoneMatch = $server->has('HTTP_IF_NONE_MATCH') &&
$server->get('HTTP_IF_NONE_MATCH') === $headerEtag;
if ($requestModifiedSince === true || $requestNoneMatch === true) {
return response()->make('', 304, $headers);
}
$headers['Content-Type'] = Storage::disk($media->storage)->mimeType($media->name);
$headers['Content-Length'] = Storage::disk($media->storage)->size($media->name);
$headers['Content-Disposition'] = 'attachment; filename="' . basename($media->name) . '"';
$stream = Storage::disk($media->storage)->readStream($media->name);
return response()->stream(
function () use ($stream) {
while (ob_get_level() > 0) {
ob_end_flush();
}
fpassthru($stream);
},
200,
$headers
);
}
/**
* Validate a File item in a request is valid
*
* @param UploadedFile $file The file to validate.
* @param string $errorKey The error key to use.
* @return JsonResponse|null
*/
private function validateFileObject(UploadedFile $file, string $errorKey = 'file'): JsonResponse|null
{
if ($file->isValid() !== true) {
switch ($file->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $this->respondTooLarge();
case UPLOAD_ERR_PARTIAL:
return $this->respondWithErrors([$errorKey => 'The file upload was interrupted.']);
default:
return $this->respondWithErrors(
[$errorKey => 'An error occurred uploading the file to the server.']
);
}
}
if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge();
}
return null;
}
}

View File

@@ -1,52 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaJobConductor;
use App\Http\Controllers\Api\ApiController;
use App\Models\MediaJob;
use Illuminate\Http\Request;
class MediaJobController extends ApiController
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = MediaJobConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total],
'resourceName' => 'media_job'
],
function ($options) {
return $options['total'] === 0;
}
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\MediaJob $mediaJob The request media job.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, MediaJob $mediaJob)
{
if (MediaJobConductor::viewable($mediaJob) === true) {
return $this->respondAsResource(
MediaJobConductor::model($request, $mediaJob),
['resourceName' => 'media_job']
);
}
return $this->respondForbidden();
}
}

View File

@@ -1,234 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use thiagoalessio\TesseractOCR\TesseractOCR;
use FFMpeg;
use App\Enum\CurlErrorCodes;
class OCRController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
// $this->middleware('auth:sanctum')
// ->only(['show']);
}
/**
* Display the specified resource.
*
* @param Request $request The log request.
* @return \Illuminate\Http\Response
*/
public function show(Request $request)
{
// if ($request->user()?->hasPermission('logs/' . $name) === true) {
$url = $request->get('url');
if ($url !== null) {
$data = ['ocr' => []];
$filters = $request->get('filters', ['tesseract']);
if (is_array($filters) === false) {
$filters = explode(',', $filters);
}
$tesseractOEM = $request->get('tesseract.oem');
$tesseractDigits = $request->get('tesseract.digits');
$tesseractAllowlist = $request->get('tesseract.allowlist');
// Download URL
$urlDownloadFilePath = tempnam(sys_get_temp_dir(), 'download');
$maxDownloadSize = (1024 * 1024); // 1MB
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// We need progress updates to break the connection mid-way
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // more progress info
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function (
$downloadSize,
$downloaded,
$uploadSize,
$uploaded
) use ($maxDownloadSize) {
return ($downloaded > $maxDownloadSize) ? 1 : 0;
});
$curlResult = curl_exec($ch);
$curlError = curl_errno($ch);
$curlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
curl_close($ch);
if ($curlError !== 0) {
$error = 'File size is larger then allowed';
if ($curlError !== CurlErrorCodes::CURLE_ABORTED_BY_CALLBACK) {
$error = CurlErrorCodes::getMessage($curlError);
}
return $this->respondWithErrors(['url' => $error]);
}
// Save url file
file_put_contents($urlDownloadFilePath, $curlResult);
$urlDownloadFilePathBase = preg_replace('/\\.[^.\\s]{3,4}$/', '', $urlDownloadFilePath);
// tesseract (overall)
$ocr = null;
foreach ($filters as $filterItem) {
if (str_starts_with($filterItem, 'tesseract') === true) {
$ocr = new TesseractOCR();
$ocr->image($urlDownloadFilePath);
if ($tesseractOEM !== null) {
$ocr->oem($tesseractOEM);
}
if ($tesseractDigits !== null) {
$ocr->digits();
}
if ($tesseractAllowlist !== null) {
$ocr->allowlist($tesseractAllowlist);
}
break;
}
}
// Image Filter Function
$tesseractImageFilterFunc = function ($filter, $options = null) use ($curlResult, $curlSize, $ocr) {
$result = '';
$img = imagecreatefromstring($curlResult);
if (
$img !== false && (($options !== null && imagefilter($img, $filter, $options) === true) ||
($options === null && imagefilter($img, $filter) === true))
) {
ob_start();
imagepng($img);
$imgData = ob_get_contents();
ob_end_clean();
$imgDataSize = strlen($imgData);
$ocr->imageData($imgData, $imgDataSize);
imagedestroy($img);
$result = $ocr->run(500);
}
return $result;
};
// Image Scale Function
$tesseractImageScaleFunc = function ($scaleFunc) use ($curlResult, $ocr) {
$result = '';
$srcImage = imagecreatefromstring($curlResult);
$srcWidth = imagesx($srcImage);
$srcHeight = imagesy($srcImage);
$dstWidth = $scaleFunc($srcWidth);
$dstHeight = $scaleFunc($srcHeight);
$dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
ob_start();
imagepng($dstImage);
$imgData = ob_get_contents();
ob_end_clean();
$imgDataSize = strlen($imgData);
imagedestroy($srcImage);
imagedestroy($dstImage);
$ocr->imageData($imgData, $imgDataSize);
$result = $ocr->run(500);
return $result;
};
// filter: tesseract
if (in_array('tesseract', $filters) === true) {
$data['ocr']['tesseract'] = $ocr->run(500);
}
// filter: tesseract.grayscale
if (in_array('tesseract.grayscale', $filters) === true) {
$data['ocr']['tesseract.grayscale'] = $tesseractImageFilterFunc(IMG_FILTER_GRAYSCALE);
}
// filter: tesseract.double_scale
if (in_array('tesseract.double_scale', $filters) === true) {
$data['ocr']['tesseract.double_scale'] = $tesseractImageScaleFunc(function ($size) {
return $size * 2;
});
}
// filter: tesseract.half_scale
if (in_array('tesseract.half_scale', $filters) === true) {
$data['ocr']['tesseract.half_scale'] = $tesseractImageScaleFunc(function ($size) {
return $size / 2;
});
}
// filter: tesseract.edgedetect
if (in_array('tesseract.edgedetect', $filters) === true) {
$data['ocr']['tesseract.edgedetect'] = $tesseractImageFilterFunc(IMG_FILTER_EDGEDETECT);
}
// filter: tesseract.mean_removal
if (in_array('tesseract.mean_removal', $filters) === true) {
$data['ocr']['tesseract.mean_removal'] = $tesseractImageFilterFunc(IMG_FILTER_MEAN_REMOVAL);
}
// filter: tesseract.negate
if (in_array('tesseract.negate', $filters) === true) {
$data['ocr']['tesseract.negate'] = $tesseractImageFilterFunc(IMG_FILTER_NEGATE);
}
// filter: tesseract.pixelate
if (in_array('tesseract.pixelate', $filters) === true) {
$data['ocr']['tesseract.pixelate'] = $tesseractImageFilterFunc(IMG_FILTER_PIXELATE, 3);
}
// filter: keras
if (in_array('keras', $filters) === true) {
$cmd = '/usr/bin/python3 ' . base_path() . '/scripts/keras_oc.py ' . urlencode($url);
$command = escapeshellcmd($cmd);
$output = shell_exec($cmd);
if ($output !== null && strlen($output) > 0) {
$output = substr($output, (strpos($output, '----------START----------') + 25));
} else {
$output = '';
}
$data['ocr']['keras'] = $output;
}
unlink($urlDownloadFilePath);
return $this->respondJson($data);
}//end if
return $this->respondWithErrors(['url' => 'url is missing']);
}
// $ffmpeg = FFMpeg\FFMpeg::create();
// // Load the input video
// $inputFile = $ffmpeg->open('input.mp4');
// // Split the video into individual frames
// $videoFrames = $inputFile->frames();
// foreach ($videoFrames as $frame) {
// // Save the frame as a PNG
// $frame->save(new FFMpeg\Format\Video\PNG(), 'frame-' . $frame->getMetadata('pts') . '.png');
// // Pass the PNG to Tesseract for processing
// exec("tesseract frame-" . $frame->getMetadata('pts') . ".png output");
// }
// // Read the output from Tesseract
// $text = file_get_contents("output.txt");
// // Do something with the text from Tesseract
// echo $text;
}

View File

@@ -1,111 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\ShortlinkConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\ShortlinkRequest;
use App\Models\Shortlink;
use Illuminate\Http\Request;
class ShortlinkController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy']);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = ShortlinkConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
],
function ($options) {
return $options['total'] === 0;
}
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Shortlink $shortlink The request shortlink.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Shortlink $shortlink)
{
if (ShortlinkConductor::viewable($shortlink) === true) {
return $this->respondAsResource(ShortlinkConductor::model($request, $shortlink));
}
return $this->respondForbidden();
}
/**
* Store a new media resource
*
* @param \App\Http\Requests\ShortlinkRequest $request The shortlink.
* @return \Illuminate\Http\Response
*/
public function store(ShortlinkRequest $request)
{
if (ShortlinkConductor::creatable() === true) {
$shortlink = Shortlink::create($request->all());
return $this->respondAsResource(
ShortlinkConductor::model($request, $shortlink),
['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
);
}//end if
return $this->respondForbidden();
}
/**
* Update the media resource in storage.
*
* @param \App\Http\Requests\ShortlinkRequest $request The update request.
* @param \App\Models\Shortlink $shortlink The specified shortlink.
* @return \Illuminate\Http\Response
*/
public function update(ShortlinkRequest $request, Shortlink $shortlink)
{
if (ShortlinkConductor::updatable($shortlink) === true) {
$shortlink->update($request->all());
return $this->respondAsResource(ShortlinkConductor::model($request, $shortlink));
}//end if
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Shortlink $shortlink Specified shortlink.
* @return \Illuminate\Http\Response
*/
public function destroy(Shortlink $shortlink)
{
if (ShortlinkConductor::destroyable($shortlink) === true) {
$shortlink->delete();
return $this->respondNoContent();
}
return $this->respondForbidden();
}
}

View File

@@ -1,369 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\EventConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\UserRequest;
use App\Http\Requests\UserForgotPasswordRequest;
use App\Http\Requests\UserRegisterRequest;
use App\Http\Requests\UserResendVerifyEmailRequest;
use App\Http\Requests\UserResetPasswordRequest;
use App\Http\Requests\UserVerifyEmailRequest;
use App\Jobs\SendEmailJob;
use App\Mail\ChangedEmail;
use App\Mail\ChangedPassword;
use App\Mail\ChangeEmailVerify;
use App\Mail\ForgotPassword;
use App\Mail\EmailVerify;
use App\Models\User;
use App\Models\UserCode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Conductors\UserConductor;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Container\BindingResolutionException;
class UserController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->except([
'index',
'show',
'register',
'exists',
'forgotPassword',
'resetPassword',
'verifyEmail',
'resendVerifyEmailCode',
'eventList',
]);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = UserConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Store a newly created user in the database.
*
* @param \App\Http\Requests\UserRequest $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function store(UserRequest $request)
{
if (UserConductor::creatable() === true) {
$user = User::create($request->all());
return $this->respondAsResource(
UserConductor::model($request, $user),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}
}
/**
* Display the specified user.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\User $user The user model.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, User $user)
{
if (UserConductor::viewable($user) === true) {
return $this->respondAsResource(UserConductor::model($request, $user));
}
return $this->respondForbidden();
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\UserRequest $request The user update request.
* @param \App\Models\User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function update(UserRequest $request, User $user)
{
if (UserConductor::updatable($user) === true) {
$input = [];
$updatable = ['first_name', 'last_name', 'email', 'phone', 'password', 'display_name'];
if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']);
}
$input = $request->only($updatable);
if (array_key_exists('password', $input) === true) {
$input['password'] = Hash::make($request->input('password'));
}
$user->update($input);
return $this->respondAsResource(UserConductor::model($request, $user));
}
return $this->respondForbidden();
}
/**
* Remove the user from the database.
*
* @param \App\Models\User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function destroy(User $user)
{
if (UserConductor::destroyable($user) === true) {
$user->delete();
return $this->respondNoContent();
}
return $this->respondForbidden();
}
/**
* Register a new user
*
* @param \App\Http\Requests\UserRegisterRequest $request The register user request.
* @return JsonResponse
*/
public function register(UserRegisterRequest $request): JsonResponse
{
try {
$userData = $request->only([
'first_name',
'last_name',
'email',
'phone',
'password',
'display_name',
]);
$userData['password'] = Hash::make($userData['password']);
$user = User::where('email', $request->input('email'))
->whereNull('password')
->first();
if ($user === null) {
$user = User::create($userData);
} else {
unset($userData['email']);
$user->update($userData);
}//end if
$code = $user->codes()->create([
'action' => 'verify-email',
]);
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
return response()->json([
'message' => 'Check your email for a welcome code.'
]);
} catch (\Exception $e) {
return response()->json([
'message' => 'A server error occurred. Please try again later' . $e
], 500);
}//end try
}
/**
* Generates a new reset password code
*
* @param \App\Http\Requests\UserForgotPasswordRequest $request The reset password request.
* @return \Illuminate\Http\Response
*/
public function forgotPassword(UserForgotPasswordRequest $request)
{
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$user->codes()->where('action', 'reset-password')->delete();
$code = $user->codes()->create([
'action' => 'reset-password'
]);
dispatch((new SendEmailJob($user->email, new ForgotPassword($user, $code->code))))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondNotFound();
}
/**
* Resets a user password
*
* @param \App\Http\Requests\UserResetPasswordRequest $request The reset password request.
* @return \Illuminate\Http\Response
*/
public function resetPassword(UserResetPasswordRequest $request)
{
UserCode::clearExpired();
$code = UserCode::where('code', $request->input('code'))->where('action', 'reset-password')->first();
if ($code !== null) {
$user = $code->user()->first();
$code->delete();
$user->codes()->where('action', 'verify-email')->delete();
$user->password = Hash::make($request->input('password'));
if ($user->email_verified_at === null) {
$user->email_verified_at = now();
}
$user->save();
dispatch((new SendEmailJob($user->email, new ChangedPassword($user))))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondError([
'code' => 'The code was not found or has expired.'
]);
}
/**
* Verify an email code
*
* @param \App\Http\Requests\UserVerifyEmailRequest $request The verify email request.
* @return \Illuminate\Http\Response
*/
public function verifyEmail(UserVerifyEmailRequest $request)
{
UserCode::clearExpired();
$code = UserCode::where('code', $request->input('code'))->where('action', 'verify-email')->first();
if ($code !== null) {
$user = $code->user()->first();
$new_email = $code->data;
if ($new_email === null) {
if ($user->email_verified_at === null) {
$user->email_verified_at = now();
}
} else {
dispatch((new SendEmailJob($user->email, new ChangedEmail($user, $user->email, $new_email))))
->onQueue('mail');
$user->email = $new_email;
$user->email_verified_at = now();
}
$code->delete();
$user->save();
return $this->respondNoContent();
}//end if
return $this->respondWithErrors([
'code' => 'The code was not found or has expired.'
]);
}
/**
* Resend a new verify email
*
* @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend verify email request.
* @return JsonResponse
*/
public function resendVerifyEmail(UserResendVerifyEmailRequest $request): JsonResponse
{
UserCode::clearExpired();
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$code = $user->codes()->where('action', 'verify-email')->first();
$code->regenerate();
$code->save();
if ($code->data === null) {
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
} else {
dispatch((new SendEmailJob($user->email, new ChangeEmailVerify($user, $code->code, $code->data))))
->onQueue('mail');
}
}
return response()->json(['message' => 'Verify email sent if user registered and required']);
}
/**
* Resend verification email
*
* @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend user request.
* @return \Illuminate\Http\Response
*/
public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request)
{
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$user->codes()->where('action', 'verify-email')->delete();
if ($user->email_verified_at === null) {
$code = $user->codes()->create([
'action' => 'verify-email'
]);
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
}
return $this->respondNoContent();
}
return $this->respondNotFound();
}
/**
* Return a JSON event list of a user.
*
* @param Request $request The http request.
* @param User $user The specified user.
* @return JsonResponse
*/
public function eventList(Request $request, User $user): JsonResponse
{
if (
$request->user() !== null && (
$request->user() === $user || $request->user()->hasPermission('admin/events') === true
)
) {
$collection = $user->events;
$total = $collection->count();
$collection = EventConductor::collection($request, $collection);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
} else {
return $this->respondForbidden();
}
}
}

View File

@@ -0,0 +1,319 @@
<?php
namespace App\Http\Controllers;
use App\Jobs\SendEmail;
use App\Mail\UserEmailUpdateConfirm;
use App\Mail\UserLogin;
use App\Mail\UserLoginBackupCode;
use App\Mail\UserRegister;
use App\Mail\UserWelcome;
use App\Models\Token;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class AuthController extends Controller
{
/**
* Show the login form or if token present, process the login
*
* @param Request $request
* @return View|RedirectResponse
*/
public function showLogin(Request $request): View|RedirectResponse
{
if (auth()->check()) {
return redirect()->action([HomeController::class, 'index']);
}
$token = $request->query('token');
if ($token) {
return $this->LoginByToken($token);
}
return view('auth.login');
}
/**
* Process the login form
*
* @param Request $request
* @return View|RedirectResponse
*/
public function postLogin(Request $request): View|RedirectResponse
{
$request->validate([
'email' => 'required|email',
'captcha' => 'required_captcha',
], [
'email.required' => __('validation.custom_messages.email_required'),
'email.email' => __('validation.custom_messages.email_invalid'),
]);
$forceEmailLogin = false;
if($request->has('code')) {
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
if($user) {
$tfa = AccountController::getTFAInstance();
if ($request->code && $tfa->verifyCode($user->tfa_secret, $request->code, 4)) {
$data = ['url' => session()->pull('url.intended', null)];
return $this->loginByUser($user, $data);
}
}
return view('auth.login-2fa', ['email' => $request->email])->withErrors([
'code' => 'The 2FA code is not valid',
]);
}
if($request->has('backup_code')) {
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
if($user) {
if($user->verifyBackupCode($request->backup_code)) {
$data = ['url' => session()->pull('url.intended', null)];
dispatch(new SendEmail($user->email, new UserLoginBackupCode($user->email)))->onQueue('mail');
return $this->loginByUser($user, $data);
}
}
return view('auth.login-2fa', ['email' => $request->email, 'method' => 'backup'])->withErrors([
'backup_code' => 'The backup code is not valid',
]);
}
if($request->has('method')) {
if($request->get('method') === 'email') {
$forceEmailLogin = true;
} else {
abort(404);
}
}
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
if ($user) {
if (!$forceEmailLogin && $user->tfa_secret !== null) {
return view('auth.login-2fa', ['user' => $user]);
}
$token = $user->tokens()->create([
'type' => 'login',
'data' => ['url' => session()->pull('url.intended', null)],
]);
dispatch(new SendEmail($user->email, new UserLogin($token->id, $user->getName(), $user->email)))->onQueue('mail');
return view('auth.login-link');
}
session()->flash('status', 'not-found');
return view('auth.login');
}
/**
* Process the login by token
*
* @param string $tokenStr
* @return View|RedirectResponse
*/
public function loginByToken(string $tokenStr): View|RedirectResponse
{
$token = Token::where('id', $tokenStr)
->where('type', 'login')
->where('expires_at', '>', now())
->first();
if ($token) {
$user = $token->user;
if($user) {
$token->delete();
return $this->loginByUser($user, $token->data);
}
}
session()->flash('message', 'That token has expired or is invalid');
session()->flash('message-title', 'Log in failed');
session()->flash('message-type', 'danger');
return view('auth.login');
}
/**
* Process the login by user
*
* @param User $user
* @param array $data
* @return RedirectResponse
*/
public function loginByUser(User $user, array $data = [])
{
$url = null;
if($data && isset($data->url) && $data->url) {
$url = $data->url;
}
Auth::login($user);
session()->flash('message', 'You have been logged in');
session()->flash('message-title', 'Logged in');
session()->flash('message-type', 'success');
if($url) {
return redirect($url);
}
return redirect()->action([HomeController::class, 'index']);
}
/**
* Process the user logout
*
* @return RedirectResponse
*/
public function logout(): RedirectResponse
{
auth()->logout();
session()->flash('message', 'You have been logged out');
session()->flash('message-title', 'Logged out');
session()->flash('message-type', 'warning');
return redirect()->route('index');
}
/**
* Show the registration form or if token present, process the registration
*
* @param Request $request
* @return View|RedirectResponse
*/
public function showRegister(Request $request): View|RedirectResponse
{
if (auth()->check()) {
return redirect()->route('index');
}
$tokenStr = $request->query('token');
if ($tokenStr) {
$token = Token::where('id', $tokenStr)
->where('type', 'register')
->where('expires_at', '>', now())
->first();
if ($token) {
$user = $token->user;
if ($user) {
$user->email_verified_at = now();
$user->save();
$user->tokens()->where('type', 'register')->delete();
dispatch(new SendEmail($user->email, new UserWelcome($user->email)))->onQueue('mail');
$this->loginByUser($user);
return redirect()->route('index');
}
}
session()->flash('message', 'That token has expired or is invalid');
session()->flash('message-title', 'Registration failed');
session()->flash('message-type', 'danger');
}
return view('auth.register');
}
/**
* Process the registration form
*
* @param Request $request
* @return View|RedirectResponse
*/
public function postRegister(Request $request): View|RedirectResponse
{
$request->validate([
'email' => 'required|email',
'captcha' => 'required_captcha',
], [
'email.required' => __('validation.custom_messages.email_required'),
'email.email' => __('validation.custom_messages.email_invalid')
]);
$key = $request->get('name', '');
$passHoneypot = ($key === 'AC9E94587F163AD93174FBF3DFDF9645B886960F2F8DD6D60F81CDB6DCDA3BC33');
$user = User::where('email', $request->email)->first();
if($user) {
if($user->email_verified_at !== null) {
return redirect()->back()->withInput()->withErrors([
'email' => __('validation.custom_messages.email_exists'),
]);
}
} else if($passHoneypot) {
$user = User::create([
'email' => $request->email,
]);
}
if($passHoneypot) {
Log::channel('honeypot')->info('Valid key used for registration using email: ' . $request->email . ', ip address: ' . $request->ip() . ', user agent: ' . $request->userAgent());
$user->tokens()->where('type', 'register')->delete();
$token = $user->tokens()->create([
'type' => 'register',
'data' => ['url' => session()->pull('url.intended', null)],
]);
dispatch(new SendEmail($user->email, new UserRegister($token->id, $user->email)))->onQueue('mail');
} else {
Log::channel('honeypot')->info('Invalid key used for registration using email: ' . $request->email . ', ip address: ' . $request->ip() . ', user agent: ' . $request->userAgent() . ', key: ' . $key);
}
return view('auth.register-link');
}
/**
* Confirm the user email update.
*
* @param Request $request
* @return RedirectResponse
*/
public function updateEmail(Request $request): RedirectResponse
{
$tokenStr = $request->query('token');
$token = Token::where('id', $tokenStr)
->where('type', 'email-update')
->where('expires_at', '>', now())
->first();
if($token && $token->user) {
if($token->data && isset($token->data['email'])) {
$user = $token->user;
$user->email = $token->data['email'];
$user->email_verified_at = now();
$user->save();
$user->tokens()->where('type', 'email-update')->delete();
session()->flash('message', 'Your email has been updated');
session()->flash('message-title', 'Email updated');
session()->flash('message-type', 'success');
dispatch(new SendEmail($user->email, new UserEmailUpdateConfirm($user->email)))->onQueue('mail');
return redirect()->route('index');
}
}
session()->flash('message', 'That token has expired or is invalid');
session()->flash('message-title', 'Email update failed');
session()->flash('message-type', 'danger');
return redirect()->route('index');
}
}

View File

@@ -2,12 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; abstract class Controller
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{ {
use AuthorizesRequests; //
use ValidatesRequests;
} }

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Workshop;
class HomeController extends Controller
{
public function index()
{
$posts = Post::query()->orderBy('created_at', 'desc')->limit(4)->get();
$workshops = Workshop::query()->where('starts_at', '>', now())->where('status', '!=', 'private')->orderBy('starts_at', 'asc')->limit(4)->get();
return view('home', [
'posts' => $posts,
'workshops' => $workshops,
]);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers;
use App\Models\Location;
use App\Models\Post;
use Illuminate\Http\Request;
class LocationController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = Location::query();
if($request->has('search')) {
$query->where('name', 'like', '%' . $request->search . '%');
$query->orWhere('address', 'like', '%' . $request->search . '%');
}
$locations = $query->orderBy('name')->paginate(12)->onEachSide(1);
return view('admin.location.index', [
'locations' => $locations
]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('admin.location.edit');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'name' => 'required',
'address_url' => 'nullable|url',
], [
// 'firstname.required' => __('validation.custom_messages.firstname_required'),
// 'surname.required' => __('validation.custom_messages.surname_required'),
]);
Location::create(array_merge(
$request->all(),
));
session()->flash('message', 'Location has been created');
session()->flash('message-title', 'Location created');
session()->flash('message-type', 'success');
return redirect()->route('admin.location.index');
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Location $location)
{
return view('admin.location.edit', ['location' => $location]);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Location $location)
{
$request->validate([
'name' => 'required',
'address_url' => 'url',
], [
// 'firstname.required' => __('validation.custom_messages.firstname_required'),
// 'surname.required' => __('validation.custom_messages.surname_required'),
]);
$location->update($request->all());
session()->flash('message', 'Location has been updated');
session()->flash('message-title', 'Location updated');
session()->flash('message-type', 'success');
return redirect()->route('admin.location.index');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Location $location)
{
$location->delete();
session()->flash('message', 'Location has been deleted');
session()->flash('message-title', 'Location deleted');
session()->flash('message-type', 'danger');
return redirect()->route('admin.location.index');
}
}

View File

@@ -0,0 +1,549 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\FileInvalidException;
use App\Exceptions\FileTooLargeException;
use App\Helpers;
use App\Models\Media;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
class MediaController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
if(!$request->wantsJson()) {
abort(404);
}
$media = $this->getMedia($request);
return response()->json($media);
}
public function admin_index(Request $request)
{
$media = $this->getMedia($request);
return view('admin.media.index', [
'media' => $media,
]);
}
public function getMedia(Request $request)
{
$query = Media::query();
$perPage = $request->input('per_page', 25);
if(!empty($request->get('search'))) {
$query->where(function($query) use ($request) {
$query->where('title', 'like', '%' . $request->search . '%');
$query->orWhere('name', 'like', '%' . $request->search . '%');
});
}
if($request->has('mime_type')) {
$mime_types = explode(',', $request->mime_type);
$query->where(function ($query) use ($mime_types) {
foreach ($mime_types as $mime_type) {
$mime_type = str_replace('*', '%', $mime_type);
$query->orWhere('mime_type', 'like', $mime_type);
}
});
}
$media = $query->orderBy('created_at', 'desc');
if($request->wantsJson() && !(empty($request->input('selected'))) && empty($request->get('search')) && !$request->has('page')) {
$selected = $request->input('selected')[0];
$selectedMedia = $media->get();
$selectedMediaIndex = $selectedMedia->search(function ($item) use ($selected) {
return $item->name == $selected;
});
if ($selectedMediaIndex !== false) {
$page = intdiv($selectedMediaIndex, $perPage) + 1;
$request->merge(['page' => $page]);
}
}
$media = $media->paginate($perPage)->onEachSide(1);
// Transform the 'password' field of each item in the collection
$media->getCollection()->transform(function ($item) {
$item->password = $item->password ? 'yes' : null;
return $item;
});
return $media;
}
/**
* Display the specified resource.
*/
public function show(Request $request, Media $media)
{
if(!$request->wantsJson()) {
abort(404);
}
return response()->json($media);
}
/**
* Show the form for creating a new resource.
*/
public function admin_create()
{
return view('admin.media.edit');
}
/**
* Store a newly created resource in storage.
*/
public function admin_store(Request $request)
{
$file = null;
$cleanupPath = null;
// Check if the endpoint received a file...
if($request->hasFile('file')) {
try {
$file = $this->upload($request);
if(is_array($file) && !empty($file['chunk'])) {
return response()->json([
'message' => 'Chunk stored',
'upload_token' => $file['token'] ?? null,
]);
} else if(!$file) {
return response()->json([
'message' => 'An error occurred processing the file.',
'errors' => [
'file' => 'An error occurred processing the file.'
]
], 422);
}
if(!$request->has('title')) {
return response()->json([
'message' => 'The file ' . $file->getClientOriginalName() . ' has been uploaded',
'file' => [
'name' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'mime_type' => $file->getMimeType()
]
]);
}
} catch(\Exception $e) {
return response()->json([
'message' => $e->getMessage(),
'errors' => [
'file' => $e->getMessage()
]
], 422);
}
// else check if it received a file name of a previous upload...
} else if($request->has('upload_token') || $request->has('file')) {
$uploadToken = $request->input('upload_token', $request->input('file'));
$chunkUploads = session()->get('chunk_uploads', []);
if(!is_string($uploadToken) || !isset($chunkUploads[$uploadToken])) {
return response()->json([
'message' => 'Could not find the referenced file on the server.',
'errors' => [
'file' => 'Could not find the referenced file on the server.'
]
], 422);
}
$tempFileName = $chunkUploads[$uploadToken];
if(!file_exists($tempFileName)) {
return response()->json([
'message' => 'Could not find the referenced file on the server.',
'errors' => [
'file' => 'Could not find the referenced file on the server ('.$tempFileName.').'
]
], 422);
}
$fileMime = mime_content_type($tempFileName);
if($fileMime === false) {
$fileMime = 'application/octet-stream';
}
$fileName = $request->input('filename', 'upload');
$fileName = Helpers::cleanFileName($fileName);
if ($fileName === '') {
$fileName = 'upload';
}
$file = new UploadedFile($tempFileName, $fileName, $fileMime, null, true);
$cleanupPath = $tempFileName;
unset($chunkUploads[$uploadToken]);
session()->put('chunk_uploads', $chunkUploads);
}
// Check there is an actual file
if(!$file) {
return response()->json([
'message' => 'A file is required.',
'errors' => [
'file' => 'A file is required.'
]
], 422);
}
if(!$request->has('title')) {
return response()->json([
'message' => 'A title is required',
'errors' => [
'title' => 'A title is required'
]
], 422);
}
$fileName = $file->getClientOriginalName();
$name = pathinfo($fileName, PATHINFO_FILENAME);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$name = Helpers::cleanFileName($name);
if(Media::find($name . '.' . $extension) !== null) {
$increment = 1;
$name = preg_replace('/-\d+$/', '', $name);
while(Media::find($name . '-' . $increment . '.' . $extension) !== null) {
$increment++;
}
$fileName = $name . '-' . $increment . '.' . $extension;
}
$hash = hash_file('sha256', $file->path());
$storage = Storage::disk('media');
$exists = $storage->exists($hash);
if(!$exists) {
if($file->storeAs('/', $hash, 'media') === false) {
if($request->wantsJson()) {
return response()->json([
'message' => 'A server error occurred uploading the file.',
], 500);
} else {
session()->flash('message', 'A server error occurred uploading the file.');
session()->flash('message-title', 'Upload failed');
session()->flash('message-type', 'danger');
return redirect()->back();
}
}
}
$media = Media::Create([
'title' => $request->get('title', Helpers::filenameToTitle($fileName)),
'user_id' => auth()->id(),
'name' => $fileName,
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'hash' => $hash
]);
if(!$exists) {
$media->generateVariants(false);
} else {
// find media with the same hash that also has variants and copy them
$mediaWithVariants = Media::where('hash', $hash)->where('variants', '!=', '')->orderBy('created_at')->first();
if($mediaWithVariants) {
$media->variants = $mediaWithVariants->variants;
$media->save();
}
}
if(is_string($cleanupPath)) {
$realPath = realpath($cleanupPath);
$tempDir = realpath(sys_get_temp_dir());
if($realPath !== false && $tempDir !== false && str_starts_with($realPath, $tempDir . DIRECTORY_SEPARATOR)) {
@unlink($realPath);
}
}
if($request->wantsJson()) {
return response()->json([
'message' => 'File has been uploaded',
'name' => $media->name,
'size' => $media->size,
'mime_type' => $media->mime_type
]);
} else {
session()->flash('message', 'Media has been uploaded');
session()->flash('message-title', 'Media uploaded');
session()->flash('message-type', 'success');
return redirect()->route('admin.media.index');
}
}
/**
* Show the form for editing the specified resource.
*/
public function admin_edit(Media $media)
{
return view('admin.media.edit', ['medium' => $media]);
}
/**
* Update the specified resource in storage.
*/
public function admin_update(Request $request, Media $media)
{
$max_size = Helpers::getMaxUploadSize();
$validator = Validator::make($request->all(), [
'title' => 'required',
// 'file' => 'nullable|file|max:' . (max(round($max_size / 1024),0)),
], [
'title.required' => __('validation.custom_messages.title_required'),
// 'file.required' => __('validation.custom_messages.file_required'),
// 'file.file' => __('validation.custom_messages.file_file'),
// 'file.max' => __('validation.custom_messages.file_max', ['max' => Helpers::bytesToString($max_size)])
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
$mediaData = $request->all();
// $file = null;
// if($request->has('file')) {
// $file = $request->file('file');
//
// $name = $file->getClientOriginalName();
// $name = Helpers::cleanFileName($name);
// if ($name !== $media->name) {
// if (Media::find($name) !== null) {
// $increment = 2;
// while (Media::find($name . '-' . $increment) !== null) {
// $increment++;
// }
//
// $name = $name . '-' . $increment;
// }
// }
//
// $hash = hash_file('sha256', $file->path());
//
// $storage = Storage::disk('media');
// if (!$storage->exists($hash)) {
// if ($file->storeAs('/', $hash, 'media') === false) {
// session()->flash('message', 'A server error occurred uploading the file.');
// session()->flash('message-title', 'Upload failed');
// session()->flash('message-type', 'danger');
// return redirect()->back();
// }
// }
//
// $mediaData['name'] = $name;
// $mediaData['size'] = $file->getSize();
// $mediaData['mime_type'] = $file->getMimeType();
// $mediaData['hash'] = $hash;
// }
if($request->get('password_clear') === 'on') {
$mediaData['password'] = null;
} else {
$password = $request->get('password');
if($password !== null && $password !== '') {
$mediaData['password'] = password_hash($request->get('password'), PASSWORD_DEFAULT);
} else {
unset($mediaData['password']);
}
}
$media->update($mediaData);
// if($file) {
// $media->generateVariants(false);
// unlink($file);
// }
session()->flash('message', 'Media has been updated');
session()->flash('message-title', 'Media updated');
session()->flash('message-type', 'success');
return redirect()->route('admin.media.index');
}
/**
* Remove the specified resource from storage.
*/
public function admin_destroy(Request $request, Media $media)
{
$media->delete();
session()->flash('message', 'Media has been deleted');
session()->flash('message-title', 'Media deleted');
session()->flash('message-type', 'danger');
if($request->wantsJson()) {
return response()->json([
'success' => true,
'redirect' => route('admin.media.index'),
]);
}
return redirect()->route('admin.media.index');
}
/**
* @throws FileInvalidException
* @throws FileTooLargeException
*/
private function upload(Request $request)
{
$max_size = Helpers::getMaxUploadSize();
$file = $request->file('file');
if(!$file->isValid()) {
throw new FileInvalidException('The file is invalid');
}
$fileName = $request->input('filename', $file->getClientOriginalName());
$fileName = Helpers::cleanFileName($fileName);
if ($fileName === '') {
$extension = strtolower($file->getClientOriginalExtension());
$fileName = 'upload' . ($extension !== '' ? '.' . $extension : '');
}
if(($request->has('filestart') || $request->has('fileappend')) && $request->has('filesize')) {
$fileSize = $request->get('filesize');
if($fileSize > $max_size) {
throw new FileTooLargeException('The file is larger than the maximum size allowed of ' . Helpers::bytesToString($max_size));
}
$chunkUploads = session()->get('chunk_uploads', []);
$uploadToken = $request->input('upload_token');
if($request->has('filestart')) {
$uploadToken = bin2hex(random_bytes(16));
$tempFilePath = tempnam(sys_get_temp_dir(), 'chunk-' . Auth::id() . '-');
if($tempFilePath === false) {
throw new FileInvalidException('Unable to create a temporary upload file.');
}
$chunkUploads[$uploadToken] = $tempFilePath;
session()->put('chunk_uploads', $chunkUploads);
} else {
if(!is_string($uploadToken) || !isset($chunkUploads[$uploadToken])) {
throw new FileInvalidException('Invalid upload token.');
}
$tempFilePath = $chunkUploads[$uploadToken];
}
$filemode = 'a';
if($request->has('filestart')) {
$filemode = 'w';
}
// Append the chunk to the temporary file
$fp = fopen($tempFilePath, $filemode);
if ($fp) {
fwrite($fp, file_get_contents($file->getRealPath()));
fclose($fp);
}
// Check if the upload is complete
if (filesize($tempFilePath) >= $fileSize) {
$fileMime = mime_content_type($tempFilePath);
if($fileMime === false) {
$fileMime = 'application/octet-stream';
}
if(is_string($uploadToken) && isset($chunkUploads[$uploadToken])) {
unset($chunkUploads[$uploadToken]);
session()->put('chunk_uploads', $chunkUploads);
}
return new UploadedFile($tempFilePath, $fileName, $fileMime, null, true);
} else {
return [
'chunk' => true,
'token' => $uploadToken,
];
}
}
return $file;
}
public function download(Request $request, Media $media)
{
$file = $media->path();
if($file === null) {
abort(404, 'File not found');
}
if($media->password !== null && !Auth::user()?->isAdmin()) {
if(!$request->has('password')) {
return view('media-password');
} else {
$password = $request->get('password');
if($password === '' || $password === null) {
return view('media-password', [
'error' => 'Password is required',
]);
}
if(!password_verify(base64_decode($password), $media->password)) {
return view('media-password', [
'error' => 'Password is incorrect',
]);
}
}
}
$variant = '';
$download = false;
$variants = array_keys($media->getVariantTypes());
$query = $request->getQueryString();
if($query !== '') {
$queryList = explode('&', $query);
foreach($queryList as $queryItem) {
$parts = explode('=', $queryItem);
if($variant === '' && in_array($parts[0], $variants) && ($parts[1] === '' || filter_var($parts[1], FILTER_VALIDATE_BOOLEAN))) {
$variant = $parts[0];
}
if($parts[0] === 'download' && ($parts[1] === '' || filter_var($parts[1], FILTER_VALIDATE_BOOLEAN))) {
$download = true;
}
}
}
$mime_type = $media->mime_type;
$name = $media->name;
if($variant !== '') {
$variantFile = $media->getClosestVariant($variant);
$file = $variantFile['file'];
$mime_type = $variantFile['mime_type'];
$name = $variantFile['name'];
}
$headers = [
'Content-Type' => $mime_type,
'Content-Disposition' => ($download ? 'attachment; ' : '') . 'filename="' . $name . '"',
];
return response()->file($file, $headers);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace App\Http\Controllers;
use App\Helpers;
use App\Models\Media;
use App\Models\Post;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = Post::query();
$query->where('status', 'published');
if($request->has('search')) {
$query->where('title', 'like', '%' . $request->search . '%');
$query->orWhere('content', 'like', '%' . $request->search . '%');
}
$posts = $query->orderBy('created_at', 'desc')->paginate(12)->onEachSide(1);
return view('post.index', [
'posts' => $posts
]);
}
/**
* Display a listing of the resource.
*/
public function admin_index(Request $request)
{
$query = Post::query();
if($request->has('search')) {
$query->where('title', 'like', '%' . $request->search . '%');
$query->orWhere('content', 'like', '%' . $request->search . '%');
}
$posts = $query->orderBy('created_at', 'desc')->paginate(12)->onEachSide(1);
return view('admin.post.index', [
'posts' => $posts
]);
}
/**
* Show the form for creating a new resource.
*/
public function admin_create()
{
return view('admin.post.edit');
}
/**
* Store a newly created resource in storage.
*/
public function admin_store(Request $request)
{
$request->validate([
'title' => 'required',
'content' => 'required',
'hero_media_name' => 'required|exists:media,name',
], [
'title.required' => __('validation.custom_messages.title_required'),
'content.required' => __('validation.custom_messages.content_required'),
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
]);
$postData = $request->all();
$postData['user_id'] = auth()->user()->id;
$post = Post::create($postData);
$post->updateFiles($request->input('files'));
$post->updateFiles($request->input('gallery'), 'gallery');
$post->updateFiles($request->input('videos'), 'videos');
session()->flash('message', 'Post has been created');
session()->flash('message-title', 'Post created');
session()->flash('message-type', 'success');
return redirect()->route('admin.post.index');
}
/**
* Display the specified resource.
*/
public function show(Post $post)
{
return view('post.show', ['post' => $post]);
}
/**
* Show the form for editing the specified resource.
*/
public function admin_edit(Post $post)
{
return view('admin.post.edit', ['post' => $post]);
}
/**
* Update the specified resource in storage.
*/
public function admin_update(Request $request, Post $post)
{
$request->validate([
'title' => 'required',
'content' => 'required',
'hero_media_name' => 'required|exists:media,name',
], [
'title.required' => __('validation.custom_messages.title_required'),
'content.required' => __('validation.custom_messages.content_required'),
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
]);
$postData = $request->all();
$post->update($postData);
$post->updateFiles($request->input('files'));
$post->updateFiles($request->input('gallery'), 'gallery');
$post->updateFiles($request->input('videos'), 'videos');
session()->flash('message', 'Post has been updated');
session()->flash('message-title', 'Post updated');
session()->flash('message-type', 'success');
return redirect()->route('admin.post.index');
}
/**
* Remove the specified resource from storage.
*/
public function admin_destroy(Post $post)
{
$post->delete();
session()->flash('message', 'Post has been deleted');
session()->flash('message-title', 'Post deleted');
session()->flash('message-type', 'danger');
return redirect()->route('admin.post.index');
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Workshop;
use Carbon\Carbon;
use Illuminate\Http\Request;
class SearchController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$search = $request->get('q', '');
$search_words = explode(' ', $search); // Split the search query into words[1]
$workshopQuery = Workshop::query()->where('status', '!=', 'draft');
$workshopQuery->where(function ($query) use ($search_words) {
foreach ($search_words as $word) {
$query->orWhere(function ($subQuery) use ($word) {
$subQuery->where('title', 'like', '%' . $word . '%')
->orWhere('content', 'like', '%' . $word . '%')
->orWhereHas('location', function ($locationQuery) use ($word) {
$locationQuery->where('name', 'like', '%' . $word . '%');
});
});
}
});
$workshops = $workshopQuery->orderBy('starts_at', 'desc')
->paginate(6, ['*'], 'workshop');
// $postQuery = Post::query()->where('status', 'published');
// $postQuery->where(function ($query) use ($search_words) {
// foreach ($search_words as $word) {
// $query->where(function ($subQuery) use ($word) {
// $subQuery->where('title', 'like', '%' . $word . '%')
// ->orWhere('content', 'like', '%' . $word . '%');
// });
// }
// });
//
// $posts = $postQuery->orderBy('created_at', 'desc')
// ->paginate(6, ['*'], 'post')
// ->onEachSide(1);
return view('search', [
'workshops' => $workshops,
// 'posts' => $posts,
'search' => $search,
]);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Models\EmailSubscriptions;
use App\Models\SentEmail;
use Illuminate\Http\Request;
class SubscribeController extends Controller
{
/**
* Display a listing of the resource.
*/
public function destroy($email)
{
$emailModel = SentEmail::where('id', $email)->first();
if (!$emailModel) {
// Email not found, redirect to home page with a message
return redirect()->route('index')->with([
'message' => 'The unsubscribe link is invalid or has expired.',
'message-title' => 'Invalid Unsubscribe Link',
'message-type' => 'warning'
]);
}
// Existing unsubscribe logic
$subscriptions = EmailSubscriptions::where('email', $emailModel->recipient)->get();
if ($subscriptions->isEmpty()) {
session()->flash('message', 'You are already unsubscribed.');
session()->flash('message-title', 'Already Unsubscribed');
session()->flash('message-type', 'info');
} else {
EmailSubscriptions::where('email', $emailModel->recipient)->delete();
session()->flash('message', 'You have been successfully unsubscribed.');
session()->flash('message-title', 'Unsubscribed');
session()->flash('message-type', 'success');
}
return redirect()->route('index');
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers;
use App\Models\Ticket;
use Illuminate\Http\Request;
class TicketController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(Ticket $ticket)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Ticket $ticket)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Ticket $ticket)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Ticket $ticket)
{
//
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = User::query();
if($request->has('search')) {
$query->where('firstname', 'like', '%' . $request->search . '%');
$query->orWhere('surname', 'like', '%' . $request->search . '%');
$query->orWhere('phone', 'like', '%' . $request->search . '%');
$query->orWhere('email', 'like', '%' . $request->search . '%');
}
$users = $query->orderBy('created_at', 'desc')->paginate(12)->onEachSide(1);
return view('admin.user.index', [
'users' => $users
]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('admin.user.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'firstname' => '',
'surname' => '',
'email' => 'email|unique:users',
'phone' => '',
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
'billing_postcode' => 'required_with:billing_address,billing_city,billing_country,billing_state',
'billing_country' => 'required_with:billing_address,billing_city,billing_postcode,billing_state',
'billing_state' => 'required_with:billing_address,billing_city,billing_postcode,billing_country',
], [
'firstname.required' => __('validation.custom_messages.firstname_required'),
'surname.required' => __('validation.custom_messages.surname_required'),
'email.required' => __('validation.custom_messages.email_required'),
'email.email' => __('validation.custom_messages.email_invalid'),
'phone.required' => __('validation.custom_messages.phone_required'),
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
'billing_city.required' => __('validation.custom_messages.billing_city_required'),
'billing_postcode.required' => __('validation.custom_messages.billing_postcode_required'),
'billing_country.required' => __('validation.custom_messages.billing_country_required'),
'billing_state.required' => __('validation.custom_messages.billing_state_required'),
]);
User::create($request->all());
session()->flash('message', 'User has been created');
session()->flash('message-title', 'User created');
session()->flash('message-type', 'success');
return redirect()->route('admin.user.index');
}
/**
* Show the form for editing the specified resource.
*/
public function edit(User $user)
{
return view('admin.user.edit', compact('user'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, User $user)
{
$request->validate([
'firstname' => '',
'surname' => '',
'email' => ['email', Rule::unique('users')->ignore($user->id)],
'phone' => '',
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
'billing_postcode' => 'required_with:billing_address,billing_city,billing_country,billing_state',
'billing_country' => 'required_with:billing_address,billing_city,billing_postcode,billing_state',
'billing_state' => 'required_with:billing_address,billing_city,billing_postcode,billing_country',
], [
'firstname.required' => __('validation.custom_messages.firstname_required'),
'surname.required' => __('validation.custom_messages.surname_required'),
'email.required' => __('validation.custom_messages.email_required'),
'email.email' => __('validation.custom_messages.email_invalid'),
'phone.required' => __('validation.custom_messages.phone_required'),
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
'billing_city.required' => __('validation.custom_messages.billing_city_required'),
'billing_postcode.required' => __('validation.custom_messages.billing_postcode_required'),
'billing_country.required' => __('validation.custom_messages.billing_country_required'),
'billing_state.required' => __('validation.custom_messages.billing_state_required'),
]);
$user->update($request->all());
session()->flash('message', 'User details have been updated');
session()->flash('message-title', 'Details updated');
session()->flash('message-type', 'success');
return redirect()->back();
}
/**
* Remove the specified resource from storage.
*/
public function destroy(User $user)
{
if($user->id !== '1') {
$user->delete();
session()->flash('message', 'User has been deleted');
session()->flash('message-title', 'User deleted');
session()->flash('message-type', 'success');
} else {
session()->flash('message', 'You cannot delete the main admin user');
session()->flash('message-title', 'User not deleted');
session()->flash('message-type', 'error');
}
return redirect()->route('admin.user.index');
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace App\Http\Controllers;
use App\Models\Workshop;
use Carbon\Carbon;
use Illuminate\Http\Request;
class WorkshopController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$query = Workshop::query();
$query = $query->where('starts_at', '>=', Carbon::now()->subDays(8))
->orderBy('starts_at', 'asc');
$workshops = $query->paginate(12);
return view('workshop.index', [
'workshops' => $workshops
]);
}
/**
* Display a listing of the resource.
*/
public function past_index()
{
$query = Workshop::query();
$query = $query->where('starts_at', '<', Carbon::now())
->orderBy('starts_at', 'desc');
$workshops = $query->paginate(12);
return view('workshop.index', [
'workshops' => $workshops
]);
}
/**
* Display a listing of the resource.
*/
public function admin_index(Request $request)
{
$query = Workshop::query();
if($request->has('search')) {
$query->where('title', 'like', '%' . $request->search . '%');
$query->orWhere('content', 'like', '%' . $request->search . '%');
}
$workshops = $query->orderBy('starts_at', 'desc')->paginate(12)->onEachSide(1);
return view('admin.workshop.index', [
'workshops' => $workshops
]);
}
/**
* Show the form for creating a new resource.
*/
public function admin_create()
{
return view('admin.workshop.edit');
}
/**
* Store a newly created resource in storage.
*/
public function admin_store(Request $request)
{
$request->validate([
'title' => 'required',
'content' => 'required',
'starts_at' => 'required',
'ends_at' => 'required|after:starts_at',
'publish_at' => 'required',
'closes_at' => 'required',
'status' => 'required',
'hero_media_name' => 'required|exists:media,name',
'registration_data' => 'required_unless:registration,none',
], [
'title.required' => __('validation.custom_messages.title_required'),
'content.required' => __('validation.custom_messages.content_required'),
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
'status.required' => __('validation.custom_messages.status_required'),
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
]);
$workshopData = $request->all();
$workshopData['user_id'] = auth()->user()->id;
if($workshopData['status'] === 'open' && Carbon::parse($workshopData['starts_at'])->lt(Carbon::now())) {
$workshopData['status'] = 'closed';
}
$workshop = Workshop::create($workshopData);
$workshop->updateFiles($request->input('files'));
session()->flash('message', 'Workshop has been created');
session()->flash('message-title', 'Workshop created');
session()->flash('message-type', 'success');
return redirect()->route('admin.workshop.index');
}
/**
* Display the specified resource.
*/
public function show(Workshop $workshop)
{
if(!auth()->user()?->admin && $workshop->status == 'draft') {
abort(404);
}
return view('workshop.show', ['workshop' => $workshop]);
}
/**
* Show the form for editing the specified resource.
*/
public function admin_edit(Workshop $workshop)
{
return view('admin.workshop.edit', ['workshop' => $workshop]);
}
/**
* Update the specified resource in storage.
*/
public function admin_update(Request $request, Workshop $workshop)
{
$request->validate([
'title' => 'required',
'content' => 'required',
'starts_at' => 'required',
'ends_at' => 'required|after:starts_at',
'publish_at' => 'required',
'closes_at' => 'required',
'status' => 'required',
'hero_media_name' => 'required|exists:media,name',
'registration_data' => 'required_unless:registration,none',
], [
'title.required' => __('validation.custom_messages.title_required'),
'content.required' => __('validation.custom_messages.content_required'),
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
'status.required' => __('validation.custom_messages.status_required'),
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
]);
$workshopData = $request->all();
if($workshopData['status'] === 'open' && Carbon::parse($workshopData['starts_at'])->lt(Carbon::now())) {
$workshopData['status'] = 'closed';
}
$workshop->update($workshopData);
$workshop->updateFiles($request->input('files'));
session()->flash('message', 'Workshop has been updated');
session()->flash('message-title', 'Workshop updated');
session()->flash('message-type', 'success');
return redirect()->route('admin.workshop.index');
}
/**
* Remove the specified resource from storage.
*/
public function admin_destroy(Workshop $workshop)
{
$workshop->delete();
session()->flash('message', 'Workshop has been deleted');
session()->flash('message-title', 'Workshop deleted');
session()->flash('message-type', 'danger');
return redirect()->route('admin.workshop.index');
}
/**
* Duplicate the specified resource.
*/
public function admin_duplicate(Workshop $workshop)
{
$newWorkshop = $workshop->replicate();
$newWorkshop->title = $newWorkshop->title . ' (copy)';
$newWorkshop->status = 'draft';
$newWorkshop->save();
foreach($workshop->files as $file) {
$newWorkshop->files()->attach($file->name);
}
session()->flash('message', 'Workshop has been duplicated');
session()->flash('message-title', 'Workshop duplicated');
session()->flash('message-type', 'success');
return redirect()->route('admin.workshop.edit', $newWorkshop);
}
}

View File

@@ -1,73 +0,0 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
// \App\Http\Middleware\TrimStrings::class,
// \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// \App\Http\Middleware\ForceJsonResponse::class,
\App\Http\Middleware\UnmangleRequest::class,
'useSanctumGuard',
\App\Http\Middleware\LogRequest::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'unmangle' => \App\Http\Middleware\UnmangleRequest::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'useSanctumGuard' => \App\Http\Middleware\UseSanctumGuard::class
];
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class Admin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
/* @var User $user */
$user = Auth::user();
if ($user) {
if($user->admin == 1) {
return $next($request);
}
abort(403, 'Forbidden');
}
session()->put('url.intended', url()->current());
return redirect()->route('login');
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param mixed $request Request.
* @return ?string
*/
protected function redirectTo(mixed $request): ?string
{
if ($request->expectsJson() === false) {
return route('login');
}
return null;
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use Closure;
use Illuminate\Http\Request;
class ForceJsonResponse
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
*/
public function handle(Request $request, Closure $next): Response
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Models\AnalyticsItemRequest;
use Symfony\Component\HttpFoundation\Response;
use Closure;
use Illuminate\Http\Request;
class LogRequest
{
/**
* Handle an incoming request.
*
* @param Illuminate\Http\Request $request HTTP Request.
* @param \Closure $next Closure.
* @return Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next): Response
{
// Make it an after middleware
$response = $next($request);
try {
AnalyticsItemRequest::create([
'type' => 'apirequest',
'path' => $request->path(),
]);
return $response;
} catch (\Error $e) {
report($e);
return $response;
}
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param Request $request Request.
* @param \Closure $next Closure.
* @param string|null ...$guards Guards.
* @return Response
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) === true ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check() === true) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var integer
*/
// @codingStandardsIgnoreStart
protected $headers = (Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB);
// @codingStandardsIgnoreEnd
}

View File

@@ -1,47 +0,0 @@
<?php
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UnmangleRequest
{
/**
* Handle an incoming request.
*
* @param Request $request Request.
* @param \Closure $next Next.
* @param string|null ...$guards Guards.
* @return Response response.
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
if (isset($_SERVER['QUERY_STRING']) === true) {
$params = $request->all();
$string = $_SERVER['QUERY_STRING'];
$parts = explode('&', $string);
foreach ($parts as $part) {
$key = $part;
$splitPos = strpos($key, '=');
if ($splitPos !== false) {
$key = urldecode(substr($key, 0, $splitPos));
}
$replace_key = str_replace('.', '_', $key);
if (strpos($key, '.') !== false && array_key_exists($replace_key, $params) === true) {
$params[$key] = $params[$replace_key];
unset($params[$replace_key]);
}
}
$request->replace($params);
}//end if
return $next($request);
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UseSanctumGuard
{
/**
* Handle an incoming request.
*
* @param Request $request Request object.
* @param \Closure $next Closure object.
* @return Response
*/
public function handle(Request $request, Closure $next): Response
{
Auth::shouldUse('sanctum');
return $next($request);
}
}

View File

@@ -1,22 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -1,35 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class AnalyticsRequest extends BaseRequest
{
/**
* Get the validation rules that apply to POST requests.
*
* @return array<string, mixed>
*/
public function postRules(): array
{
return [
'type' => 'required|string',
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules(): array
{
return [
'type' => 'string',
'useragent' => 'string',
'ip' => 'ipv4|ipv6',
'session' => 'number',
];
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class ArticleRequest extends BaseRequest
{
/**
* Get the validation rules that apply to POST requests.
*
* @return array<string, mixed>
*/
public function postRules(): array
{
return [
'slug' => 'required|string|min:6|unique:articles',
'title' => 'required|string|min:6|max:255',
'publish_at' => 'required|date',
'user_id' => 'required|uuid|exists:users,id',
'content' => 'required|string|min:6',
'hero' => 'required|uuid|exists:media,id',
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules(): array
{
return [
'slug' => [
'string',
'min:6',
Rule::unique('articles')->ignoreModel($this->article),
],
'title' => 'string|min:6|max:255',
'publish_at' => 'date',
'user_id' => 'uuid|exists:users,id',
'content' => 'string|min:6',
'hero' => 'uuid|exists:media,id',
];
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AuthLoginRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'email' => 'required|string|min:6|max:255',
'password' => 'required|string|min:6',
];
}
}

View File

@@ -1,107 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class BaseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
if (request()->isMethod('post') === true && method_exists($this, 'postAuthorize') === true) {
return $this->postAuthorize();
} elseif (
(
request()->isMethod('put') === true || request()->isMethod('patch') === true
) && method_exists($this, 'putAuthorize') === true
) {
return $this->putAuthorize();
} elseif (request()->isMethod('delete') === true && method_exists($this, 'destroyAuthorize') === true) {
return $this->deleteAuthorize();
}
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
$rules = [];
if (method_exists($this, 'baseRules') === true) {
$rules = $this->baseRules();
}
if (method_exists($this, 'postRules') === true && request()->isMethod('post') === true) {
$rules = $this->mergeRules($rules, $this->postRules());
} elseif (
method_exists($this, 'putRules') === true && (
request()->isMethod('put') === true || request()->isMethod('patch') === true
)
) {
$rules = $this->mergeRules($rules, $this->putRules());
} elseif (method_exists($this, 'destroyRules') === true && request()->isMethod('delete') === true) {
$rules = $this->mergeRules($rules, $this->destroyRules());
}
return $rules;
}
/**
* Merge two collections of rules.
*
* @param array $collection1 The first collection of rules.
* @param array $collection2 The second collection of rules to merge.
* @return array
*/
private function mergeRules(array $collection1, array $collection2): array
{
$rules = [];
foreach ($collection1 as $key => $ruleset) {
if (array_key_exists($key, $collection2) === true) {
if (is_string($collection1[$key]) === true && is_string($collection2[$key]) === true) {
$rules[$key] = $collection1[$key] . '|' . $collection2[$key];
} else {
$key_ruleset = [];
if (is_array($collection1[$key]) === true) {
$key_ruleset = $collection1[$key];
} elseif (is_string($collection1[$key]) === true) {
$key_ruleset = explode('|', $collection1[$key]);
}
if (is_array($collection2[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, $collection2[$key]);
} elseif (is_string($collection2[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, explode('|', $collection2[$key]));
}
if (count($key_ruleset) > 0) {
$rules[$key] = $key_ruleset;
}
}//end if
} else {
$rules[$key] = $ruleset;
}//end if
}//end foreach
foreach ($collection2 as $key => $ruleset) {
if (array_key_exists($key, $rules) === false) {
$rules[$key] = $collection2[$key];
}
}
return $rules;
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class ContactSendRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|max:255',
'email' => 'required|email|max:255',
'content' => 'required|max:2000',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class EventRequest extends BaseRequest
{
/**
* Apply the base rules to this request
*
* @return array<string, mixed>
*/
public function baseRules(): array
{
return [
'title' => 'min:6',
'location' => [
Rule::in(['online', 'physical']),
],
'address' => 'string|nullable',
'start_at' => 'date',
'end_at' => 'date|after:start_date',
'publish_at' => 'date|nullable',
'status' => [
Rule::in(['draft', 'soon', 'open', 'closed', 'cancelled', 'scheduled', 'full']),
],
'registration_type' => [
Rule::in(['none', 'email', 'link', 'message']),
],
'registration_data' => [
Rule::when(strcasecmp('email', $this->attributes->get('registration_type')) == 0, 'required|email'),
Rule::when(strcasecmp('link', $this->attributes->get('registration_type')) == 0, 'required|url'),
Rule::when(strcasecmp('message', $this->attributes->get('registration_type')) == 0, 'required|message'),
],
'hero' => 'uuid|exists:media,id',
'location_url' => 'sometimes|string|max:255',
];
}
/**
* Apply the additional POST base rules to this request
*
* @return array<string, mixed>
*/
protected function postRules(): array
{
return [
'title' => 'required',
'location' => 'required',
'address' => 'required_if:location,physical',
'start_at' => 'required',
'end_at' => 'required',
'status' => 'required',
'registration_type' => 'required',
'hero' => 'required',
];
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class MediaRequest extends BaseRequest
{
/**
* POST request rules
*
* @return array
*/
public function postRules(): array
{
return [
'job_id' => [
Rule::requiredIf(function () {
return request()->has('chunk') && request('chunk') != 1;
}),
'string',
],
'name' => [
Rule::requiredIf(function () {
return request()->has('chunk') && request('chunk') == 1;
}),
'string',
],
'chunk' => 'required_with:chunk_count|integer|min:1|max:999|lte:chunk_count',
'chunk_count' => 'required_with:chunk|integer|min:1',
];
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class ShortlinkRequest extends BaseRequest
{
/**
* Apply the additional POST base rules to this request
*
* @return array<string, mixed>
*/
public function postRules(): array
{
return [
'code' => 'required|string|max:255|min:2|unique:shortlinks',
'url' => 'required|string|max:255|min:2',
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules(): array
{
$shortlink = $this->route('shortlink');
return [
'code' => ['required', 'string', 'max:255', 'min:2', Rule::unique('shortlinks')->ignore($shortlink->id)],
'url' => 'required|string|max:255|min:2',
];
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
class SubscriptionRequest extends BaseRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function postRules(): array
{
return [
'email' => 'required|email|unique:subscriptions',
// 'captcha_token' => [new Recaptcha()],
];
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function destroyRules(): array
{
return [
'email' => 'required|email',
// 'captcha_token' => [new Recaptcha()],
];
}
/**
* Get the custom error messages.
*
* @return array
*/
public function messages(): array
{
return [
'email.unique' => 'This email address has already subscribed',
];
}
}

View File

@@ -1,22 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserForgotPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'email' => 'required|exists:users,email',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\Uniqueish;
use Illuminate\Foundation\Http\FormRequest;
class UserRegisterRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'display_name' => ['required','string','max:255', new Uniqueish('users')],
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
];
}
}

View File

@@ -1,111 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\RequiredIfAny;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\RequiredIf;
use App\Rules\Uniqueish;
use Illuminate\Support\Arr;
class UserRequest extends BaseRequest
{
/**
* Apply the additional POST base rules to this request
*
* @return array<string, mixed>
*/
public function postRules(): array
{
$user = auth()->user();
$isAdminUser = $user->hasPermission('admin/users');
return [
'first_name' => (
$isAdminUser === true ? 'required_with:last_name,display_name,phone' : 'required'
) . '|string|max:255|min:2',
'last_name' => (
$isAdminUser === true ? 'required_with:first_name,display_name,phone' : 'required'
) . '|string|max:255|min:2',
'display_name' => [
$isAdminUser === true ? 'required_with:first_name,last_name,phone' : 'required',
'string',
'max:255',
new Uniqueish('users')
],
'email' => 'required|string|email|max:255|unique:users',
'phone' => ['string', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'email_verified_at' => 'date'
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules(): array
{
$user = auth()->user();
$ruleUser = $this->route('user');
$isAdminUser = $user->hasPermission('admin/users');
$requiredIfFieldsPresent = function (array $fields) use ($ruleUser): RequiredIf {
return new RequiredIf(function () use ($fields, $ruleUser) {
$input = $this->all();
$values = Arr::only($input, $fields);
foreach ($values as $key => $value) {
if ($value !== null && $value !== '') {
return true;
}
}
$fields = array_diff($fields, array_keys($values));
foreach ($fields as $field) {
if ($ruleUser->$field !== '') {
return true;
}
}
return false;
});
};
return [
'first_name' => [
'sometimes',
$isAdminUser === true ? $requiredIfFieldsPresent(['last_name', 'display_name', 'phone']) : 'required',
'string',
'between:2,255',
],
'last_name' => [
'sometimes',
$isAdminUser === true ? $requiredIfFieldsPresent(['first_name', 'last_name', 'phone']) : 'required',
'string',
'between:2,255',
],
'display_name' => [
'sometimes',
$isAdminUser === true ? $requiredIfFieldsPresent(['first_name', 'display_name', 'phone']) : 'required',
'string',
'between:2,255',
(new Uniqueish('users', 'display_name'))->ignore($ruleUser->id)
],
'email' => [
'string',
'email',
'max:255',
Rule::unique('users')->ignore($ruleUser->id)->when(
$this->email !== $ruleUser->email,
function ($query) {
return $query->where('email', $this->email);
}
),
],
'phone' => ['nullable', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'password' => "nullable|string|min:8"
];
}
}

View File

@@ -1,22 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserResendVerifyEmailRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'email' => 'required|exists:users,email',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserResetPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'code' => 'required|digits:6',
'password' => 'required|string|min:8',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -1,22 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserVerifyEmailRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'code' => 'required|digits:6',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace App\Jobs\Media;
use App\Models\Media;
use App\Helpers;
use FFMpeg\Coordinate\TimeCode;
use FFMpeg\FFMpeg;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Imagick\Driver;
class GenerateVariants implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Media ID
*
* @var String
*/
public $media_name;
/**
* Overwrite existing
*
* @var bool
*/
public $overwrite;
/**
* Create a new job instance.
*
* @param Media $media The media to process
*/
public function __construct(Media $media, bool $overwrite = true)
{
$this->media_name = $media->name;
$this->overwrite = $overwrite;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new WithoutOverlapping($this->media_name)];
}
/**
* Execute the job.
*/
public function handle(): void
{
$media = Media::find($this->media_name);
if ($media === null) {
return;
}
if(Storage::disk('media')->exists($media->hash) === false) {
return;
}
$variantData = $media->getVariantTypes($matchingMimeType);
if(count($variantData) === 0) {
return;
}
$temp = $media->getAsTempFile();
if($temp === null) {
return;
}
$tempDir = pathinfo($temp, PATHINFO_DIRNAME);
$media->deleteAllVariants();
/* Images */
if($matchingMimeType === 'image/*') {
$manager = new ImageManager(new Driver());
$image = $manager->read($temp);
$isPortrait = $image->height() > $image->width();
foreach ($variantData as $variantName => $size) {
$image = $manager->read($temp);
if($isPortrait === true) {
$width = $size['height'];
$height = $size['width'];
} else {
$width = $size['width'];
$height = $size['height'];
}
if($variantName !== 'scaled' && ($image->height() < $height || $image->width() < $width)) {
continue;
}
$image->scaleDown($width, $height);
$variantFile = $tempDir . '/' . $media->hash . '-' . $variantName . '.webp';
$image->save($variantFile, quality: 75);
$media->addVariant($variantName, 'image/webp', 'webp', $variantFile);
unset($variantFile);
}//end foreach
} else if($matchingMimeType === 'text/plain') {
/* Text */
$width = $variantData['thumbnail']['width'];
$height = $variantData['thumbnail']['height'];
$manager = new ImageManager(new Driver());
$image = $manager->create($width, $height)->fill('fff');
// Read the first few lines of the text file
$numLines = 5;
$text = file_get_contents($temp);
$lines = explode("\n", $text);
$previewText = implode("\n", array_slice($lines, 0, $numLines));
// Center the text on the image
$fontSize = 8;
$textColor = '#000000'; // Black text color
// Calculate the position to start drawing the text
$x = 10; // Left padding
$y = 10; // Top padding
// Draw the text on the canvas with text wrapping
$lines = explode("\n", wordwrap($previewText, 30, "\n", true));
foreach ($lines as $line) {
$image->text($line, $x, $y, function ($font) use ($fontSize, $textColor) {
$font->file(1);
$font->size($fontSize);
$font->color($textColor);
});
// Move to the next line
$y += ($fontSize + 4); // Add some vertical spacing between lines (adjust as needed)
}
$variantFile = $tempDir . '/' . $media->hash . '-thumbnail.webp';
$image->save($variantFile, quality: 75);
$media->addVariant('thumbnail', 'image/webp', 'webp', $variantFile);
unset($variantFile);
} else if($matchingMimeType === 'application/pdf') {
/* PDF */
$width = $variantData['thumbnail']['width'];
$height = $variantData['thumbnail']['height'];
$manager = new ImageManager(new Driver());
$imagick = new \Imagick();
$imagick->readImage($temp . '[0]'); // Read the first page of the PDF
$imagick->setImageFormat('png');
$image = $manager->read($imagick);
$image->scaleDown($width, $height);
$variantFile = $tempDir . '/' . $media->hash . '-thumbnail.webp';
$image->save($variantFile, quality: 75);
$media->addVariant('thumbnail', 'image/webp', 'webp', $variantFile);
unset($variantFile);
} else if($matchingMimeType === 'video/*') {
/* Video */
$tempImage = $tempDir . '/' . $media->hash . '-temp-frame.jpg';
$variantFile = $tempDir . '/' . $media->hash . '-thumbnail.webp';
try {
$ffmpeg = FFMpeg::create();
$video = $ffmpeg->open($temp);
$frame = $video->frame(TimeCode::fromSeconds(5));
$frame->save($tempImage);
$width = $variantData['thumbnail']['width'];
$height = $variantData['thumbnail']['height'];
$manager = new ImageManager(new Driver());
$image = $manager->read($tempImage);
$image->scaleDown($width, $height);
$image->save($variantFile, quality: 75);
$media->addVariant('thumbnail', 'image/webp', 'webp', $variantFile);
unset($variantFile);
} catch (\Exception $e) {
Log::error($e);
}
if(file_exists($tempImage)) {
unlink($tempImage);
}
}
$media->status = 'ready';
$media->save();
}
}

View File

@@ -1,393 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Media;
use App\Models\MediaJob;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use FFMpeg;
use FFMpeg\Coordinate\Dimension;
use FFMpeg\FFProbe;
use FFMpeg\Format\VideoInterface;
use Intervention\Image\Facades\Image;
/** @property on $format */
class MediaWorkerJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* MediaJob item
*
* @var MediaJob
*/
protected $mediaJob;
/**
* Create a new job instance.
*
* @param MediaJob $mediaJob The mediaJob model.
* @return void
*/
public function __construct(MediaJob $mediaJob)
{
$this->mediaJob = $mediaJob;
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
$media = $this->mediaJob->media()->first();
$newMedia = false;
$data = json_decode($this->mediaJob->data, true);
try {
// FILE
if (array_key_exists('file', $data) === true) {
if (file_exists($data['file']) === false) {
$this->throwMediaJobFailure('temporary upload file no longer exists');
}
// convert HEIC files to JPG
$fileExtension = File::extension($data['file']);
if ($fileExtension === 'heic') {
$this->mediaJob->setStatusProcessing(0, 0, 'converting image');
// Get the path without the file name
$uploadedFileDirectory = dirname($data['file']);
// Convert the HEIC file to JPG
$jpgFileName = pathinfo($data['file'], PATHINFO_FILENAME) . '.jpg';
$jpgFilePath = $uploadedFileDirectory . '/' . $jpgFileName;
if (file_exists($jpgFilePath) === true) {
$this->throwMediaJobFailure('file already exists on server');
}
Image::make($data['file'])->save($jpgFilePath);
// Update the uploaded file path and file name
unlink($data['file']);
$data['file'] = $jpgFileName;
}//end if
// get security
$security = [];
if ($media === null) {
if (array_key_exists('security', $data) === true) {
$security = $data['security'];
}
} else {
$security['type'] = $media->security_type;
$security['data'] = $media->security_data;
}
// get storage
$storage = '';
if ($media === null) {
if (array_key_exists('storage', $data) === true) {
$storage = $data['storage'];
}
} else {
$storage = $media->storage;
}
if ($storage === '') {
if (count($security) === 0 || $security['type'] === '') {
if (strpos($data['mime_type'], 'image/') === 0) {
$storage = 'local';
} else {
$storage = 'cdn';
}
} else {
$storage = 'private';
}
}
// Check if file already exists
$exists = Storage::disk($storage)->exists($data['name']);
if ($exists === true) {
if (array_key_exists('noreplace', $data) === true && isTrue($data['noreplace']) === true) {
$this->throwMediaJobFailure('file already exists on server');
}
}
if ($exists === true) {
$pathInfo = pathinfo($data['name']);
$basename = $pathInfo['filename'];
$extension = $pathInfo['extension'];
$index = 0;
do {
$index++;
$data['name'] = $basename . '-' . $index . '.' . $extension;
} while (Storage::disk($storage)->exists($data['name']) === true);
}
if ($media === null) {
$newMedia = true;
$media = new Media([
'user_id' => $this->mediaJob->user_id,
'title' => $data['title'],
'name' => $data['name'],
'mime_type' => $data['mime_type'],
'size' => $data['size'],
'security_type' => $data['security']['type'],
'security_data' => $data['security']['data'],
'storage' => $storage,
]);
}//end if
$media->setStagingFile($data['file']);
} else {
if ($media === null) {
$this->throwMediaJobFailure('The media item no longer exists');
}
}//end if
if (array_key_exists('transform', $data) === true) {
$media->createStagingFile();
// Modifications
if (strpos($media->mime_type, 'image/') === 0) {
$modified = false;
$image = Image::make($media->getStagingFilePath());
// ROTATE
if (array_key_exists("rotate", $data['transform']) === true) {
$rotate = intval($data['transform']['rotate']);
if ($rotate !== 0) {
$this->mediaJob->setStatusProcessing(0, 0, 'rotating image');
$image = $image->rotate($rotate);
$modified = true;
}
}
// FLIP-H/V
if (array_key_exists('flip', $data['transform']) === true) {
if (stripos($data['transform']['flip'], 'h') !== false) {
$this->mediaJob->setStatusProcessing(0, 0, 'flipping image');
$image = $image->flip('h');
$modified = true;
}
if (stripos($data['transform']['flip'], 'v') !== false) {
$this->mediaJob->setStatusProcessing(0, 0, 'flipping image');
$image = $image->flip('v');
$modified = true;
}
}
// CROP
if (array_key_exists("crop", $data['transform']) === true) {
$cropData = $data['transform']['crop'];
$width = intval(arrayDefaultValue("width", $cropData, $image->getWidth()));
$height = intval(arrayDefaultValue("height", $cropData, $image->getHeight()));
$x = intval(arrayDefaultValue("x", $cropData, 0));
$y = intval(arrayDefaultValue("y", $cropData, 0));
$this->mediaJob->setStatusProcessing(0, 0, 'cropping image');
$image = $image->crop($width, $height, $x, $y);
$modified = true;
}//end if
if ($modified === true) {
$image->save();
}
} elseif (strpos($data['mime_type'], 'video/') === 0) {
$stagingFilePath = $media->getStagingFilePath();
$ffmpeg = FFMpeg\FFMpeg::create();
$video = $ffmpeg->open($stagingFilePath);
$format = $this->detectVideoFormat($stagingFilePath);
$modified = false;
if ($format === null) {
$this->mediaJob->setStatusFailed('Unsupported video format');
return;
}
/** @var FFMpeg\Media\Video::filters */
$filters = $video->filters();
// ROTATE
if (array_key_exists("rotate", $data['transform']) === true) {
$rotate = intval($data['transform']['rotate']);
$rotate = (($rotate % 360 + 360) % 360); // remove excess rotations
$rotate = intval(round($rotate / 90) * 90); // round to nearest 90%
if ($rotate > 0) {
$this->mediaJob->setStatusProcessing(0, 0, 'rotating video');
if ($rotate === 90) {
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_270);
$modified = true;
} elseif ($rotate === 180) {
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_180);
$modified = true;
} elseif ($rotate === 270) {
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_90);
$modified = true;
}
}
}
// FLIP-H/V
if (array_key_exists('flip', $data['transform']) === true) {
if (stripos($data['transform']['flip'], 'h') !== false) {
$this->mediaJob->setStatusProcessing(0, 0, 'flipping video');
$filters->hflip()->synchronize();
$modified = true;
}
if (stripos($data['transform']['flip'], 'v') !== false) {
$this->mediaJob->setStatusProcessing(0, 0, 'flipping video');
$filters->vflip()->synchronize();
$modified = true;
}
}
// CROP
if (array_key_exists("crop", $data['transform']) === true) {
$cropData = $data['transform']['crop'];
$videoStream = $video->getStreams()->videos()->first();
$width = intval(arrayDefaultValue("width", $cropData, $videoStream->get('width')));
$height = intval(arrayDefaultValue("height", $cropData, $videoStream->get('height')));
$x = intval(arrayDefaultValue("x", $cropData, 0));
$y = intval(arrayDefaultValue("y", $cropData, 0));
$cropDimension = new Dimension($width, $height);
$this->mediaJob->setStatusProcessing(0, 0, 'cropping video');
$filters->crop($cropDimension, $x, $y)->synchronize();
$modified = true;
}//end if
$tempFilePath = generateTempFilePath(pathinfo($stagingFilePath, PATHINFO_EXTENSION));
if (method_exists($format, 'on') === true) {
$mediaJob = $this->mediaJob;
$format->on('progress', function ($video, $format, $percentage) use ($mediaJob) {
$mediaJob->setStatusProcessing($percentage, 100, 'transcoded');
});
}
if ($modified === true) {
$video->save($format, $tempFilePath);
$media->changeStagingFile($tempFilePath);
}
}//end if
}//end if
// Update attributes
if (array_key_exists('title', $data) === true) {
$media->title = $data['title'];
}
// Relocate file (if requested)
if (array_key_exists('security', $data) === true && array_key_exists('type', $data['security']) === true) {
$media->security_type = $data['security']['type'];
$media->security_data = $data['security']['data'];
}
if (array_key_exists('storage', $data) === true) {
if ($media->storage !== $data['storage']) {
$media->createStagingFile();
Storage::disk($media->storage)->delete($media->name);
$media->storage = $data['storage'];
}
}
// Finish media object
if ($media->hasStagingFile() === true) {
$this->mediaJob->setStatusProcessing(0, 0, 'transferring to cdn');
$media->deleteFile();
$media->saveStagingFile(true);
}
$media->save();
$this->mediaJob->media_id = $media->id;
$this->mediaJob->setStatusComplete();
} catch (\Exception $e) {
if ($this->mediaJob->status !== 'failed') {
$this->mediaJob->setStatusFailed('Unexpected server error occurred');
}
if ($media !== null) {
$media->deleteStagingFile();
if ($newMedia === true) {
$media->delete();
}
}
Log::error($e->getMessage() . "\n" . $e->getFile() . " - " . $e->getLine() . "\n" . $e->getTraceAsString());
$this->fail($e);
}//end try
}
/**
* Detects the format of a video using FFProbe
*
* @param string $videoPath The video file path.
* @return VideoInterface | null
*/
public function detectVideoFormat(string $videoPath): VideoInterface | null
{
$ffprobe = FFProbe::create();
$videoStream = $ffprobe
->streams($videoPath) // Provide the path to the video file
->videos() // Filter video streams
->first();
$codecName = $videoStream->get('codec_name');
$codecToFormatClass = [
'h264' => 'FFMpeg\Format\Video\X264',
'wmv2' => 'FFMpeg\Format\Video\WMV',
'vp9' => 'FFMpeg\Format\Video\WebM',
'theora' => 'FFMpeg\Format\Video\Ogg',
'mpeg4' => 'FFMpeg\Format\Video\Mpeg4',
// Add more mappings as needed
];
if (isset($codecToFormatClass[$codecName]) === false) {
Log::info("Unsupported codec: $codecName");
return null;
}
$formatClassName = $codecToFormatClass[$codecName];
if (class_exists($formatClassName) === false) {
Log::info("Format class does not exist: $formatClassName");
return null;
}
return new $formatClassName();
}
/**
* Set failure status of MediaJob and throw exception.
*
* @param string $error The failure message.
* @return void
*/
private function throwMediaJobFailure(string $error): void
{
$this->mediaJob->setStatusFailed($error);
throw new \Exception($error);
}
}

View File

@@ -2,20 +2,19 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\SentEmail;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
class SendEmailJob implements ShouldQueue class SendEmail implements ShouldQueue
{ {
use Dispatchable; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/** /**
* Mail to receipt * Mail to receipt
@@ -31,7 +30,6 @@ class SendEmailJob implements ShouldQueue
*/ */
public $mailable; public $mailable;
/** /**
* Create a new job instance. * Create a new job instance.
* *
@@ -52,6 +50,18 @@ class SendEmailJob implements ShouldQueue
*/ */
public function handle(): void public function handle(): void
{ {
// Record sent email
$sentEmail = SentEmail::create([
'recipient' => $this->to,
'mailable_class' => get_class($this->mailable)
]);
// Add unsubscribe link if mailable supports it
if (method_exists($this->mailable, 'withUnsubscribeLink')) {
$unsubscribeLink = route('unsubscribe', ['email' => $sentEmail->id]);
$this->mailable->withUnsubscribeLink($unsubscribeLink);
}
Mail::to($this->to)->send($this->mailable); Mail::to($this->to)->send($this->mailable);
} }
} }

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Livewire;
use App\Jobs\SendEmail;
use Carbon\Carbon;
use Livewire\Component;
use App\Models\EmailSubscriptions;
use App\Mail\UserWelcome;
class EmailSubscribe extends Component
{
public string $email = '';
public bool $success = false;
public string $message = '';
public string $trap = '';
public int $renderedAt; // unix timestamp
protected $rules = [
'email' => 'required|email|max:255',
];
public function mount()
{
$this->renderedAt = now()->timestamp;
}
public function subscribe(): void
{
$this->validate();
// 1. Honeypot - if this hidden field is filled, treat as success but do nothing
if (! empty($this->trap)) {
$this->reset(['email', 'trap']);
$this->success = true;
$this->message = 'Thanks, you have been subscribed to our newsletter.';
return;
}
// 2. Block submits in first 10 seconds after render
if (now()->timestamp - $this->renderedAt < 4) {
$this->success = false;
$this->message = 'That was a bit quick. Please wait a few seconds and try again.';
return;
}
// 3. Enforce 30 seconds between attempts per session
$lastAttempt = session('subscribe_last_attempt'); // int timestamp or null
if (! is_int($lastAttempt)) {
$lastAttempt = null;
}
$now = time();
if ($lastAttempt && ($now - $lastAttempt) < 20) {
$this->success = false;
$this->message = 'Please wait a little before trying again.';
return;
}
session(['subscribe_last_attempt' => $now]);
// 4. Limit to 5 attempts per session (your existing logic)
$attempts = session('subscribe_attempts', 0);
if ($attempts >= 5) {
$this->success = false;
$this->message = 'Too many attempts. Please try again in a little while.';
return;
}
session(['subscribe_attempts' => $attempts + 1]);
// Look up existing subscription by email
$subscription = EmailSubscriptions::where('email', $this->email)->first();
// If already confirmed, do not create a new record or resend confirmation
if ($subscription && $subscription->confirmed) {
// Optionally you could set a different flag or message here
$this->success = false;
$this->message = 'That email is already subscribed to our newsletter.';
} else {
// If no subscription exists, create a new unconfirmed one
if (!$subscription) {
$subscription = EmailSubscriptions::create([
'email' => $this->email,
'confirmed' => Carbon::now()
]);
$subscription->save();
}
dispatch(new SendEmail($subscription->email, new UserWelcome($subscription->email)))->onQueue('mail');
$this->success = true;
$this->message = 'Thanks, you have been subscribed to our newsletter.';
}
$this->reset(['email', 'trap']);
}
public function render()
{
return view('livewire.email-subscribe');
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangeEmailVerify extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The registration code.
*
* @var integer
*/
public $code;
/**
* The new email address.
*
* @var string
*/
public $new_email;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @param string $new_email The new email address.
* @return void
*/
public function __construct(User $user, int $code, string $new_email)
{
$this->user = $user;
$this->code = $code;
$this->new_email = $new_email;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '👋🏻 Lets change your email!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.change_email_verify',
text: 'emails.user.change_email_verify_plain',
);
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangedEmail extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The old email.
*
* @var string
*/
public $old_email;
/**
* The new email.
*
* @var string
*/
public $new_email;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param string $old_email The previous email address.
* @param string $new_email The new email address.
* @return void
*/
public function __construct(User $user, string $old_email, string $new_email)
{
$this->user = $user;
$this->old_email = $old_email;
$this->new_email = $new_email;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '👍 Your email has been changed!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.changed_email',
text: 'emails.user.changed_email_plain',
);
}
}

View File

@@ -1,61 +0,0 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangedPassword extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '👍 Your password has been changed!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.changed_password',
text: 'emails.user.changed_password_plain',
);
}
}

View File

@@ -1,78 +0,0 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class Contact extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The contact name.
*
* @var string
*/
public $name;
/**
* The contact email.
*
* @var string
*/
public $email;
/**
* The contact content.
*
* @var string
*/
public $content;
/**
* Create a new message instance.
*
* @param string $name The contact name.
* @param string $email The contact email.
* @param string $content The contact content.
* @return void
*/
public function __construct(string $name, string $email, string $content)
{
$this->name = $name;
$this->email = $email;
$this->content = $content;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: config('contact.contact_subject'),
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.contact',
text: 'emails.user.contact_plain',
);
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class EmailVerify extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The registration code.
*
* @var integer
*/
public $code;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @return void
*/
public function __construct(User $user, int $code)
{
$this->user = $user;
$this->code = $code;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '👋🏻 Welcome to STEMMechanics!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.email_verify',
text: 'emails.user.email_verify_plain',
);
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ExceptionMail extends Mailable
{
use Queueable;
use SerializesModels;
/**
* Create a new message instance.
*/
public function __construct()
{
//
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Exception Mail',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'view.name',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ForgotPassword extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user
*
* @var \App\Models\User
*/
public $user;
/**
* The reset code
*
* @var integer
*/
public $code;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @return void
*/
public function __construct(User $user, int $code)
{
$this->user = $user;
$this->code = $code;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '🤦 Forgot your password?',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.forgot_password',
text: 'emails.user.forgot_password_plain',
);
}
}

View File

@@ -1,61 +0,0 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class SubscriptionConfirm extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The email address.
*
* @var string
*/
public $email;
/**
* Create a new message instance.
*
* @param string $email The email address.
* @return void
*/
public function __construct(string $email)
{
$this->email = $email;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '🗞️ You\'re on the mailing list!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.subscription_confirm',
text: 'emails.user.subscription_confirm_plain',
);
}
}

Some files were not shown because too many files have changed in this diff Show More