Compare commits

..

488 Commits

Author SHA1 Message Date
Shift
c6c639afc2 Apply code style 2023-05-24 21:36:42 +00:00
41147b26f2 dependency updates 2023-05-25 07:35:33 +10:00
4e97209494 fixes to support analytics 2023-05-25 07:18:28 +10:00
aa0b010bed ignore certain spellings 2023-05-25 07:18:28 +10:00
cb79ea64cf filter changes to support collections 2023-05-25 07:18:28 +10:00
James Collins
38409d0d63 Merge pull request #48 from STEMMechanics/dependabot/npm_and_yarn/eslint-plugin-jsdoc-44.2.5
Bump eslint-plugin-jsdoc from 39.9.1 to 44.2.5
2023-05-25 07:17:35 +10:00
James Collins
32dfb4eef3 Merge pull request #47 from STEMMechanics/dependabot/composer/guzzlehttp/guzzle-7.7.0
Bump guzzlehttp/guzzle from 7.6.1 to 7.7.0
2023-05-25 07:17:19 +10:00
James Collins
a6de64a089 Merge pull request #41 from STEMMechanics/dependabot/composer/phpunit/phpunit-10.1.3
Bump phpunit/phpunit from 9.6.7 to 10.1.3
2023-05-25 07:17:01 +10:00
James Collins
b226814676 Merge pull request #34 from STEMMechanics/dependabot/npm_and_yarn/vite-plugin-compression2-0.9.1
Bump vite-plugin-compression2 from 0.8.4 to 0.9.1
2023-05-25 07:16:45 +10:00
James Collins
805de3291b Merge pull request #30 from STEMMechanics/dependabot/npm_and_yarn/vue-final-modal-4.4.2
Bump vue-final-modal from 3.4.11 to 4.4.2
2023-05-25 07:16:13 +10:00
James Collins
b0ab63e30e Merge pull request #27 from STEMMechanics/dependabot/npm_and_yarn/prettier-2.8.8
Bump prettier from 2.8.2 to 2.8.8
2023-05-25 07:15:50 +10:00
James Collins
13bfc52b77 Merge pull request #23 from STEMMechanics/dependabot/npm_and_yarn/tinymce/tinymce-vue-5.1.0
Bump @tinymce/tinymce-vue from 4.0.7 to 5.1.0
2023-05-25 07:15:22 +10:00
dependabot[bot]
bbce78cab4 Bump phpunit/phpunit from 9.6.7 to 10.1.3
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 9.6.7 to 10.1.3.
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/10.1.3/ChangeLog-10.1.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/9.6.7...10.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-24 21:13:12 +00:00
James Collins
1e084d5131 Merge pull request #22 from STEMMechanics/dependabot/composer/nunomaduro/collision-7.1.0
Bump nunomaduro/collision from 6.4.0 to 7.1.0
2023-05-25 07:12:27 +10:00
dependabot[bot]
af699161da Bump eslint-plugin-jsdoc from 39.9.1 to 44.2.5
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 39.9.1 to 44.2.5.
- [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/v39.9.1...v44.2.5)

---
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>
2023-05-24 01:11:37 +00:00
2e1c2cd0b2 added autocomplete 2023-05-22 17:31:50 +10:00
cd7366b8ff remove debug 2023-05-22 16:45:22 +10:00
ec74f6594c prettier 2023-05-22 16:29:27 +10:00
5d2e9affc0 support editing/deleting multiple items 2023-05-22 16:17:44 +10:00
06b7ce4db0 added 2023-05-22 16:17:26 +10:00
dependabot[bot]
4bf695f559 Bump guzzlehttp/guzzle from 7.6.1 to 7.7.0
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.6.1 to 7.7.0.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/7.7/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.6.1...7.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-22 01:46:03 +00:00
59daa1ff08 fix disabling css 2023-05-21 17:51:27 +10:00
04044673e2 added gap 2023-05-21 17:51:19 +10:00
3b837dc6b0 fix checkboxes not always checking 2023-05-21 09:27:51 +10:00
2f029e2523 dont reduce size of checkbox labels when active 2023-05-21 09:02:42 +10:00
66d477795f better handle 503 errors 2023-05-21 08:50:14 +10:00
78f23db801 updated maintenance page 2023-05-21 07:50:29 +10:00
e62a21c469 loop images 2023-05-20 22:02:49 +10:00
9756622148 fix click propagation 2023-05-20 21:57:39 +10:00
065cb1b746 added prev/next arrows 2023-05-20 21:49:43 +10:00
54a7ad86dc dependencies update 2023-05-20 20:56:44 +10:00
0e7c86ac2b fix properties on email inputs 2023-05-20 20:46:37 +10:00
e023964cb2 added media sorting to editor 2023-05-20 20:13:35 +10:00
16f4eb65ef empty sort now reverts to default instead of 503 2023-05-20 19:24:28 +10:00
11eb12324e when saving object, show original query list 2023-05-20 18:34:06 +10:00
7a0d3fc8a0 fix delete option 2023-05-20 17:57:52 +10:00
3ed2aadc34 media controller should not directly delete files 2023-05-20 17:56:48 +10:00
b2004e3483 fix not uploading multiple files 2023-05-20 17:49:04 +10:00
55f363a64f reduce gap around images 2023-05-20 16:55:55 +10:00
7a6ed9f7f4 reduce banner height 2023-05-20 16:55:47 +10:00
8fa8c85077 fix status check 2023-05-20 16:42:15 +10:00
245ffc9d45 include tinymce themes 2023-05-20 16:42:05 +10:00
6a9a2f0a9e firefox fix 2023-05-19 14:17:18 +10:00
9dbefe5a8a add page loading icon 2023-05-19 14:00:15 +10:00
cce2a79ee4 fix table name 2023-05-19 13:41:38 +10:00
8e94ab2d7d rel=prefetch 2023-05-19 13:37:32 +10:00
6e0337cdeb prefetching and performance improvements 2023-05-19 13:21:02 +10:00
1c3b8f065e remove older code 2023-05-19 13:20:53 +10:00
c43d5574b4 removed elements page 2023-05-19 12:50:49 +10:00
0e5c654b02 fix potential sql injections 2023-05-18 09:33:57 +10:00
14d6d59581 dependency updates 2023-05-16 15:29:51 +10:00
3796961293 added copy shortlink option to dropdown 2023-05-16 11:19:34 +10:00
c471a97a23 add event users 2023-05-11 16:49:12 +10:00
fc853bd5f1 add private option to attachments 2023-05-11 13:37:40 +10:00
d0ea0ae4d3 support empty values with prefixes 2023-05-11 09:14:37 +10:00
8a6d1281bb remove debug 2023-05-11 09:06:17 +10:00
8797d51ef4 default filter on status is OK 2023-05-11 09:04:58 +10:00
2c8ac1f155 Revert media status to OK 2023-05-11 09:04:39 +10:00
42706de9df added defaultFilters option 2023-05-11 09:04:23 +10:00
3ce99b8751 improve error handling on upload failures 2023-05-11 08:40:30 +10:00
44c4f16c5c fix test styling 2023-05-11 08:40:10 +10:00
cdccde528e added --loading variables 2023-05-11 08:12:32 +10:00
f32655c156 fix firefox missing :has selector 2023-05-10 20:31:53 +10:00
d7255f004d use const form 2023-05-10 20:21:16 +10:00
56a1aaa19c duplicate form object 2023-05-10 20:20:59 +10:00
86491bfb2e use seperate form id 2023-05-10 20:20:50 +10:00
8dc43ccfce support form-id and check form exists 2023-05-10 20:20:34 +10:00
7ff49700fd support form-id 2023-05-10 20:20:27 +10:00
e14c7aafb3 added status page link 2023-05-10 12:42:21 +10:00
0f727168be dependency updates 2023-05-09 14:39:30 +10:00
0de47b3104 added location url option 2023-05-09 10:58:51 +10:00
6b3eb97568 dont validate empty url strings 2023-05-09 10:58:43 +10:00
bd4ba41b0b fix small screens and make full height 2023-05-09 10:30:41 +10:00
fb2f2d9739 removed forgot-username 2023-05-09 10:22:10 +10:00
a468ae01ff fix rules 2023-05-09 10:07:59 +10:00
6d534bd1c3 revert put requirements 2023-05-08 21:59:42 +10:00
31820317de check if password exists on login 2023-05-08 21:53:37 +10:00
e2efa1f1bd updated rules 2023-05-08 21:51:52 +10:00
86a0936cd4 only allow ghost users by admins 2023-05-08 21:46:07 +10:00
645b623a40 ignore id on fallback 2023-05-08 21:45:27 +10:00
96bd56a828 obsolete 2023-05-08 21:45:11 +10:00
870f1c5194 fix phone requirement 2023-05-08 20:28:26 +10:00
5d1adf7af8 support creating ghost users 2023-05-08 20:25:01 +10:00
a1170a1347 added RequiredIf option 2023-05-08 20:24:54 +10:00
9b5aab6e6e modelValue should support boolean 2023-05-08 20:08:23 +10:00
a25776fbbb fix grammer 2023-05-08 19:40:46 +10:00
54b5929aa4 forced 0 margin top 2023-05-08 19:40:41 +10:00
729fc3fd39 added shortlinks on frontend 2023-05-08 19:28:07 +10:00
b4cf05ad44 fixed error code 2023-05-08 19:28:01 +10:00
4da8b32b1a added Length 2023-05-08 19:26:02 +10:00
c35342df59 fix active watch 2023-05-08 19:18:27 +10:00
7d8d407d07 fix button dropdown colours in dark mode 2023-05-08 19:16:46 +10:00
1ceb109a28 fix append item height 2023-05-08 19:11:40 +10:00
a8181ff2b7 fix static active 2023-05-08 19:09:53 +10:00
e42c4c3023 fix row column + buttonrow 2023-05-08 17:29:22 +10:00
ff040eec58 added title and description upload support 2023-05-08 17:27:01 +10:00
5d663d21b3 check lastDialog exists before resolving 2023-05-08 17:26:52 +10:00
b73c2d3726 emit value if non set on init 2023-05-08 16:53:17 +10:00
ee96acbe4f use modelValue to automatic selection changes 2023-05-08 16:39:42 +10:00
fd22b79d42 dynamic disabling 2023-05-08 16:39:27 +10:00
99f56b9ef8 cleanup hovering 2023-05-08 16:09:52 +10:00
9a686c1112 added tab-color-hover 2023-05-08 16:09:44 +10:00
ffcf823a7f reduced top margin 2023-05-08 15:16:50 +10:00
a85c4bf115 added no-help property 2023-05-08 15:16:25 +10:00
29d7167c24 fix borders 2023-05-08 15:11:09 +10:00
3c6a570394 fix label padding on small controls 2023-05-08 14:43:46 +10:00
419fa322a3 use default alignment of toolbar 2023-05-08 14:41:34 +10:00
71a2e1b6dd update media button to medium size 2023-05-08 14:41:25 +10:00
a352c21198 fix typing 2023-05-08 12:36:11 +10:00
4accb60a24 use new format 2023-05-08 12:34:33 +10:00
88b92a9572 remove footer top margin 2023-05-08 12:32:20 +10:00
e39fa78981 cleanup 2023-05-08 12:25:59 +10:00
4205113b00 block width 100% 2023-05-08 12:25:45 +10:00
ad47efefaf cleanup 2023-05-08 12:21:03 +10:00
890399dd74 set default first/last name 2023-05-08 12:20:57 +10:00
2698ede55e added fallback 2023-05-08 12:11:20 +10:00
c9f0ea2512 remove macros 2023-05-08 12:11:14 +10:00
228f9c7c6b remove obsolete fields 2023-05-08 12:11:09 +10:00
63e05e924a fix footer on small displays 2023-05-08 11:16:56 +10:00
a66cde3934 show form errors 2023-05-08 11:14:53 +10:00
0b8a2904ec cleaned up 2023-05-08 11:14:44 +10:00
5fffa97ea7 lightened danger lighter 2023-05-08 11:14:36 +10:00
1d14d86d8e added 2023-05-08 11:14:21 +10:00
217ab89667 fix padding 2023-05-08 10:43:48 +10:00
ac2dd23ad7 remove usernames 2023-05-08 10:40:48 +10:00
7a4f72378d control-help div always shown 2023-05-08 10:40:33 +10:00
8190839823 added new rules 2023-05-08 10:40:04 +10:00
4e9d97268f remove any query items 2023-05-06 22:38:32 +10:00
985f7c94da force ssl and remove obsolete stuff 2023-05-06 22:38:24 +10:00
3d28e73369 added used counter 2023-05-06 18:29:03 +10:00
4076b138b9 wrong schema 2023-05-06 18:23:24 +10:00
dependabot[bot]
99d4c709bd Bump nunomaduro/collision from 6.4.0 to 7.1.0
Bumps [nunomaduro/collision](https://github.com/nunomaduro/collision) from 6.4.0 to 7.1.0.
- [Release notes](https://github.com/nunomaduro/collision/releases)
- [Changelog](https://github.com/nunomaduro/collision/blob/v7.x/CHANGELOG.md)
- [Commits](https://github.com/nunomaduro/collision/compare/v6.4.0...v7.1.0)

---
updated-dependencies:
- dependency-name: nunomaduro/collision
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-06 08:21:01 +00:00
93951cfbc8 shortlink support 2023-05-06 18:20:17 +10:00
4ac86c434e added phpdotenv 2023-05-06 18:17:07 +10:00
171cfa7aab added table 2023-05-06 18:16:53 +10:00
a494dbe662 remove recapcha statement 2023-05-06 15:30:33 +10:00
c9d02fb11c dont parse data 2023-05-05 21:35:02 +10:00
3ba65385c5 override padding 2023-05-04 19:02:09 +10:00
58a2da1996 updated homepage 2023-05-04 18:58:53 +10:00
845d6ba12b use correct timezone 2023-05-04 18:46:58 +10:00
f0b55b7b2e added total to UserCollection 2023-05-04 16:49:32 +10:00
e6dd75c2a8 dont show 4th card on single column 2023-05-04 06:52:03 +10:00
7b7154085e dependency updates 2023-05-04 06:48:06 +10:00
1c119e80e9 fix style colouring 2023-05-04 06:46:15 +10:00
3d86f859c6 darkmode support and new variables 2023-05-03 21:49:55 +10:00
0bfad00df7 fix saving attachments 2023-05-03 20:46:14 +10:00
64efa723b3 fix incorrect index 2023-05-03 20:46:07 +10:00
0072b28965 updated front page layout 2023-05-03 20:23:35 +10:00
21fa5d24af added 3rd accent 2023-05-03 20:23:19 +10:00
16ec3c515e added align-items-stretch 2023-05-03 20:23:13 +10:00
6868144e25 fix identifying sessions 2023-05-03 07:35:55 +10:00
0e5af96900 added use 2023-05-02 21:42:25 +10:00
ded5caf271 restructure api request 2023-05-02 21:31:30 +10:00
e8e1e91d1c update the media value url 2023-05-02 21:05:07 +10:00
6a16d545ec remove debug 2023-05-02 20:51:05 +10:00
976b6fbb78 update image gallery 2023-05-02 20:49:36 +10:00
efc5571fb3 hide container when no toasts are present 2023-05-02 20:28:43 +10:00
6ad4b3a6c4 embed variant types into Media model 2023-05-02 19:16:39 +10:00
cc0fe080cf analytics backend update 2023-05-01 19:04:08 +10:00
dependabot[bot]
1de89fba5f Bump vite-plugin-compression2 from 0.8.4 to 0.9.1
Bumps [vite-plugin-compression2](https://github.com/nonzzz/vite-compression-plugin) from 0.8.4 to 0.9.1.
- [Release notes](https://github.com/nonzzz/vite-compression-plugin/releases)
- [Changelog](https://github.com/nonzzz/vite-compression-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nonzzz/vite-compression-plugin/compare/v0.8.4...v0.9.1)

---
updated-dependencies:
- dependency-name: vite-plugin-compression2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 01:25:02 +00:00
0c668c9c62 fix styling on smaller screens 2023-04-28 08:57:17 +10:00
6a3c3a3566 fix small screen layout 2023-04-27 16:51:52 +10:00
e17f79e0b1 improve small screen layout 2023-04-27 16:51:46 +10:00
f0461fb65f update to buttonrow 2023-04-27 16:51:38 +10:00
aa38927522 upgrade to large 2023-04-27 15:57:01 +10:00
881d06deea reduce margin for edit button 2023-04-27 15:45:32 +10:00
683869214b added view opton 2023-04-27 15:43:36 +10:00
3322a6e005 added margin between buttons 2023-04-27 15:33:17 +10:00
65a48454ba missing button-block class 2023-04-27 15:33:11 +10:00
0de8e17593 declare time before usage 2023-04-27 14:42:21 +10:00
17beb4152b fix helper text 2023-04-27 14:42:13 +10:00
b09097294f fix const 2023-04-27 14:09:33 +10:00
c6e1b0248d performance improvements 2023-04-27 14:05:34 +10:00
0a956e1fc5 show large images 2023-04-27 13:35:02 +10:00
e2dee426bf fix test expectation 2023-04-27 13:33:29 +10:00
37a738c094 fix toTitleCase 2023-04-27 13:31:02 +10:00
bef4c3440b performance improvements 2023-04-27 13:24:40 +10:00
b36ad8042f use webp image 2023-04-27 08:34:50 +10:00
6ec38853ff store images locally by default 2023-04-27 07:25:13 +10:00
69144a665f updated config 2023-04-27 06:55:33 +10:00
382a1d0ef8 move cdn 2023-04-27 05:47:23 +10:00
41c751a76d just use medium size 2023-04-26 21:45:34 +10:00
01e46042fb change to webp 2023-04-26 21:42:01 +10:00
f1a28b6efe change to webp 2023-04-26 21:41:43 +10:00
7fddeeeaae dont force webp as original is not 2023-04-26 21:16:33 +10:00
bacf35bb4b use webp 2023-04-26 21:15:27 +10:00
4a83c7e171 explicitly use webp 2023-04-26 20:58:39 +10:00
3cbce25394 performance improvements 2023-04-26 20:34:04 +10:00
dependabot[bot]
c2b58cc82d Bump vue-final-modal from 3.4.11 to 4.4.2
Bumps [vue-final-modal](https://github.com/vue-final/vue-final-modal/tree/HEAD/packages/vue-final-modal) from 3.4.11 to 4.4.2.
- [Release notes](https://github.com/vue-final/vue-final-modal/releases)
- [Changelog](https://github.com/vue-final/vue-final-modal/blob/master/packages/vue-final-modal/CHANGELOG.md)
- [Commits](https://github.com/vue-final/vue-final-modal/commits/v4.4.2/packages/vue-final-modal)

---
updated-dependencies:
- dependency-name: vue-final-modal
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-26 10:26:28 +00:00
ac326d2d74 disable recaptcha 2023-04-26 20:25:38 +10:00
50aaf8b343 added analytics box 2023-04-26 20:08:25 +10:00
f667ac6430 fix margins 2023-04-26 20:01:50 +10:00
e2e599ed35 bugfixes 2023-04-26 20:00:19 +10:00
897e422e15 dont replace some statuses after end 2023-04-26 19:55:24 +10:00
0031be6882 add progress text 2023-04-26 19:55:10 +10:00
45880ed7a8 remove debug 2023-04-26 18:52:39 +10:00
b7e964174a add upload progress 2023-04-26 18:52:23 +10:00
77b8f60cb1 fix styling 2023-04-26 18:52:16 +10:00
4cbde00ef3 added disabled to other types 2023-04-26 18:32:15 +10:00
196472181e added disabled class 2023-04-26 18:32:05 +10:00
2fa7a0c7a5 update fields when file changes 2023-04-26 18:19:11 +10:00
38e982e70b fix styling 2023-04-26 18:08:35 +10:00
8a9b57547a fix double search 2023-04-26 18:01:20 +10:00
6dd3d6255d fix small styling 2023-04-26 18:01:13 +10:00
656de567d7 fix css for disabled input 2023-04-26 17:54:16 +10:00
24e80d7851 fix hero loading in edit 2023-04-26 17:47:22 +10:00
bc642b48da move edit button higher 2023-04-26 17:45:41 +10:00
cc07998a8a bugfix temp file creation 2023-04-26 17:33:34 +10:00
178309bb1e updated autoload 2023-04-26 17:33:23 +10:00
40c19f47c7 added temp helper 2023-04-26 17:33:16 +10:00
7f82b24b0c fix css priority 2023-04-26 16:02:35 +10:00
f31a8da0e1 added edit button 2023-04-26 13:22:20 +10:00
8ed158ab3c input select shows current value 2023-04-26 12:30:47 +10:00
08af379a57 bug fixes 2023-04-26 12:29:34 +10:00
79dbdd3e5a fix margin 2023-04-26 12:29:28 +10:00
f7e8d5bdf7 remove obsolete options 2023-04-26 12:29:23 +10:00
52eba56e34 remove obsolete variable 2023-04-26 12:13:39 +10:00
4c42276deb bug fixes 2023-04-26 12:00:26 +10:00
b4eb772662 whitespacing 2023-04-26 12:00:11 +10:00
825730c3f9 reduce margins on medium size 2023-04-26 12:00:04 +10:00
b7a2253b01 add userHasPermission helper 2023-04-26 11:59:53 +10:00
ce1174d41b remove download options 2023-04-26 11:32:02 +10:00
f2da168a03 updated 2023-04-26 11:29:22 +10:00
fec4b29261 updated 2023-04-26 11:29:18 +10:00
01b8dadd5f rounded corners 2023-04-26 11:29:06 +10:00
3ee97468f9 change posts to articles 2023-04-26 10:57:27 +10:00
c6d318bbc3 use large instead of scaled image 2023-04-26 09:38:31 +10:00
4ebb07a79a fix missing calc 2023-04-26 09:35:34 +10:00
2580d0874f bug fixes 2023-04-25 19:34:01 +10:00
2168e693d8 support dark-mode 2023-04-25 19:24:20 +10:00
bc2a25346b dont display if no items 2023-04-25 11:57:39 +10:00
03e969e08c fix icon spacing 2023-04-25 11:55:18 +10:00
37e3872782 fix margins on small devices 2023-04-25 11:44:29 +10:00
71442e4160 remove debug code 2023-04-25 11:39:40 +10:00
c2b69a769a use new range input 2023-04-24 21:56:31 +10:00
c0bc0e03c0 added range type 2023-04-24 21:56:25 +10:00
f49bef1112 added rules 2023-04-24 21:30:37 +10:00
191b2978ec update css 2023-04-24 21:23:25 +10:00
2771cdd053 remove debug 2023-04-24 20:53:33 +10:00
2d576645d8 update community images and links 2023-04-24 19:21:09 +10:00
be9884b468 set dialog width 2023-04-24 18:31:04 +10:00
df280ce7a6 change discord button name 2023-04-24 18:29:50 +10:00
c1182b3b90 fix classes 2023-04-24 15:38:58 +10:00
425a3561ac change SMFooter to SMPageFooter 2023-04-24 15:37:11 +10:00
12be354cc9 remove sm- prefix 2023-04-24 15:25:36 +10:00
7e2917d447 obsolete 2023-04-24 15:25:31 +10:00
cb0de6a2d5 obsolete 2023-04-24 15:21:48 +10:00
95318d1b36 update SMFormFooter to SMButtonRow 2023-04-24 15:21:05 +10:00
b92456c178 update all SMHeaders 2023-04-24 14:59:59 +10:00
bcb25b5d5e grammar 2023-04-24 14:42:31 +10:00
6a79204204 update position 2023-04-24 14:41:48 +10:00
d3776a8d3f bugfix SMHeader scroll 2023-04-24 14:40:48 +10:00
325c6a0448 update to new SMHeader 2023-04-24 14:14:49 +10:00
7c089eed80 remove old smheading 2023-04-24 14:14:43 +10:00
8b40a46c1b added component 2023-04-24 14:11:33 +10:00
ef4061b96c updates rules page 2023-04-24 13:40:59 +10:00
dependabot[bot]
506849b450 Bump prettier from 2.8.2 to 2.8.8
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.2 to 2.8.8.
- [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/2.8.2...2.8.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-24 02:25:08 +00:00
dependabot[bot]
e73ce8c943 Bump @tinymce/tinymce-vue from 4.0.7 to 5.1.0
Bumps [@tinymce/tinymce-vue](https://github.com/tinymce/tinymce-vue) from 4.0.7 to 5.1.0.
- [Release notes](https://github.com/tinymce/tinymce-vue/releases)
- [Changelog](https://github.com/tinymce/tinymce-vue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tinymce/tinymce-vue/compare/4.0.7...5.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-24 02:24:28 +00:00
bb4543bc65 dependency update 2023-04-24 12:24:09 +10:00
James Collins
f8d5850a89 Create dependabot.yml 2023-04-24 12:24:03 +10:00
cce994d35c added minecraft curve 2023-04-24 12:18:06 +10:00
02cfb53147 Added SMSocialIcons component 2023-04-24 12:16:48 +10:00
0755df03cf use exact active class 2023-04-24 10:35:28 +10:00
51f0ad7497 only add variant if created 2023-04-24 09:53:47 +10:00
b4a49d20c8 fix styling 2023-04-24 08:40:41 +10:00
44ccaf10d4 test for NaN price 2023-04-24 08:22:29 +10:00
967c14b93a bug fixes 2023-04-23 20:44:11 +10:00
0e42fc657b missing handleinput on select 2023-04-23 20:37:02 +10:00
2d7a91e368 fix params.id 2023-04-23 20:35:31 +10:00
072ab038fe fix missimg media icon and select styling 2023-04-23 20:34:52 +10:00
6723c27d5e ignore missing media 2023-04-23 20:28:17 +10:00
f5fc700886 typos 2023-04-23 20:25:15 +10:00
96a5ba0ceb media create 2023-04-23 20:21:08 +10:00
dfe5e72526 fix event create 2023-04-23 20:20:07 +10:00
d1cbebee84 sort oldest to newest 2023-04-23 20:02:23 +10:00
eac313feb8 footer spacing 2023-04-23 20:00:23 +10:00
a71ed56cf2 align text center 2023-04-23 19:57:08 +10:00
e3bc72e8d2 bugfix 2023-04-23 19:55:39 +10:00
0fadfed7f6 bug fixes 2023-04-23 19:53:10 +10:00
031db78590 bug fixes 2023-04-23 19:25:52 +10:00
6fd32fb84b support small 2023-04-23 16:42:58 +10:00
98e6464be6 support media input 2023-04-23 16:19:15 +10:00
52ce2afad2 added media type 2023-04-23 16:19:05 +10:00
28f89c9469 cleanup 2023-04-23 16:18:57 +10:00
6f7de8da66 reduce margin 2023-04-23 15:42:10 +10:00
50caf2753d added checkbox 2023-04-23 15:42:05 +10:00
30c0caa04d FormControl value is know unknown 2023-04-23 15:41:53 +10:00
e53d8c14a9 added Booleanish 2023-04-23 15:41:41 +10:00
eca89358db show status 2023-04-23 14:35:45 +10:00
1e58c71e67 fix search fields 2023-04-23 14:32:20 +10:00
43beaefc07 reverse sort 2023-04-23 14:32:00 +10:00
f74faadace fix date range filter 2023-04-23 14:30:05 +10:00
77aa622610 bug fixes 2023-04-23 13:56:27 +10:00
95aadd45ee bug fixes 2023-04-23 12:18:14 +10:00
89880016ea loading icon 2023-04-22 22:34:35 +10:00
beb91553ef min height 2023-04-22 22:31:05 +10:00
b2037c9575 css updates 2023-04-22 22:25:42 +10:00
ce4cec5589 fix 2023-04-22 21:25:04 +10:00
e8a597ec6b disable dark mode temp 2023-04-22 21:21:25 +10:00
a663e2bd56 updates 2023-04-22 21:18:07 +10:00
84bfd3cda2 updates 2023-04-21 15:46:12 +10:00
3dfe96fa89 updated css 2023-04-21 11:49:37 +10:00
93cbcef93f updated 2023-04-21 11:37:20 +10:00
5c758536a4 renamed 2023-04-21 11:37:13 +10:00
bc5a9aa9f1 remove image 2023-04-21 11:37:05 +10:00
b8ed77f6d5 add option to replace existing files 2023-04-21 10:07:17 +10:00
6c25cd029f added support to ignore existing files 2023-04-21 10:05:40 +10:00
68d59eda69 fix migration rename column issues 2023-04-21 09:58:05 +10:00
2534d4c159 dependency update 2023-04-21 08:55:32 +10:00
54252e768c dependency updates 2023-04-21 07:12:41 +10:00
7a2f263061 updates 2023-04-21 07:11:00 +10:00
5ae6e02ce8 updates 2023-04-19 22:31:47 +10:00
fb9944ef14 lots of changes 2023-04-19 16:26:13 +10:00
190493179f cleanup 2023-04-19 14:52:32 +10:00
4b1bc23622 added loading support 2023-04-19 14:49:15 +10:00
2ad5b04a48 added large option 2023-04-19 14:49:03 +10:00
afbbbcb4d1 remove debug 2023-04-19 14:28:23 +10:00
820c3aec9d remove debug permission 2023-04-19 14:27:31 +10:00
eafbcd8389 cleanup 2023-04-19 14:26:37 +10:00
f0459b3f6e cleanup 2023-04-19 14:26:27 +10:00
ff93265890 show banners and dates 2023-04-19 13:40:38 +10:00
320e282dc8 added banner colors 2023-04-19 13:40:29 +10:00
d23c911c78 fix ul margin 2023-04-19 13:13:51 +10:00
51df812a6c add display_name support 2023-04-19 09:40:35 +10:00
a96aba57f7 add php test debugging 2023-04-19 09:40:20 +10:00
36c71da4bb lots o updates 2023-04-18 21:47:44 +10:00
b53fca9648 removed dataset 2023-04-18 17:04:24 +10:00
9fafb8bd2a removed 2023-04-18 17:03:33 +10:00
41c7ba35a0 table width 100% 2023-04-18 16:59:57 +10:00
4cc5702da7 updated text 2023-04-18 15:47:17 +10:00
65f626f15e added cod 2023-04-18 15:47:09 +10:00
2abf6f67af added cod 2023-04-18 15:47:04 +10:00
72cde997ab added italic, updated small 2023-04-18 15:46:54 +10:00
8b27cb4690 removed small, added li 2023-04-18 15:46:47 +10:00
fb0cec0850 fix error not applying to border 2023-04-18 15:29:25 +10:00
a9a0bfdad0 fix small div 2023-04-18 15:26:47 +10:00
00a752173d added new formcard 2023-04-18 15:24:41 +10:00
24caa9a4f4 added space-between 2023-04-18 15:24:28 +10:00
a1075e000a fix styling 2023-04-18 15:11:52 +10:00
c416902280 cleanup 2023-04-18 15:04:06 +10:00
a29d707183 toolbar should be 100% 2023-04-18 15:04:00 +10:00
69d08a85ac input should be 100% 2023-04-18 15:03:51 +10:00
475ea08517 changes! 2023-04-18 13:52:36 +10:00
b4c97c20d6 inner container items 2023-04-18 13:52:28 +10:00
f7da2c8185 override default container center 2023-04-18 13:52:19 +10:00
a1a630fc02 align container center default 2023-04-18 13:52:10 +10:00
22ef117493 missing important 2023-04-18 13:51:58 +10:00
04b80d5ff8 updated darkmode 2023-04-18 13:35:06 +10:00
b764979c3b added accent-2 dark 2023-04-18 13:34:57 +10:00
a78c0491ef added accent-2 2023-04-18 13:32:25 +10:00
6082beb964 updated 2023-04-18 13:32:16 +10:00
9d9a5fd9d2 added narrow option 2023-04-18 13:01:56 +10:00
4332f389a1 change to use body page instead of data-set 2023-04-18 13:01:45 +10:00
a26b60e726 added accent colors 2023-04-18 12:48:40 +10:00
465d76cd08 added click to hide 2023-04-18 12:48:28 +10:00
e0300148cf cleanup 2023-04-18 12:08:02 +10:00
4442c6c625 cleanup styling 2023-04-18 12:07:58 +10:00
289eb86d97 cleanup 2023-04-18 12:07:51 +10:00
59724777e9 update header sizes 2023-04-18 12:07:46 +10:00
99e0b297b2 added change emitter 2023-04-18 11:29:38 +10:00
7036747042 use center option 2023-04-18 10:44:02 +10:00
b9cd3e3f9f added transitions 2023-04-18 10:43:54 +10:00
c8e90b6887 default is start, added center option 2023-04-18 10:43:25 +10:00
56973b62f6 added extra items 2023-04-18 10:43:15 +10:00
990a13e777 fixes 2023-04-18 10:21:55 +10:00
cd37623746 cleanup 2023-04-18 10:21:45 +10:00
193620e4e4 reduce font weight 2023-04-18 09:55:34 +10:00
78d85e2440 added clear option 2023-04-18 09:55:25 +10:00
9fa9689db9 fix padding on icon only buttons 2023-04-18 09:55:16 +10:00
81fc33183c changed dropdown to chevron 2023-04-18 09:20:21 +10:00
2600011736 fix dropdown clicks and overflow 2023-04-18 09:11:25 +10:00
e5c297eb7c hide easydatatable 2023-04-18 09:11:15 +10:00
a3766aca6c apply margin-top to all h3 2023-04-18 08:29:20 +10:00
857689dc22 remove obsolete loader 2023-04-18 08:29:08 +10:00
2ed5917e96 css disabled 2023-04-18 08:29:00 +10:00
84380bf333 #app as flex 2023-04-18 08:28:53 +10:00
e7d517f264 use new table 2023-04-17 22:57:40 +10:00
40a9cc424e cleanup 2023-04-17 22:57:35 +10:00
b725bc2b5b change search button to icon 2023-04-17 22:57:28 +10:00
50306c319e added 2023-04-17 22:57:18 +10:00
c1e86c6897 added back links 2023-04-17 22:57:07 +10:00
eb02142afc bug fixes 2023-04-17 22:56:49 +10:00
5f0526eef7 cleanup 2023-04-17 19:56:39 +10:00
cbdc55df8f fix scrollbar padding 2023-04-17 19:56:33 +10:00
e0022b15c5 remove obsolete code 2023-04-17 19:56:25 +10:00
983edc53d1 update workshop route name 2023-04-17 19:38:41 +10:00
2af1dcd24e update router namespace 2023-04-17 19:38:28 +10:00
2814a5f044 fix responsive 2023-04-17 19:34:42 +10:00
802fd87850 change button to primary type 2023-04-17 19:34:34 +10:00
50a6a39632 dark mode always on home page 2023-04-17 19:28:49 +10:00
49d0d3b35a updated page 2023-04-17 16:11:57 +10:00
8017f017f2 bug fixes 2023-04-17 16:11:51 +10:00
864798be7c updated h3 2023-04-17 16:11:45 +10:00
e20ef40e02 remove input group 2023-04-17 16:01:04 +10:00
955f9021f7 updated the new input group slots 2023-04-17 15:58:37 +10:00
e006090be2 removed 2023-04-17 15:58:27 +10:00
1c6bc56e08 added prepend and append slots 2023-04-17 15:58:21 +10:00
aa2da29b4c remove input group 2023-04-17 15:19:39 +10:00
9e47d28660 bug fix 2023-04-17 15:19:33 +10:00
a5383c87c7 cleanup 2023-04-17 15:07:53 +10:00
152a637e31 update button font color 2023-04-17 15:07:45 +10:00
5d947000ca fix validator and font weight on smaller 2023-04-17 15:07:35 +10:00
bf4f378108 added cta button 2023-04-17 14:58:08 +10:00
7f03228efa added sizes to button 2023-04-17 14:57:59 +10:00
7e6fd1859e added offset option 2023-04-17 14:30:41 +10:00
979c77c1b9 bring back model function and cleanup 2023-04-17 14:26:42 +10:00
15c9603902 support new conductor features 2023-04-17 14:11:42 +10:00
20dd8bcb3a apply includes if no fields are set 2023-04-17 14:11:33 +10:00
bec4b03a17 return a blank string when name attribute 2023-04-17 13:54:37 +10:00
2686a162e7 cleanup and fields support includes fields 2023-04-17 13:54:17 +10:00
7d9c982cf5 bug fixes and updates 2023-04-17 07:16:31 +10:00
d1c09ce74e added 2023-04-13 07:39:30 +10:00
fe5f429039 dependency udates 2023-04-12 18:53:28 +10:00
8937571214 fix background colors 2023-04-12 14:08:33 +10:00
f9591951cb fix footer sheme 2023-04-12 14:08:19 +10:00
956d2a25f2 fix navbar variables 2023-04-12 14:08:10 +10:00
365bec10a6 added support for scheme 2023-04-12 13:45:14 +10:00
c69c11b0fe added base-darker 2023-04-12 13:45:08 +10:00
40b8414f8a update css 2023-04-12 13:39:08 +10:00
06cb735b68 update component 2023-04-12 13:39:03 +10:00
985f32e06e update page 2023-04-12 13:38:57 +10:00
36469b20b3 use variables 2023-04-12 13:38:28 +10:00
2173e4c6b8 update footer 2023-04-12 13:38:19 +10:00
d7a35e651e remove breadcrumb and bg reference 2023-04-12 13:38:08 +10:00
4238c977f3 added dark/light logo 2023-04-12 10:07:39 +10:00
74e9a3204f added dark/light d-none 2023-04-12 10:06:54 +10:00
7bf94ced84 added css variables 2023-04-12 09:53:41 +10:00
1b3a40c22a support css scheme 2023-04-12 09:53:28 +10:00
28bef07e37 update color 2023-04-12 09:53:05 +10:00
3b7cb57e7a fix hover and spacing css 2023-04-12 08:56:42 +10:00
3f069e6d22 removed depreciated progressbar 2023-04-12 08:49:29 +10:00
12e7269591 removed depreciated carousel 2023-04-12 08:49:20 +10:00
0127cf0a6b added fontaine 2023-04-12 08:44:54 +10:00
fd1522a2ca fix event test to specifically set status 2023-04-11 17:10:53 +10:00
c79ec065d3 update structure 2023-04-11 14:09:10 +10:00
19fe484049 fix factory 2023-04-11 14:09:03 +10:00
db3d831bc0 remove whitespace 2023-04-11 14:08:53 +10:00
d27e707044 add permission default value 2023-04-11 14:08:41 +10:00
c142440068 cleanup 2023-04-11 13:19:48 +10:00
df6456f5d2 fix error changing defaults after rename 2023-04-11 13:15:24 +10:00
1578a2b8d1 fix bad table name in drop 2023-04-11 13:14:57 +10:00
4a43c152d5 remove unused constraints 2023-04-11 13:14:50 +10:00
0d1ee37272 remove page transitions 2023-04-10 21:23:15 +10:00
5c0b97cd1e h1 align left 2023-04-10 21:23:08 +10:00
511e8d6074 update post view 2023-04-10 21:18:31 +10:00
826e4a7de2 remove breadcrumbs 2023-04-10 21:18:24 +10:00
1ac66b4ece update default background color 2023-04-10 21:18:16 +10:00
8f58de9f4e remove carousel 2023-04-10 20:52:13 +10:00
611d997df9 remove padding 2023-04-10 20:52:07 +10:00
3f66e3f1f1 remove page loader 2023-04-10 20:51:59 +10:00
6154fa5dcc added new element 2023-04-10 20:46:31 +10:00
dd0914cd89 started removal of rounded borders 2023-04-10 20:46:25 +10:00
b8000d9a64 white hamburger 2023-04-10 19:51:13 +10:00
d94bd66c54 white stem 2023-04-10 19:51:05 +10:00
2ebd2018db updated navbar design 2023-04-10 19:25:54 +10:00
04b41e16e1 added components 2023-04-10 18:03:02 +10:00
7ad73f3c84 bug fixes and support new media 2023-04-10 14:49:53 +10:00
359698d54f added apiAttachmentResource 2023-04-10 14:49:27 +10:00
26ea658f9c updated types 2023-04-10 14:49:13 +10:00
a13be0530f added media variants helper 2023-04-10 14:49:07 +10:00
b018b11c57 fix docs 2023-04-10 14:48:55 +10:00
aac023351a remove logging 2023-04-10 14:48:42 +10:00
fd2fbea03f S3 jobs 2023-04-10 14:47:53 +10:00
f3bbdec77c rewrote to support S3 2023-04-10 14:47:38 +10:00
b54ace0272 added respondAccepted 2023-04-10 14:47:09 +10:00
55bc78d9cb return fulfilled hero and user 2023-04-10 14:47:00 +10:00
fabe027d54 changed default permission from null to '' 2023-04-10 14:46:48 +10:00
b4f4450573 return fulfilled hero 2023-04-10 14:46:21 +10:00
990cc66600 support null models 2023-04-10 14:45:58 +10:00
aa76147144 added public/private aws and CF 2023-04-10 14:45:46 +10:00
ee46af08ca update media table 2023-04-10 14:44:23 +10:00
c33de944ef dependencies update 2023-04-10 14:44:08 +10:00
79e5103b08 updated config 2023-04-06 21:20:54 +10:00
4797b213ee added clamav config 2023-04-06 21:18:54 +10:00
c232042af5 use static function in Media modalk 2023-04-06 21:17:38 +10:00
252448f4a9 added uniqueFileName rule 2023-04-06 21:14:57 +10:00
ec4febe5e9 added additional rows for media table 2023-04-06 21:12:12 +10:00
35ca0d90f7 added the route macro apiAttachmentResource 2023-04-06 17:28:07 +10:00
eae3d4689b add the storage macro public 2023-04-06 17:27:29 +10:00
67c9d4084c added sunspikes/clamav-validator 2023-04-06 17:26:10 +10:00
fb52428219 update dependencies 2023-04-06 17:25:31 +10:00
23e620e168 reduce to XLarge 2023-04-06 08:52:43 +10:00
324054b3db fix button text justification 2023-04-06 08:36:40 +10:00
d9dcbeef7b preview uses thumb size 2023-04-05 19:20:04 +10:00
9b31d52d7e renamed header to Files 2023-04-05 19:18:47 +10:00
260d2d28ad support relational links 2023-04-05 15:56:58 +10:00
dd74bdda6a explicit ask for small images 2023-04-05 15:53:21 +10:00
7486390da5 show file to large error 2023-04-05 14:53:35 +10:00
340 changed files with 43228 additions and 11107 deletions

View File

@@ -38,11 +38,24 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID= AWS_PUBLIC_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_PUBLIC_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1 AWS_PUBLIC_DEFAULT_REGION="us-west-002"
AWS_BUCKET= AWS_PUBLIC_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_PUBLIC_USE_PATH_STYLE_ENDPOINT=false
AWS_PUBLIC_ENDPOINT=
AWS_PUBLIC_URL=
AWS_PRIVATE_ACCESS_KEY_ID=
AWS_PRIVATE_SECRET_ACCESS_KEY=
AWS_PRIVATE_DEFAULT_REGION="us-west-002"
AWS_PRIVATE_BUCKET=
AWS_PRIVATE_USE_PATH_STYLE_ENDPOINT=false
AWS_PRIVATE_ENDPOINT=
AWS_PRIVATE_URL=
CLOUDFLARE_ZONE_ID=
CLOUDFLARE_API_KEY=
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=

15
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "daily"

2
.gitignore vendored
View File

@@ -237,7 +237,7 @@ dist/
### This Project ### ### This Project ###
/public/uploads /public/uploads
/public/build /public/build
/public/tinymce # /public/tinymce
*.key *.key
### Synk ### ### Synk ###

View File

@@ -15,5 +15,6 @@
"[php]": { "[php]": {
// "editor.defaultFormatter": "bmewburn.vscode-intelephense-client" // "editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
"editor.defaultFormatter": "wongjn.php-sniffer" "editor.defaultFormatter": "wongjn.php-sniffer"
} },
"cSpell.words": ["TIMESTAMPDIFF"]
} }

View File

@@ -0,0 +1,83 @@
<?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 LogicException;
class AnalyticsConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\Analytics';
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* The default includes to include in a request.
*
* @var array
*/
protected $includes = ['duration'];
/**
* 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)
{
$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()
{
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)
{
$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)
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/analytics') === true);
}
}

View File

@@ -0,0 +1,152 @@
<?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 LogicException;
class ArticleConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\Article';
/**
* The default sorting field
* @var string
*/
protected $sort = '-publish_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['attachments', 'user'];
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder)
{
$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)
{
if (Carbon::parse($model->publish_at)->isFuture() === true) {
$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()
{
$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)
{
$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)
{
$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)
{
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->attachments()->get()->map(function ($attachment) {
return MediaConductor::includeModel(request(), 'attachments', $attachment->media);
});
}
/**
* Include User Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeUser(Model $model)
{
return UserConductor::includeModel(request(), 'user', User::find($model['user_id']));
}
/**
* Transform the Hero field.
*
* @param mixed $value The current value.
* @return array The new value.
*/
public function transformHero(mixed $value)
{
return MediaConductor::includeModel(request(), 'hero', Media::find($value));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
namespace App\Conductors; namespace App\Conductors;
use App\Models\Media;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\InvalidCastException; use Illuminate\Database\Eloquent\InvalidCastException;
@@ -19,7 +20,13 @@ class EventConductor extends Conductor
* The default sorting field * The default sorting field
* @var string * @var string
*/ */
protected $sort = 'start_at'; protected $sort = '-start_at';
/**
* The included fields
* @var string[]
*/
protected $includes = ['attachments'];
/** /**
@@ -92,19 +99,30 @@ class EventConductor extends Conductor
} }
/** /**
* Transform the model * Include Attachments Field.
* *
* @param Model $model The model to transform. * @param Model $model Them model.
* @return array The transformed model. * @return mixed The model result.
* @throws InvalidCastException Cannot cast item to model.
*/ */
public function transform(Model $model) public function includeAttachments(Model $model)
{ {
$result = $model->toArray(); $user = auth()->user();
$result['attachments'] = $model->attachments()->get()->map(function ($attachment) {
return MediaConductor::model(request(), $attachment->media);
});
return $result; return $model->attachments()->get()->map(function ($attachment) use ($user) {
if ($attachment->private === false || ($user !== null && ($user->hasPermission('admin/events') === true || $attachment->users->contains($user) === true))) {
return MediaConductor::includeModel(request(), 'attachments', $attachment->media);
}
});
}
/**
* Transform the Hero field.
*
* @param mixed $value The current value.
* @return array The new value.
*/
public function transformHero(mixed $value)
{
return MediaConductor::includeModel(request(), 'hero', Media::find($value));
} }
} }

View File

@@ -4,6 +4,7 @@ namespace App\Conductors;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User;
class MediaConductor extends Conductor class MediaConductor extends Conductor
{ {
@@ -19,6 +20,22 @@ class MediaConductor extends Conductor
*/ */
protected $sort = 'created_at'; protected $sort = 'created_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['user'];
/**
* 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. * Return an array of model fields visible to the current user.
@@ -32,7 +49,7 @@ class MediaConductor extends Conductor
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission('admin/media') === false) { if ($user === null || $user->hasPermission('admin/media') === false) {
$fields = arrayRemoveItem($fields, 'permission'); $fields = arrayRemoveItem($fields, ['permission', 'storage']);
} }
return $fields; return $fields;
@@ -48,9 +65,9 @@ class MediaConductor extends Conductor
{ {
$user = auth()->user(); $user = auth()->user();
if ($user === null) { if ($user === null) {
$builder->whereNull('permission'); $builder->where('permission', '');
} else { } else {
$builder->whereNull('permission')->orWhereIn('permission', $user->permissions); $builder->where('permission', '')->orWhereIn('permission', $user->permissions);
} }
} }
@@ -62,7 +79,7 @@ class MediaConductor extends Conductor
*/ */
public static function viewable(Model $model) public static function viewable(Model $model)
{ {
if ($model->permission !== null) { if ($model->permission !== '') {
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission($model->permission) === false) { if ($user === null || $user->hasPermission($model->permission) === false) {
return false; return false;
@@ -106,4 +123,27 @@ class MediaConductor extends Conductor
$user = auth()->user(); $user = auth()->user();
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true)); 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)
{
unset($data['user_id']);
return $data;
}
/**
* Include User Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeUser(Model $model)
{
return UserConductor::includeModel(request(), 'user', User::find($model['user_id']));
}
} }

View File

@@ -1,109 +0,0 @@
<?php
namespace App\Conductors;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
class PostConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\Post';
/**
* The default sorting field
* @var string
*/
protected $sort = '-publish_at';
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder)
{
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/posts') === 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)
{
if (Carbon::parse($model->publish_at)->isFuture() === true) {
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/posts') === false) {
return false;
}
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable()
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model)
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model)
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true);
}
/**
* Transform the model
*
* @param Model $model The model to transform.
* @return array The transformed model.
* @throws InvalidCastException Cannot cast item to model.
*/
public function transform(Model $model)
{
$result = $model->toArray();
$result['attachments'] = $model->attachments()->get()->map(function ($attachment) {
return MediaConductor::model(request(), $attachment->media);
});
return $result;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User;
class ShortlinkConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\Shortlink';
/**
* 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()
{
$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)
{
$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)
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/shortlinks') === true);
}
}

View File

@@ -23,7 +23,7 @@ class UserConductor extends Conductor
{ {
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission('admin/users') === false) { if ($user === null || $user->hasPermission('admin/users') === false) {
return ['id', 'username']; return ['id', 'display_name'];
} }
return parent::fields($model); return parent::fields($model);
@@ -41,8 +41,10 @@ class UserConductor extends Conductor
$data = $model->toArray(); $data = $model->toArray();
if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) { if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) {
$fields = ['id', 'username']; $fields = ['id', 'display_name'];
$data = arrayLimitKeys($data, $fields); $data = arrayLimitKeys($data, $fields);
} else {
$data['permissions'] = $user->permissions;
} }
return $data; return $data;

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Console\Commands;
use App\Jobs\StoreUploadedFileJob;
use Illuminate\Console\Command;
use App\Models\Media;
use File;
use Symfony\Component\Console\Input\InputOption;
class MediaMigrate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'media:migrate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate the uploads folder to the CDN';
/**
* Configure the command options.
*
* @return void
*/
protected function configure()
{
$this->addOption(
'replace',
null,
InputOption::VALUE_NONE,
'Replace existing files'
);
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$replace = $this->option('replace');
$files = File::allFiles(public_path('uploads'));
foreach ($files as $file) {
$filename = pathinfo($file, PATHINFO_BASENAME);
$medium = Media::where('name', $filename)->first();
if ($medium !== null) {
$medium->update(['status' => 'Processing media']);
StoreUploadedFileJob::dispatch($medium, $file, $replace)->onQueue('media');
} else {
unlink($file);
}
}
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Console\Commands;
use App\Jobs\StoreUploadedFileJob;
use Illuminate\Console\Command;
use App\Models\Media;
use Symfony\Component\Console\Input\InputOption;
class MediaRebuild extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'media:rebuild';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rebuild the media table';
/**
* Configure the command options.
*
* @return void
*/
protected function configure()
{
$this->addOption(
'replace',
null,
InputOption::VALUE_NONE,
'Replace existing files'
);
$this->addOption(
'all',
null,
InputOption::VALUE_NONE,
'Rebuild all variants'
);
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$replace = $this->option('replace');
$all = $this->option('replace');
$media = [];
if ($all === true) {
$media = Media::all();
} else {
$media = Media::where(['variants' => ''])->orWhere(['variants' => '[]'])->orWhere(['variants' => '{}'])->get();
}
foreach ($media as $medium) {
StoreUploadedFileJob::dispatch($medium, '', $replace)->onQueue('media');
}
}
}

View File

@@ -62,7 +62,7 @@ class Enum
*/ */
public static function getMessage(int $messageIndex, string $defaultMessage = 'Unknown'): string public static function getMessage(int $messageIndex, string $defaultMessage = 'Unknown'): string
{ {
if(array_key_exists($messageIndex, self::$messages) === true) { if (array_key_exists($messageIndex, self::$messages) === true) {
return self::$messages[$messageIndex]; return self::$messages[$messageIndex];
} }

19
app/Helpers/Temp.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
/* Temp File Helper Functions */
/**
* Generate a temporary file path.
*
* @return str The filtered array.
*/
function generateTempFilePath(): string
{
$temporaryDir = storage_path('app/tmp');
if (is_dir($temporaryDir) === false) {
mkdir($temporaryDir, 0777, true);
}
return $temporaryDir . DIRECTORY_SEPARATOR . uniqid('upload_', true);
}

View File

@@ -0,0 +1,163 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\AnalyticsConductor;
use App\Conductors\Conductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\AnalyticsRequest;
use App\Models\Media;
use App\Models\Analytics;
use Illuminate\Http\JsonResponse;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\MassAssignmentException;
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) {
$searchFields = ['attribute', 'type', 'useragent', 'ip'];
$queryRequest = new Request();
$queryRequest->merge($request->only($searchFields));
foreach ($searchFields as $field) {
unset($request[$field]);
}
$query = Analytics::query()
->selectRaw('session,
MIN(created_at) as created_at,
TIMESTAMPDIFF(MINUTE, MIN(created_at), MAX(created_at)) as duration');
$query = Conductor::filterQuery($query, $queryRequest);
list($collection, $total) = AnalyticsConductor::collection($request, $query
->groupBy('session')
->get());
return $this->respondAsResource(
$collection,
['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\Analytics $analytics The analyics model.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, int $session)
{
if ($request->user() !== null && $request->user()?->hasPermission('admin/analytics') === true) {
list($collection, $total) = AnalyticsConductor::collection($request, Analytics::query()
->where('session', $session)
->get());
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
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'),
'attribute' => $request->input('attribute', ''),
'useragent' => $request->userAgent(),
'ip' => $request->ip()
];
if ($user !== null && $user->hasPermission('admin/analytics') === true && $request->has('session') === true) {
$data['session'] = $request->input('session');
$analytics = Analytics::create($data);
} else {
$analytics = Analytics::createWithSession($data);
}
return $this->respondAsResource(
AnalyticsConductor::model($request, $analytics),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}//end if
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\AnalyticsRequest $request The analytics update request.
* @param \App\Models\Analytics $analytics The specified analytics.
* @return \Illuminate\Http\Response
*/
public function update(AnalyticsRequest $request, Analytics $analytics)
{
if (AnalyticsConductor::updatable($analytics) === true) {
$analytics->update($request->all());
return $this->respondAsResource(AnalyticsConductor::model($request, $analytics));
}
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Analytics $analytics The specified analytics.
* @return \Illuminate\Http\Response
*/
public function destroy(Analytics $analytics)
{
if (AnalyticsConductor::destroyable($analytics) === true) {
$analytics->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
}

View File

@@ -81,6 +81,15 @@ class ApiController extends Controller
return response()->json([], HttpResponseCodes::HTTP_CREATED); return response()->json([], HttpResponseCodes::HTTP_CREATED);
} }
/**
* Return accepted
* @return \Illuminate\Http\JsonResponse
*/
public function respondAccepted()
{
return response()->json([], HttpResponseCodes::HTTP_ACCEPTED);
}
/** /**
* Return single error message * Return single error message
* *
@@ -121,28 +130,36 @@ class ApiController extends Controller
/** /**
* Return resource data * Return resource data
* *
* @param array|Model|Collection $data Resource data. * @param array|Model|Collection $data Resource data.
* @param array $options Respond options. * @param array $options Respond options.
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
protected function respondAsResource( protected function respondAsResource(
mixed $data, mixed $data,
array $options = [], array $options = [],
$validationFn = null
) { ) {
$isCollection = $options['isCollection'] ?? false; $isCollection = $options['isCollection'] ?? false;
$appendData = $options['appendData'] ?? null; $appendData = $options['appendData'] ?? null;
$resourceName = $options['resourceName'] ?? null; $resourceName = $options['resourceName'] ?? null;
$respondCode = $options['respondCode'] ?? HttpResponseCodes::HTTP_OK; $respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK);
if ($data === null || ($data instanceof Collection && $data->count() === 0)) { if ($data === null || ($data instanceof Collection && $data->count() === 0)) {
return $this->respondNotFound(); $validationData = [];
if (array_key_exists('appendData', $options) === true) {
$validationData = $options['appendData'];
}
if ($validationFn === null || $validationFn($validationData) === true) {
return $this->respondNotFound();
}
} }
if(is_null($resourceName) === true || empty($resourceName) === true) { if (is_null($resourceName) === true || empty($resourceName) === true) {
$resourceName = $this->resourceName; $resourceName = $this->resourceName;
} }
if(is_null($resourceName) === true || empty($resourceName) === true) { if (is_null($resourceName) === true || empty($resourceName) === true) {
$resourceName = get_class($this); $resourceName = get_class($this);
$resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1)); $resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1));
$resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller')); $resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller'));

View File

@@ -3,11 +3,11 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor; use App\Conductors\MediaConductor;
use App\Conductors\PostConductor; use App\Conductors\ArticleConductor;
use App\Enum\HttpResponseCodes; use App\Enum\HttpResponseCodes;
use App\Http\Requests\PostRequest; use App\Http\Requests\ArticleRequest;
use App\Models\Media; use App\Models\Media;
use App\Models\Post; use App\Models\Article;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\BindingResolutionException;
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\MassAssignmentException; use Illuminate\Database\Eloquent\MassAssignmentException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class PostController extends ApiController class ArticleController extends ApiController
{ {
/** /**
* ApplicationController constructor. * ApplicationController constructor.
@@ -38,12 +38,13 @@ class PostController extends ApiController
*/ */
public function index(Request $request) public function index(Request $request)
{ {
list($collection, $total) = PostConductor::request($request); list($collection, $total) = ArticleConductor::request($request);
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
]
); );
} }
@@ -51,13 +52,13 @@ class PostController extends ApiController
* Display the specified resource. * Display the specified resource.
* *
* @param \Illuminate\Http\Request $request The endpoint request. * @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Post $post The post model. * @param \App\Models\Article $article The article model.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function show(Request $request, Post $post) public function show(Request $request, Article $article)
{ {
if (PostConductor::viewable($post) === true) { if (ArticleConductor::viewable($article) === true) {
return $this->respondAsResource(PostConductor::model($request, $post)); return $this->respondAsResource(ArticleConductor::model($request, $article));
} }
return $this->respondForbidden(); return $this->respondForbidden();
@@ -66,15 +67,15 @@ class PostController extends ApiController
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\PostRequest $request The user request. * @param \App\Http\Requests\ArticleRequest $request The user request.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function store(PostRequest $request) public function store(ArticleRequest $request)
{ {
if (PostConductor::creatable() === true) { if (ArticleConductor::creatable() === true) {
$post = Post::create($request->all()); $article = Article::create($request->all());
return $this->respondAsResource( return $this->respondAsResource(
PostConductor::model($request, $post), ArticleConductor::model($request, $article),
['respondCode' => HttpResponseCodes::HTTP_CREATED] ['respondCode' => HttpResponseCodes::HTTP_CREATED]
); );
} else { } else {
@@ -85,15 +86,15 @@ class PostController extends ApiController
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \App\Http\Requests\PostRequest $request The post update request. * @param \App\Http\Requests\ArticleRequest $request The article update request.
* @param \App\Models\Post $post The specified post. * @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(PostRequest $request, Post $post) public function update(ArticleRequest $request, Article $article)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$post->update($request->all()); $article->update($request->all());
return $this->respondAsResource(PostConductor::model($request, $post)); return $this->respondAsResource(ArticleConductor::model($request, $article));
} }
return $this->respondForbidden(); return $this->respondForbidden();
@@ -102,13 +103,13 @@ class PostController extends ApiController
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *
* @param \App\Models\Post $post The specified post. * @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function destroy(Post $post) public function destroy(Article $article)
{ {
if (PostConductor::destroyable($post) === true) { if (ArticleConductor::destroyable($article) === true) {
$post->delete(); $article->delete();
return $this->respondNoContent(); return $this->respondNoContent();
} else { } else {
return $this->respondForbidden(); return $this->respondForbidden();
@@ -119,16 +120,16 @@ class PostController extends ApiController
* Get a list of attachments related to this model. * Get a list of attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Article $article The article model.
* @return JsonResponse Returns the post attachments. * @return JsonResponse Returns the article attachments.
* @throws InvalidFormatException * @throws InvalidFormatException
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws InvalidCastException * @throws InvalidCastException
*/ */
public function getAttachments(Request $request, Post $post) public function getAttachments(Request $request, Article $article)
{ {
if (PostConductor::viewable($post) === true) { if (ArticleConductor::viewable($article) === true) {
$medium = $post->attachments->map(function ($attachment) { $medium = $article->attachments->map(function ($attachment) {
return $attachment->media; return $attachment->media;
}); });
@@ -142,16 +143,16 @@ class PostController extends ApiController
* Store an attachment related to this model. * Store an attachment related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Article $article The article model.
* @return JsonResponse The response. * @return JsonResponse The response.
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws MassAssignmentException * @throws MassAssignmentException
*/ */
public function storeAttachment(Request $request, Post $post) public function storeAttachment(Request $request, Article $article)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
if($request->has("medium") && Media::find($request->medium)) { if ($request->has("medium") && Media::find($request->medium)) {
$post->attachments()->create(['media_id' => $request->medium]); $article->attachments()->create(['media_id' => $request->medium]);
return $this->respondCreated(); return $this->respondCreated();
} }
@@ -165,21 +166,21 @@ class PostController extends ApiController
* Update/replace attachments related to this model. * Update/replace attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The related model. * @param Article $article The related model.
* @return JsonResponse * @return JsonResponse
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws MassAssignmentException * @throws MassAssignmentException
*/ */
public function updateAttachments(Request $request, Post $post) public function updateAttachments(Request $request, Article $article)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$mediaIds = $request->attachments; $mediaIds = $request->attachments;
if(is_array($mediaIds) === false) { if (is_array($mediaIds) === false) {
$mediaIds = explode(',', $request->attachments); $mediaIds = explode(',', $request->attachments);
} }
$mediaIds = array_map('trim', $mediaIds); // trim each media ID $mediaIds = array_map('trim', $mediaIds); // trim each media ID
$attachments = $post->attachments; $attachments = $article->attachments;
// Delete attachments that are not in $mediaIds // Delete attachments that are not in $mediaIds
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
@@ -188,7 +189,7 @@ class PostController extends ApiController
} }
} }
// Create new attachments for media IDs that are not already in $post->attachments() // Create new attachments for media IDs that are not already in $article->attachments()
foreach ($mediaIds as $mediaId) { foreach ($mediaIds as $mediaId) {
$found = false; $found = false;
@@ -200,12 +201,12 @@ class PostController extends ApiController
} }
if (!$found) { if (!$found) {
$post->attachments()->create(['media_id' => $mediaId]); $article->attachments()->create(['media_id' => $mediaId]);
} }
} }
return $this->respondNoContent(); return $this->respondNoContent();
} }//end if
return $this->respondForbidden(); return $this->respondForbidden();
} }
@@ -213,15 +214,15 @@ class PostController extends ApiController
/** /**
* Delete a specific related attachment. * Delete a specific related attachment.
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The model. * @param Article $article The model.
* @param Media $medium The attachment medium. * @param Media $medium The attachment medium.
* @return JsonResponse * @return JsonResponse
* @throws BindingResolutionException * @throws BindingResolutionException
*/ */
public function deleteAttachment(Request $request, Post $post, Media $medium) public function deleteAttachment(Request $request, Article $article, Media $medium)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$attachments = $post->attachments; $attachments = $article->attachments;
$deleted = false; $deleted = false;
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {

View File

@@ -29,7 +29,7 @@ class AttachmentController extends ApiController
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function store(Request $request) public function store(Request $request)
@@ -40,7 +40,7 @@ class AttachmentController extends ApiController
/** /**
* Display the specified resource. * Display the specified resource.
* *
* @param \App\Models\Attachment $attachment * @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function show(Attachment $attachment) public function show(Attachment $attachment)
@@ -51,7 +51,7 @@ class AttachmentController extends ApiController
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param \App\Models\Attachment $attachment * @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function edit(Attachment $attachment) public function edit(Attachment $attachment)
@@ -62,11 +62,11 @@ class AttachmentController extends ApiController
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \App\Models\Attachment $attachment * @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(Request $request, Attachment $attachment) public function update(Request $request, Attachment $attachment)
{ {
// //
} }
@@ -74,10 +74,10 @@ class AttachmentController extends ApiController
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *
* @param \App\Models\Attachment $attachment * @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function destroy(Attachment $attachment) public function destroy(Attachment $attachment)
{ {
// //
} }

View File

@@ -47,18 +47,18 @@ class AuthController extends ApiController
*/ */
public function login(AuthLoginRequest $request) public function login(AuthLoginRequest $request)
{ {
$user = User::where('username', '=', $request->input('username'))->first(); $user = User::where('email', '=', $request->input('email'))->first();
if ($user !== null && Hash::check($request->input('password'), $user->password) === true) { if ($user !== null && strlen($user->password) > 0 && Hash::check($request->input('password'), $user->password) === true) {
if ($user->email_verified_at === null) { if ($user->email_verified_at === null) {
return $this->respondWithErrors([ return $this->respondWithErrors([
'username' => 'Email address has not been verified.' 'email' => 'Email address has not been verified.'
]); ]);
} }
if ($user->disabled === true) { if ($user->disabled === true) {
return $this->respondWithErrors([ return $this->respondWithErrors([
'username' => 'Account has been disabled.' 'email' => 'Account has been disabled.'
]); ]);
} }
@@ -78,8 +78,8 @@ class AuthController extends ApiController
}//end if }//end if
return $this->respondWithErrors([ return $this->respondWithErrors([
'username' => 'Invalid username or password', 'email' => 'Invalid email or password',
'password' => 'Invalid username or password', 'password' => 'Invalid email or password',
]); ]);
} }

View File

@@ -6,8 +6,10 @@ use App\Enum\HttpResponseCodes;
use App\Models\Event; use App\Models\Event;
use App\Conductors\EventConductor; use App\Conductors\EventConductor;
use App\Conductors\MediaConductor; use App\Conductors\MediaConductor;
use App\Conductors\UserConductor;
use App\Http\Requests\EventRequest; use App\Http\Requests\EventRequest;
use App\Models\Media; use App\Models\Media;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class EventController extends ApiController class EventController extends ApiController
@@ -18,7 +20,7 @@ class EventController extends ApiController
public function __construct() public function __construct()
{ {
$this->middleware('auth:sanctum') $this->middleware('auth:sanctum')
->only(['store','update','destroy']); ->only(['store','update','destroy', 'userAdd', 'userUpdate', 'userDelete']);
} }
/** /**
@@ -111,11 +113,8 @@ class EventController extends ApiController
* Get a list of attachments related to this model. * Get a list of attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Event $event The event model.
* @return JsonResponse Returns the post attachments. * @return JsonResponse Returns the event attachments.
* @throws InvalidFormatException
* @throws BindingResolutionException
* @throws InvalidCastException
*/ */
public function getAttachments(Request $request, Event $event) public function getAttachments(Request $request, Event $event)
{ {
@@ -134,15 +133,13 @@ class EventController extends ApiController
* Store an attachment related to this model. * Store an attachment related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Event $event The event model.
* @return JsonResponse The response. * @return JsonResponse The response.
* @throws BindingResolutionException
* @throws MassAssignmentException
*/ */
public function storeAttachment(Request $request, Event $event) public function storeAttachment(Request $request, Event $event)
{ {
if (EventConductor::updatable($event) === true) { if (EventConductor::updatable($event) === true) {
if ($request->has("medium") && Media::find($request->medium)) { if ($request->has("medium") === true && Media::find($request->medium) !== null) {
$event->attachments()->create(['media_id' => $request->medium]); $event->attachments()->create(['media_id' => $request->medium]);
return $this->respondCreated(); return $this->respondCreated();
} }
@@ -157,10 +154,8 @@ class EventController extends ApiController
* Update/replace attachments related to this model. * Update/replace attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The related model. * @param Event $event The related model.
* @return JsonResponse * @return JsonResponse
* @throws BindingResolutionException
* @throws MassAssignmentException
*/ */
public function updateAttachments(Request $request, Event $event) public function updateAttachments(Request $request, Event $event)
{ {
@@ -175,23 +170,23 @@ class EventController extends ApiController
// Delete attachments that are not in $mediaIds // Delete attachments that are not in $mediaIds
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if (!in_array($attachment->media_id, $mediaIds)) { if (in_array($attachment->media_id, $mediaIds) === false) {
$attachment->delete(); $attachment->delete();
} }
} }
// Create new attachments for media IDs that are not already in $post->attachments() // Create new attachments for media IDs that are not already in $article->attachments()
foreach ($mediaIds as $mediaId) { foreach ($mediaIds as $mediaId) {
$found = false; $found = false;
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if ($attachment->media_id == $mediaId) { if ($attachment->media_id === $mediaId) {
$found = true; $found = true;
break; break;
} }
} }
if (!$found) { if ($found === false) {
$event->attachments()->create(['media_id' => $mediaId]); $event->attachments()->create(['media_id' => $mediaId]);
} }
} }
@@ -204,11 +199,11 @@ class EventController extends ApiController
/** /**
* Delete a specific related attachment. * Delete a specific related attachment.
*
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The model. * @param Event $event The model.
* @param Media $medium The attachment medium. * @param Media $medium The attachment medium.
* @return JsonResponse * @return JsonResponse
* @throws BindingResolutionException
*/ */
public function deleteAttachment(Request $request, Event $event, Media $medium) public function deleteAttachment(Request $request, Event $event, Media $medium)
{ {
@@ -224,7 +219,7 @@ class EventController extends ApiController
} }
} }
if ($deleted) { if ($deleted === true) {
// Attachment was deleted successfully // Attachment was deleted successfully
return $this->respondNoContent(); return $this->respondNoContent();
} else { } else {
@@ -235,4 +230,82 @@ class EventController extends ApiController
return $this->respondForbidden(); return $this->respondForbidden();
} }
public function userList(Request $request, Event $event)
{
$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();
}
return $this->respondForbidden();
}
public function userAdd(Request $request, Event $event)
{
$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();
}
public function userUpdate(Request $request, Event $event)
{
// only admin/events permitted
}
public function userDelete(Request $request, Event $event, User $user)
{
$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

@@ -8,6 +8,7 @@ use App\Http\Requests\MediaRequest;
use App\Models\Media; use App\Models\Media;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Laravel\Sanctum\PersonalAccessToken; use Laravel\Sanctum\PersonalAccessToken;
class MediaController extends ApiController class MediaController extends ApiController
@@ -34,7 +35,11 @@ class MediaController extends ApiController
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
],
function ($options) {
return $options['total'] === 0;
}
); );
} }
@@ -80,31 +85,23 @@ class MediaController extends ApiController
} }
} }
if ($file->getSize() > Media::maxUploadSize()) { if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge(); return $this->respondTooLarge();
} }
$title = $file->getClientOriginalName(); try {
$mime = $file->getMimeType(); $media = Media::createFromUploadedFile($request, $file);
$fileInfo = Media::store($file, empty($request->input('permission'))); } catch (\Exception $e) {
if ($fileInfo === null) { if ($e->getCode() === Media::FILE_SIZE_EXCEEDED_ERROR) {
return $this->respondWithErrors( return $this->respondTooLarge();
['file' => 'The file could not be stored on the server'], } else {
HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR return $this->respondWithErrors(['file' => $e->getMessage()]);
); }
} }
$request->merge([
'title' => $title,
'mime' => $mime,
'name' => $fileInfo['name'],
'size' => filesize($fileInfo['path'])
]);
$media = $request->user()->media()->create($request->all());
return $this->respondAsResource( return $this->respondAsResource(
MediaConductor::model($request, $media), MediaConductor::model($request, $media),
['respondCode' => HttpResponseCodes::HTTP_CREATED] ['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
); );
}//end if }//end if
@@ -123,32 +120,36 @@ class MediaController extends ApiController
if (MediaConductor::updatable($medium) === true) { if (MediaConductor::updatable($medium) === true) {
$file = $request->file('file'); $file = $request->file('file');
if ($file !== null) { if ($file !== null) {
if ($file->getSize() > Media::maxUploadSize()) { if ($file->isValid() !== true) {
return $this->respondTooLarge(); 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.']);
}
} }
$oldPath = $medium->path(); if ($file->getSize() > Media::getMaxUploadSize()) {
$fileInfo = Media::store($file, empty($request->input('permission'))); return $this->respondTooLarge();
if ($fileInfo === null) { }
}
$medium->update($request->all());
if ($file !== null) {
try {
$medium->updateWithUploadedFile($file);
} catch (\Exception $e) {
return $this->respondWithErrors( return $this->respondWithErrors(
['file' => 'The file could not be stored on the server'], ['file' => $e->getMessage()],
HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR
); );
} }
}
if (file_exists($oldPath) === true) {
unlink($oldPath);
}
$request->merge([
'title' => $file->getClientOriginalName(),
'mime' => $file->getMimeType(),
'name' => $fileInfo['name'],
'size' => filesize($fileInfo['path'])
]);
}//end if
$medium->update($request->all());
return $this->respondAsResource(MediaConductor::model($request, $medium)); return $this->respondAsResource(MediaConductor::model($request, $medium));
}//end if }//end if
@@ -164,10 +165,6 @@ class MediaController extends ApiController
public function destroy(Media $medium) public function destroy(Media $medium)
{ {
if (MediaConductor::destroyable($medium) === true) { if (MediaConductor::destroyable($medium) === true) {
if (file_exists($medium->path()) === true) {
unlink($medium->path());
}
$medium->delete(); $medium->delete();
return $this->respondNoContent(); return $this->respondNoContent();
} }

View File

@@ -32,7 +32,7 @@ class OCRController extends ApiController
$data = ['ocr' => []]; $data = ['ocr' => []];
$filters = $request->get('filters', ['tesseract']); $filters = $request->get('filters', ['tesseract']);
if(is_array($filters) === false) { if (is_array($filters) === false) {
$filters = explode(',', $filters); $filters = explode(',', $filters);
} }
@@ -52,9 +52,12 @@ class OCRController extends ApiController
// We need progress updates to break the connection mid-way // We need progress updates to break the connection mid-way
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // more progress info curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // more progress info
curl_setopt($ch, CURLOPT_NOPROGRESS, false); curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function( curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function (
$downloadSize, $downloaded, $uploadSize, $uploaded $downloadSize,
) use($maxDownloadSize) { $downloaded,
$uploadSize,
$uploaded
) use ($maxDownloadSize) {
return ($downloaded > $maxDownloadSize) ? 1 : 0; return ($downloaded > $maxDownloadSize) ? 1 : 0;
}); });
@@ -62,9 +65,9 @@ class OCRController extends ApiController
$curlError = curl_errno($ch); $curlError = curl_errno($ch);
$curlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); $curlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
curl_close($ch); curl_close($ch);
if($curlError !== 0) { if ($curlError !== 0) {
$error = 'File size is larger then allowed'; $error = 'File size is larger then allowed';
if($curlError !== CurlErrorCodes::CURLE_ABORTED_BY_CALLBACK) { if ($curlError !== CurlErrorCodes::CURLE_ABORTED_BY_CALLBACK) {
$error = CurlErrorCodes::getMessage($curlError); $error = CurlErrorCodes::getMessage($curlError);
} }
@@ -77,8 +80,8 @@ class OCRController extends ApiController
// tesseract (overall) // tesseract (overall)
$ocr = null; $ocr = null;
foreach($filters as $filterItem) { foreach ($filters as $filterItem) {
if(str_starts_with($filterItem, 'tesseract') === true) { if (str_starts_with($filterItem, 'tesseract') === true) {
$ocr = new TesseractOCR(); $ocr = new TesseractOCR();
$ocr->image($urlDownloadFilePath); $ocr->image($urlDownloadFilePath);
if ($tesseractOEM !== null) { if ($tesseractOEM !== null) {
@@ -95,11 +98,10 @@ class OCRController extends ApiController
} }
// Image Filter Function // Image Filter Function
$tesseractImageFilterFunc = function($filter, $options = null) use($curlResult, $curlSize, $ocr) { $tesseractImageFilterFunc = function ($filter, $options = null) use ($curlResult, $curlSize, $ocr) {
$result = ''; $result = '';
$img = imagecreatefromstring($curlResult); $img = imagecreatefromstring($curlResult);
if ($img !== false && (($options !== null && imagefilter($img, $filter, $options) === true) || ($options === null && imagefilter($img, $filter) === true))) { if ($img !== false && (($options !== null && imagefilter($img, $filter, $options) === true) || ($options === null && imagefilter($img, $filter) === true))) {
ob_start(); ob_start();
imagepng($img); imagepng($img);
$imgData = ob_get_contents(); $imgData = ob_get_contents();
@@ -116,7 +118,7 @@ class OCRController extends ApiController
}; };
// Image Scale Function // Image Scale Function
$tesseractImageScaleFunc = function($scaleFunc) use ($curlResult, $ocr) { $tesseractImageScaleFunc = function ($scaleFunc) use ($curlResult, $ocr) {
$result = ''; $result = '';
$srcImage = imagecreatefromstring($curlResult); $srcImage = imagecreatefromstring($curlResult);
$srcWidth = imagesx($srcImage); $srcWidth = imagesx($srcImage);
@@ -143,7 +145,7 @@ class OCRController extends ApiController
}; };
// filter: tesseract // filter: tesseract
if(in_array('tesseract', $filters) === true) { if (in_array('tesseract', $filters) === true) {
$data['ocr']['tesseract'] = $ocr->run(500); $data['ocr']['tesseract'] = $ocr->run(500);
} }
@@ -154,14 +156,14 @@ class OCRController extends ApiController
// filter: tesseract.double_scale // filter: tesseract.double_scale
if (in_array('tesseract.double_scale', $filters) === true) { if (in_array('tesseract.double_scale', $filters) === true) {
$data['ocr']['tesseract.double_scale'] = $tesseractImageScaleFunc(function($size) { $data['ocr']['tesseract.double_scale'] = $tesseractImageScaleFunc(function ($size) {
return $size * 2; return $size * 2;
}); });
} }
// filter: tesseract.half_scale // filter: tesseract.half_scale
if (in_array('tesseract.half_scale', $filters) === true) { if (in_array('tesseract.half_scale', $filters) === true) {
$data['ocr']['tesseract.half_scale'] = $tesseractImageScaleFunc(function($size) { $data['ocr']['tesseract.half_scale'] = $tesseractImageScaleFunc(function ($size) {
return $size / 2; return $size / 2;
}); });
} }
@@ -187,12 +189,12 @@ class OCRController extends ApiController
} }
// filter: keras // filter: keras
if(in_array('keras', $filters) === true) { if (in_array('keras', $filters) === true) {
$cmd = '/usr/bin/python3 ' . base_path() . '/scripts/keras_oc.py ' . urlencode($url); $cmd = '/usr/bin/python3 ' . base_path() . '/scripts/keras_oc.py ' . urlencode($url);
$command = escapeshellcmd($cmd); $command = escapeshellcmd($cmd);
$output = shell_exec($cmd); $output = shell_exec($cmd);
if ($output !== null && strlen($output) > 0) { if ($output !== null && strlen($output) > 0) {
$output = substr($output, strpos($output, '----------START----------') + 25); $output = substr($output, (strpos($output, '----------START----------') + 25));
} else { } else {
$output = ''; $output = '';
} }

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor;
use App\Conductors\ShortlinkConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\MediaRequest;
use App\Http\Requests\ShortlinkRequest;
use App\Models\Media;
use App\Models\Shortlink;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Laravel\Sanctum\PersonalAccessToken;
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 $medium 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 $medium 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

@@ -35,7 +35,8 @@ class SubscriptionController extends ApiController
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
]
); );
} }

View File

@@ -2,10 +2,10 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Conductors\EventConductor;
use App\Enum\HttpResponseCodes; use App\Enum\HttpResponseCodes;
use App\Http\Requests\UserRequest; use App\Http\Requests\UserRequest;
use App\Http\Requests\UserForgotPasswordRequest; use App\Http\Requests\UserForgotPasswordRequest;
use App\Http\Requests\UserForgotUsernameRequest;
use App\Http\Requests\UserRegisterRequest; use App\Http\Requests\UserRegisterRequest;
use App\Http\Requests\UserResendVerifyEmailRequest; use App\Http\Requests\UserResendVerifyEmailRequest;
use App\Http\Requests\UserResetPasswordRequest; use App\Http\Requests\UserResetPasswordRequest;
@@ -14,7 +14,6 @@ use App\Jobs\SendEmailJob;
use App\Mail\ChangedEmail; use App\Mail\ChangedEmail;
use App\Mail\ChangedPassword; use App\Mail\ChangedPassword;
use App\Mail\ChangeEmailVerify; use App\Mail\ChangeEmailVerify;
use App\Mail\ForgotUsername;
use App\Mail\ForgotPassword; use App\Mail\ForgotPassword;
use App\Mail\EmailVerify; use App\Mail\EmailVerify;
use App\Models\User; use App\Models\User;
@@ -22,6 +21,8 @@ use App\Models\UserCode;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use App\Conductors\UserConductor; use App\Conductors\UserConductor;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Container\BindingResolutionException;
class UserController extends ApiController class UserController extends ApiController
{ {
@@ -37,10 +38,10 @@ class UserController extends ApiController
'register', 'register',
'exists', 'exists',
'forgotPassword', 'forgotPassword',
'forgotUsername',
'resetPassword', 'resetPassword',
'verifyEmail', 'verifyEmail',
'resendVerifyEmailCode' 'resendVerifyEmailCode',
'eventList',
]); ]);
} }
@@ -57,7 +58,8 @@ class UserController extends ApiController
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
]
); );
} }
@@ -97,14 +99,14 @@ class UserController extends ApiController
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \App\Http\Requests\UserRequest $request The user update request. * @param \App\Http\Requests\UserRequest $request The user update request.
* @param \App\Models\User $user The specified user. * @param \App\Models\User $user The specified user.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(UserRequest $request, User $user) public function update(UserRequest $request, User $user)
{ {
if (UserConductor::updatable($user) === true) { if (UserConductor::updatable($user) === true) {
$input = []; $input = [];
$updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password']; $updatable = ['first_name', 'last_name', 'email', 'phone', 'password', 'display_name'];
if ($request->user()->hasPermission('admin/user') === true) { if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']); $updatable = array_merge($updatable, ['email_verified_at']);
@@ -148,15 +150,28 @@ class UserController extends ApiController
public function register(UserRegisterRequest $request) public function register(UserRegisterRequest $request)
{ {
try { try {
$user = User::create([ $userData = $request->only([
'first_name' => $request->input('first_name'), 'first_name',
'last_name' => $request->input('last_name'), 'last_name',
'username' => $request->input('username'), 'email',
'email' => $request->input('email'), 'phone',
'phone' => $request->input('phone'), 'password',
'password' => Hash::make($request->input('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([ $code = $user->codes()->create([
'action' => 'verify-email', 'action' => 'verify-email',
]); ]);
@@ -173,26 +188,6 @@ class UserController extends ApiController
}//end try }//end try
} }
/**
* Sends an email with all the usernames registered at that address
*
* @param \App\Http\Requests\UserForgotUsernameRequest $request The forgot username request.
* @return \Illuminate\Http\Response
*/
public function forgotUsername(UserForgotUsernameRequest $request)
{
$users = User::where('email', $request->input('email'))->whereNotNull('email_verified_at')->get();
if ($users->count() > 0) {
dispatch((new SendEmailJob(
$users->first()->email,
new ForgotUsername($users->pluck('username')->toArray())
)))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondJson(['message' => 'Username send to the email address if registered']);
}
/** /**
* Generates a new reset password code * Generates a new reset password code
* *
@@ -201,7 +196,7 @@ class UserController extends ApiController
*/ */
public function forgotPassword(UserForgotPasswordRequest $request) public function forgotPassword(UserForgotPasswordRequest $request)
{ {
$user = User::where('username', $request->input('username'))->first(); $user = User::where('email', $request->input('email'))->first();
if ($user !== null) { if ($user !== null) {
$user->codes()->where('action', 'reset-password')->delete(); $user->codes()->where('action', 'reset-password')->delete();
$code = $user->codes()->create([ $code = $user->codes()->create([
@@ -297,7 +292,7 @@ class UserController extends ApiController
{ {
UserCode::clearExpired(); UserCode::clearExpired();
$user = User::where('username', $request->input('username'))->first(); $user = User::where('email', $request->input('email'))->first();
if ($user !== null) { if ($user !== null) {
$code = $user->codes()->where('action', 'verify-email')->first(); $code = $user->codes()->where('action', 'verify-email')->first();
$code->regenerate(); $code->regenerate();
@@ -322,7 +317,7 @@ class UserController extends ApiController
*/ */
public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request) public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request)
{ {
$user = User::where('username', $request->input('username'))->first(); $user = User::where('email', $request->input('email'))->first();
if ($user !== null) { if ($user !== null) {
$user->codes()->where('action', 'verify-email')->delete(); $user->codes()->where('action', 'verify-email')->delete();
@@ -339,4 +334,29 @@ class UserController extends ApiController
return $this->respondNotFound(); 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)
{
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

@@ -21,8 +21,8 @@ class LogRequest
$response = $next($request); $response = $next($request);
try { try {
Analytics::create([ Analytics::createWithSession([
'type' => 'pageview', 'type' => 'apirequest',
'attribute' => $request->path(), 'attribute' => $request->path(),
'useragent' => $request->userAgent(), 'useragent' => $request->userAgent(),
'ip' => $request->ip(), 'ip' => $request->ip(),

View File

@@ -0,0 +1,35 @@
<?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()
{
return [
'type' => 'required|string',
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules()
{
return [
'type' => 'string',
'useragent' => 'string',
'ip' => 'ipv4|ipv6',
'session' => 'number',
];
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Http\Requests;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class PostRequest extends BaseRequest class ArticleRequest extends BaseRequest
{ {
/** /**
* Get the validation rules that apply to POST requests. * Get the validation rules that apply to POST requests.
@@ -14,7 +14,7 @@ class PostRequest extends BaseRequest
public function postRules() public function postRules()
{ {
return [ return [
'slug' => 'required|string|min:6|unique:posts', 'slug' => 'required|string|min:6|unique:articles',
'title' => 'required|string|min:6|max:255', 'title' => 'required|string|min:6|max:255',
'publish_at' => 'required|date', 'publish_at' => 'required|date',
'user_id' => 'required|uuid|exists:users,id', 'user_id' => 'required|uuid|exists:users,id',
@@ -34,7 +34,7 @@ class PostRequest extends BaseRequest
'slug' => [ 'slug' => [
'string', 'string',
'min:6', 'min:6',
Rule::unique('posts')->ignoreModel($this->post), Rule::unique('articles')->ignoreModel($this->article),
], ],
'title' => 'string|min:6|max:255', 'title' => 'string|min:6|max:255',
'publish_at' => 'date', 'publish_at' => 'date',

View File

@@ -14,7 +14,7 @@ class AuthLoginRequest extends FormRequest
public function rules() public function rules()
{ {
return [ return [
'username' => 'required|string|min:6|max:255', 'email' => 'required|string|min:6|max:255',
'password' => 'required|string|min:6', 'password' => 'required|string|min:6',
]; ];
} }

View File

@@ -18,7 +18,7 @@ class ContactSendRequest extends FormRequest
'name' => 'required|max:255', 'name' => 'required|max:255',
'email' => 'required|email|max:255', 'email' => 'required|email|max:255',
'content' => 'required|max:2000', 'content' => 'required|max:2000',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

View File

@@ -0,0 +1,36 @@
<?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()
{
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()
{
$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

@@ -15,7 +15,7 @@ class SubscriptionRequest extends BaseRequest
{ {
return [ return [
'email' => 'required|email|unique:subscriptions', 'email' => 'required|email|unique:subscriptions',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
@@ -28,7 +28,7 @@ class SubscriptionRequest extends BaseRequest
{ {
return [ return [
'email' => 'required|email', 'email' => 'required|email',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }

View File

@@ -15,8 +15,8 @@ class UserForgotPasswordRequest extends FormRequest
public function rules() public function rules()
{ {
return [ return [
'username' => 'required|exists:users,username', 'email' => 'required|exists:users,email',
'captcha_token' => [new Recaptcha()], // '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 UserForgotUsernameRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'email' => 'required|email|max:255',
'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Rules\Uniqueish;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class UserRegisterRequest extends FormRequest class UserRegisterRequest extends FormRequest
@@ -14,10 +15,8 @@ class UserRegisterRequest extends FormRequest
public function rules() public function rules()
{ {
return [ return [
'first_name' => 'required|string|max:255', 'display_name' => ['required','string','max:255', new Uniqueish('users')],
'last_name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users',
'email' => 'required|string|email|max:255',
'username' => 'required|string|min:4|max:255|unique:users',
'password' => 'required|string|min:8', 'password' => 'required|string|min:8',
]; ];
} }

View File

@@ -2,7 +2,11 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Rules\RequiredIfAny;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\RequiredIf;
use App\Rules\Uniqueish;
use Illuminate\Support\Arr;
class UserRequest extends BaseRequest class UserRequest extends BaseRequest
{ {
@@ -13,11 +17,19 @@ class UserRequest extends BaseRequest
*/ */
public function postRules() public function postRules()
{ {
$user = auth()->user();
$isAdminUser = $user->hasPermission('admin/users');
return [ return [
'username' => 'required|string|max:255|min:4|unique:users', 'first_name' => ($isAdminUser === true ? 'required_with:last_name,display_name,phone' : 'required') . '|string|max:255|min:2',
'first_name' => 'required|string|max:255|min:2', 'last_name' => ($isAdminUser === true ? 'required_with:first_name,display_name,phone' : 'required') . '|string|max:255|min:2',
'last_name' => 'required|string|max:255|min:2', 'display_name' => [
'email' => 'required|string|email|max:255', $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}$/'], 'phone' => ['string', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'email_verified_at' => 'date' 'email_verified_at' => 'date'
]; ];
@@ -30,25 +42,66 @@ class UserRequest extends BaseRequest
*/ */
public function putRules() public function putRules()
{ {
$user = $this->route('user'); $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 [ return [
'username' => [ 'first_name' => [
'sometimes',
$isAdminUser === true ? $requiredIfFieldsPresent(['last_name', 'display_name', 'phone']) : 'required',
'string', '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', 'max:255',
'min:4', Rule::unique('users')->ignore($ruleUser->id)->when(
Rule::unique('users')->ignore($user->id)->when( $this->email !== $ruleUser->email,
$this->username !== $user->username,
function ($query) { function ($query) {
return $query->where('username', $this->username); return $query->where('email', $this->email);
} }
), ),
], ],
'first_name' => 'string|max:255|min:2', 'phone' => ['nullable', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'last_name' => 'string|max:255|min:2', 'password' => "nullable|string|min:8"
'email' => 'string|email|max:255',
'phone' => ['nullable','regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'password' => 'string|min:8'
]; ];
} }
} }

View File

@@ -15,8 +15,8 @@ class UserResendVerifyEmailRequest extends FormRequest
public function rules() public function rules()
{ {
return [ return [
'username' => 'required|exists:users,username', 'email' => 'required|exists:users,email',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

View File

@@ -17,7 +17,7 @@ class UserResetPasswordRequest extends FormRequest
return [ return [
'code' => 'required|digits:6', 'code' => 'required|digits:6',
'password' => 'required|string|min:8', 'password' => 'required|string|min:8',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

View File

@@ -16,7 +16,7 @@ class UserVerifyEmailRequest extends FormRequest
{ {
return [ return [
'code' => 'required|digits:6', 'code' => 'required|digits:6',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

84
app/Jobs/MoveMediaJob.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
namespace App\Jobs;
use App\Models\Media;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class MoveMediaJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Media item
*
* @var Media
*/
public $media;
/**
* New storage ID
*
* @var string
*/
protected $newStorage;
/**
* Create a new job instance.
*
* @param Media $media The media model.
* @param string $newStorage The new storage ID.
* @return void
*/
public function __construct(Media $media, string $newStorage)
{
$this->media = $media;
$this->newStorage = $newStorage;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// Don't continue if the media is already on the new storage disk
if ($this->media->storage === $this->newStorage) {
return;
}
$this->media->status = 'Moving file';
$this->media->save();
$files = ["/{$this->media->name}"];
if (empty($this->media->variants) === false) {
foreach ($this->media->variants as $variant => $name) {
$files[] = "/{$name}";
}
}
$this->media->invalidateCFCache();
// Move the files from the old storage disk to the new storage disk
foreach ($files as $file) {
Storage::disk($this->newStorage)->put($file, Storage::disk($this->media->storage)->get($file));
Storage::disk($this->media->storage)->delete($file);
}
// Update the media model with the new storage and save it to the database
$this->media->storage = $this->newStorage;
$this->media->status = 'OK';
$this->media->save();
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Jobs;
use App\Models\Media;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use SplFileInfo;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Intervention\Image\Facades\Image;
use Spatie\ImageOptimizer\OptimizerChainFactory;
class StoreUploadedFileJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Media item
*
* @var Media
*/
protected $media;
/**
* Uploaded file item
*
* @var string
*/
protected $uploadedFilePath;
/**
* Replace existing files
*
* @var string
*/
protected $replaceExisting;
/**
* Create a new job instance.
*
* @param Media $media The media model.
* @param string $filePath The uploaded file.
* @param boolean $replaceExisting Replace existing files.
* @return void
*/
public function __construct(Media $media, string $filePath, bool $replaceExisting = true)
{
$this->media = $media;
$this->uploadedFilePath = $filePath;
$this->replaceExisting = $replaceExisting;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$storageDisk = $this->media->storage;
$fileName = $this->media->name;
try {
$this->media->status = "Uploading to CDN";
$this->media->save();
if (strlen($this->uploadedFilePath) > 0) {
if (Storage::disk($storageDisk)->exists($fileName) === false || $this->replaceExisting === true) {
Storage::disk($storageDisk)->putFileAs('/', new SplFileInfo($this->uploadedFilePath), $fileName);
Log::info("uploading file {$storageDisk} / {$fileName} / {$this->uploadedFilePath}");
} else {
Log::info("file {$fileName} already exists in {$storageDisk} / {$this->uploadedFilePath}. Not replacing file and using local {$fileName} for variants.");
}
} else {
if (Storage::disk($storageDisk)->exists($fileName) === true) {
Log::info("file {$fileName} already exists in {$storageDisk} / {$this->uploadedFilePath}. No local {$fileName} for variants, downloading from CDN.");
$readStream = Storage::disk($storageDisk)->readStream($fileName);
$tempFilePath = tempnam(sys_get_temp_dir(), 'download-');
$writeStream = fopen($tempFilePath, 'w');
while (feof($readStream) !== true) {
fwrite($writeStream, fread($readStream, 8192));
}
fclose($readStream);
fclose($writeStream);
$this->uploadedFilePath = $tempFilePath;
} else {
$errorStr = "cannot upload file {$storageDisk} / {$fileName} / {$this->uploadedFilePath} as temp file is empty";
Log::info($errorStr);
throw new \Exception($errorStr);
}
}//end if
if (strpos($this->media->mime_type, 'image/') === 0) {
$this->media->status = "Optimizing image";
$this->media->save();
// Generate additional image sizes
$sizes = Media::getTypeVariants('image');
$originalImage = Image::make($this->uploadedFilePath);
$dimensions = [$originalImage->getWidth(), $originalImage->getHeight()];
$this->media->dimensions = implode('x', $dimensions);
foreach ($sizes as $variantName => $size) {
$postfix = "{$size['width']}x{$size['height']}";
if ($variantName === 'scaled') {
$postfix = 'scaled';
}
if (is_array($this->media->variants) === true && array_key_exists($postfix, $this->media->variants) === true && Storage::disk($storageDisk)->exists($this->media->variants[$postfix]) === true && $this->replaceExisting === true) {
Storage::disk($storageDisk)->delete($this->media->variants[$postfix]);
}
$newFilename = pathinfo($this->media->name, PATHINFO_FILENAME) . "-$postfix.webp";
if (Storage::disk($storageDisk)->exists($newFilename) === false || $this->replaceExisting === true) {
// Get the largest available variant
if ($dimensions[0] >= $size['width'] && $dimensions[1] >= $size['height']) {
// Store the variant in the variants array
$variants[$variantName] = $newFilename;
// Resize the image to the variant size if its dimensions are greater than the specified size
$image = clone $originalImage;
$imageSize = $image->getSize();
if ($imageSize->getWidth() > $size['width'] || $imageSize->getHeight() > $size['height']) {
$image->resize($size['width'], $size['height'], function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$image->resizeCanvas($size['width'], $size['height'], 'center', false, '#FFFFFF');
}
// Optimize and store image
$tempImagePath = tempnam(sys_get_temp_dir(), 'optimize');
$image->encode('webp', 75)->save($tempImagePath);
Storage::disk($storageDisk)->putFileAs('/', new SplFileInfo($tempImagePath), $newFilename);
unlink($tempImagePath);
}//end if
} else {
Log::info("variant {$variantName} already exists for file {$fileName}");
}//end if
}//end foreach
// Set missing variants to the largest available variant
foreach ($sizes as $variantName => $size) {
if (isset($variants[$variantName]) === false) {
$variants[$variantName] = $this->media->name;
}
}
$this->media->variants = $variants;
}//end if
if (strlen($this->uploadedFilePath) > 0) {
unlink($this->uploadedFilePath);
}
$this->media->status = 'OK';
$this->media->save();
} catch (\Exception $e) {
Log::error($e->getMessage());
$this->media->status = "Failed";
$this->media->save();
$this->fail($e);
}//end try
}
}

View File

@@ -1,60 +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 ForgotUsername extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The list of usernames
*
* @var string[]
*/
public $usernames;
/**
* Create a new message instance.
*
* @param array $usernames The usernames.
* @return void
*/
public function __construct(array $usernames)
{
$this->usernames = $usernames;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '🤦 Forgot your username?',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.forgot_username',
text: 'emails.user.forgot_username_plain',
);
}
}

View File

@@ -15,4 +15,31 @@ class Analytics extends Model
* @var array * @var array
*/ */
protected $guarded = []; protected $guarded = [];
/**
* Create a new row in the analytics table with the given attributes,
* automatically assigning a session value based on previous rows.
*
* @param array $attributes Model attributes.
* @return static
*/
public static function createWithSession(array $attributes)
{
$previousRow = self::where('useragent', $attributes['useragent'])
->where('ip', $attributes['ip'])
->where('created_at', '>=', now()->subMinutes(30))
->whereNotNull('session')
->orderBy('created_at', 'desc')
->first();
if ($previousRow !== null) {
$attributes['session'] = $previousRow->session;
} else {
$lastSession = self::max('session');
$attributes['session'] = ($lastSession + 1);
}
return static::create($attributes);
}
} }

View File

@@ -5,8 +5,9 @@ namespace App\Models;
use App\Traits\Uuids; use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model class Article extends Model
{ {
use HasFactory; use HasFactory;
use Uuids; use Uuids;
@@ -27,7 +28,7 @@ class Post extends Model
/** /**
* Get the post user * Get the article user
* *
* @return BelongsTo * @return BelongsTo
*/ */
@@ -37,7 +38,9 @@ class Post extends Model
} }
/** /**
* Get all of the post's attachments. * Get all of the article's attachments.
*
* @return MorphMany
*/ */
public function attachments() public function attachments()
{ {

View File

@@ -4,6 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Attachment extends Model class Attachment extends Model
{ {
@@ -16,11 +18,23 @@ class Attachment extends Model
*/ */
protected $fillable = [ protected $fillable = [
'media_id', 'media_id',
'private',
];
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'private' => 'false',
]; ];
/** /**
* Get attachments attachable * Get attachments attachable
*
* @return MorphTo
*/ */
public function attachable() public function attachable()
{ {
@@ -29,6 +43,8 @@ class Attachment extends Model
/** /**
* Get the media for this attachment. * Get the media for this attachment.
*
* @return BelongsTo
*/ */
public function media() public function media()
{ {

View File

@@ -5,6 +5,8 @@ namespace App\Models;
use App\Traits\Uuids; use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Event extends Model class Event extends Model
{ {
@@ -19,6 +21,7 @@ class Event extends Model
protected $fillable = [ protected $fillable = [
'title', 'title',
'location', 'location',
'location_url',
'address', 'address',
'start_at', 'start_at',
'end_at', 'end_at',
@@ -34,10 +37,22 @@ class Event extends Model
/** /**
* Get all of the post's attachments. * Get all of the article's attachments.
*
* @return MorphMany
*/ */
public function attachments() public function attachments()
{ {
return $this->morphMany('App\Models\Attachment', 'attachable'); return $this->morphMany('App\Models\Attachment', 'attachable');
} }
/**
* Get all the associated users.
*
* @return BelongsToMany
*/
public function users()
{
return $this->belongsToMany(User::class, 'event_user', 'event_id', 'user_id');
}
} }

44
app/Models/EventUsers.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class EventUser extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'event_id',
'user_id',
];
/**
* Get the event for this attachment.
*
* @return BelongsTo
*/
public function event()
{
return $this->belongsTo(Event::class);
}
/**
* Get the user for this attachment.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -2,19 +2,37 @@
namespace App\Models; namespace App\Models;
use App\Enum\HttpResponseCodes;
use App\Jobs\MoveMediaJob;
use App\Jobs\OptimizeMediaJob;
use App\Jobs\StoreUploadedFileJob;
use App\Traits\Uuids; use App\Traits\Uuids;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Http\UploadedFile; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image; use Illuminate\Support\Str;
use Spatie\ImageOptimizer\OptimizerChainFactory; use InvalidArgumentException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Media extends Model class Media extends Model
{ {
use HasFactory; use HasFactory;
use Uuids; use Uuids;
use DispatchesJobs;
public const INVALID_FILE_ERROR = 1;
public const FILE_SIZE_EXCEEDED_ERROR = 2;
public const FILE_NAME_EXISTS_ERROR = 3;
public const TEMP_FILE_ERROR = 4;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -23,20 +41,14 @@ class Media extends Model
*/ */
protected $fillable = [ protected $fillable = [
'title', 'title',
'name',
'mime',
'user_id', 'user_id',
'mime_type',
'permission',
'storage',
'description',
'name',
'size', 'size',
'permission' 'status',
];
/**
* The attributes that are hidden.
*
* @var array<int, string>
*/
protected $hidden = [
'path',
]; ];
/** /**
@@ -48,6 +60,43 @@ class Media extends Model
'url', 'url',
]; ];
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'storage' => 'cdn',
'variants' => '[]',
'description' => '',
'dimensions' => '',
'permission' => '',
];
/**
* The storage file list cache.
*
* @var array
*/
protected static $storageFileListCache = [];
/**
* The variant types.
*
* @var int[][][]
*/
protected static $variantTypes = [
'image' => [
'thumb' => ['width' => 150, 'height' => 150],
'small' => ['width' => 300, 'height' => 225],
'medium' => ['width' => 768, 'height' => 576],
'large' => ['width' => 1024, 'height' => 768],
'xlarge' => ['width' => 1536, 'height' => 1152],
'xxlarge' => ['width' => 2048, 'height' => 1536],
'scaled' => ['width' => 2560, 'height' => 1920]
]
];
/** /**
* Model Boot * Model Boot
@@ -63,30 +112,212 @@ class Media extends Model
$origPermission = $media->getOriginal()['permission']; $origPermission = $media->getOriginal()['permission'];
$newPermission = $media->permission; $newPermission = $media->permission;
$origPath = Storage::disk(Media::getStorageId(empty($origPermission)))->path($media->name); $newPermissionLen = strlen($newPermission);
$newPath = Storage::disk(Media::getStorageId(empty($newPermission)))->path($media->name);
if ($origPath !== $newPath) { if ($newPermissionLen !== strlen($origPermission)) {
if (file_exists($origPath) === true) { if ($newPermissionLen === 0) {
if (file_exists($newPath) === true) { $this->moveToStorage('cdn');
$fileParts = pathinfo($newPath); } else {
$newName = ''; $this->moveToStorage('private');
}
// need a new name! }
$tmpPath = $newPath; }
while (file_exists($tmpPath) === true) {
$newName = uniqid('', true) . $fileParts['extension'];
$tmpPath = $fileParts['dirname'] . '/' . $newName;
}
$media->name = $newName;
}
rename($origPath, $newPath);
}//end if
}//end if
}//end if
}); });
static::deleting(function ($media) {
$media->deleteFile();
});
}
/**
* Get Type Variants.
*
* @param string $type The variant type to get.
* @return array The variant data.
*/
public static function getTypeVariants(string $type)
{
if (isset(self::$variantTypes[$type]) === true) {
return self::$variantTypes[$type];
}
return [];
}
/**
* Variants Get Mutator.
*
* @param mixed $value The value to mutate.
* @return array The mutated value.
*/
public function getVariantsAttribute(mixed $value)
{
if (is_string($value) === true) {
return json_decode($value, true);
}
return [];
}
/**
* Variants Set Mutator.
*
* @param mixed $value The value to mutate.
* @return void
*/
public function setVariantsAttribute(mixed $value)
{
if (is_array($value) !== true) {
$value = [];
}
$this->attributes['variants'] = json_encode(($value ?? []));
}
/**
* Get previous variant.
*
* @param string $type The variant type.
* @param string $variant The initial variant.
* @return string The previous variant name (or '').
*/
public function getPreviousVariant(string $type, string $variant)
{
if (isset(self::$variantTypes[$type]) === false) {
return '';
}
$variants = self::$variantTypes[$type];
$keys = array_keys($variants);
$currentIndex = array_search($variant, $keys);
if ($currentIndex === false || $currentIndex === 0) {
return '';
}
return $keys[($currentIndex - 1)];
}
/**
* Get next variant.
*
* @param string $type The variant type.
* @param string $variant The initial variant.
* @return string The next variant name (or '').
*/
public function getNextVariant(string $type, string $variant)
{
if (isset(self::$variantTypes[$type]) === false) {
return '';
}
$variants = self::$variantTypes[$type];
$keys = array_keys($variants);
$currentIndex = array_search($variant, $keys);
if ($currentIndex === false || $currentIndex === (count($keys) - 1)) {
return '';
}
return $keys[($currentIndex + 1)];
}
/**
* Get variant URL.
*
* @param string $variant The variant to find.
* @param boolean $returnNearest Return the nearest variant if request is not found.
* @return string The URL.
*/
public function getVariantURL(string $variant, bool $returnNearest = true)
{
$variants = $this->variants;
if (isset($variants[$variant]) === true) {
return self::getUrlPath() . $variants[$variant];
}
if ($returnNearest === true) {
$variantType = explode('/', $this->mime_type)[0];
$previousVariant = $variant;
while (empty($previousVariant) === false) {
$previousVariant = $this->getPreviousVariant($variantType, $previousVariant);
if (empty($previousVariant) === false && isset($variants[$previousVariant]) === true) {
return self::getUrlPath() . $variants[$previousVariant];
}
}
}
return '';
}
/**
* Delete file and associated files with the modal.
*
* @return void
*/
public function deleteFile()
{
$fileName = $this->name;
$baseName = pathinfo($fileName, PATHINFO_FILENAME);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$files = Storage::disk($this->storage)->files();
foreach ($files as $file) {
if (preg_match("/{$baseName}(-[a-zA-Z0-9]+)?\.{$extension}/", $file) === 1) {
Storage::disk($this->storage)->delete($file);
}
}
$this->invalidateCFCache();
}
/**
* Invalidate Cloudflare Cache.
*
* @return void
* @throws InvalidArgumentException Exception.
*/
private function invalidateCFCache()
{
$zone_id = env("CLOUDFLARE_ZONE_ID");
$api_key = env("CLOUDFLARE_API_KEY");
if ($zone_id !== null && $api_key !== null && $this->url !== "") {
$urls = [$this->url];
foreach ($this->variants as $variant => $name) {
$urls[] = str_replace($this->name, $name, $this->url);
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cloudflare.com/client/v4/zones/" . $zone_id . "/purge_cache",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_POSTFIELDS => json_encode(["files" => $urls]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Bearer " . $api_key
],
]);
curl_exec($curl);
curl_close($curl);
}//end if
}
/**
* Get URL path
*
* @return string
*/
public function getUrlPath()
{
$url = config("filesystems.disks.$this->storage.url");
return "$url/";
} }
/** /**
@@ -96,19 +327,9 @@ class Media extends Model
*/ */
public function getUrlAttribute() public function getUrlAttribute()
{ {
$url = config('filesystems.disks.' . Media::getStorageId($this) . '.url'); if (isset($this->attributes['name']) === true) {
if (empty($url) === false) { return self::getUrlPath() . $this->name;
$replace = [ }
'id' => $this->id,
'name' => $this->name
];
$url = str_ireplace(array_map(function ($item) {
return '%' . $item . '%';
}, array_keys($replace)), array_values($replace), $url);
return $url;
}//end if
return ''; return '';
} }
@@ -124,84 +345,151 @@ class Media extends Model
} }
/** /**
* Get the file full local path * Move files to new storage device.
* *
* @return string * @param string $storage The storage ID to move to.
* @return void
*/ */
public function path() public function moveToStorage(string $storage)
{ {
return Storage::disk(Media::getStorageId($this))->path($this->name); if ($storage !== $this->storage && Config::has("filesystems.disks.$storage") === true) {
$this->status = "Processing media";
MoveMediaJob::dispatch($this, $storage)->onQueue('media');
$this->save();
}
} }
/** /**
* Get Storage ID * Create new Media from UploadedFile data.
* *
* @param mixed $mediaOrPublic Media object or if file is public. * @param App\Models\Request $request The request data.
* @return string * @param Illuminate\Http\UploadedFile $file The file.
* @return null|Media The result or null if not successful.
*/ */
public static function getStorageId(mixed $mediaOrPublic) public static function createFromUploadedFile(Request $request, UploadedFile $file)
{ {
$isPublic = true; $request->merge([
'title' => $request->get('title', ''),
'name' => '',
'size' => 0,
'mime_type' => '',
'status' => '',
]);
if ($mediaOrPublic instanceof Media) { if ($request->get('storage') === null) {
$isPublic = empty($mediaOrPublic->permission); // We store images by default locally
} else { if (strpos($file->getMimeType(), 'image/') === 0) {
$isPublic = boolval($mediaOrPublic); $request->merge([
'storage' => 'local',
]);
} else {
$request->merge([
'storage' => 'cdn',
]);
}
} }
return $isPublic === true ? 'public' : 'local'; $mediaItem = $request->user()->media()->create($request->all());
$mediaItem->updateWithUploadedFile($file);
return $mediaItem;
} }
/** /**
* Place uploaded file into storage. Return full path or null * Update Media with UploadedFile data.
* *
* @param UploadedFile $file File to put into storage. * @param Illuminate\Http\UploadedFile $file The file.
* @param boolean $public Is the file available to the public. * @return null|Media The media item.
* @return array|null
*/ */
public static function store(UploadedFile $file, bool $public = true) public function updateWithUploadedFile(UploadedFile $file)
{ {
$storage = Media::getStorageId($public); if ($file === null || $file->isValid() !== true) {
$name = $file->store('', ['disk' => $storage]); throw new \Exception('The file is invalid.', self::INVALID_FILE_ERROR);
}
if ($file->getSize() > static::getMaxUploadSize()) {
throw new \Exception('The file size is larger then permitted.', self::FILE_SIZE_EXCEEDED_ERROR);
}
$name = static::generateUniqueFileName($file->getClientOriginalName());
if ($name === false) { if ($name === false) {
return null; throw new \Exception('The file name already exists in storage.', self::FILE_NAME_EXISTS_ERROR);
} }
$path = Storage::disk($storage)->path($name); // remove file if there is an existing entry in this medium item
if (in_array($file->getClientOriginalExtension(), ['jpg', 'jpeg', 'png', 'gif']) === true) { if (strlen($this->name) > 0 && strlen($this->storage) > 0) {
// Generate additional image sizes Storage::disk($this->storage)->delete($this->name);
$sizes = [ foreach ($this->variants as $variantName => $fileName) {
'thumb' => [150, 150], Storage::disk($this->storage)->delete($fileName);
'small' => [300, 300],
'medium' => [640, 640],
'large' => [1024, 1024],
'xlarge' => [1536, 1536],
'xxlarge' => [2560, 2560],
];
$images = ['full' => $path];
foreach ($sizes as $sizeName => $size) {
$image = Image::make($path);
$image->resize($size[0], $size[1], function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$newPath = pathinfo($path, PATHINFO_DIRNAME) . '/' . pathinfo($path, PATHINFO_FILENAME) . "-$sizeName." . pathinfo($path, PATHINFO_EXTENSION);
$image->save($newPath);
$images[$sizeName] = $newPath;
} }
// Optimize all images $this->name = '';
$optimizerChain = OptimizerChainFactory::create(); $this->variants = [];
foreach ($images as $imagePath) { }
$optimizerChain->optimize($imagePath);
}
}//end if
return [ if (strlen($this->title) === 0) {
'name' => $name, $this->title = $name;
'path' => $path }
];
$this->name = $name;
$this->size = $file->getSize();
$this->mime_type = $file->getMimeType();
$this->status = 'Processing media';
$this->save();
$temporaryFilePath = generateTempFilePath();
copy($file->path(), $temporaryFilePath);
try {
StoreUploadedFileJob::dispatch($this, $temporaryFilePath)->onQueue('media');
} catch (\Exception $e) {
$this->status = 'Error';
$this->save();
throw $e;
}//end try
return $this;
}
/**
* Download the file from the storage to the user.
*
* @param string $variant The variant to download or null if none.
* @param boolean $fallback Fallback to the original file if the variant is not found.
* @return JsonResponse|StreamedResponse The response.
* @throws BindingResolutionException The Exception.
*/
public function download(string $variant = null, bool $fallback = true)
{
$path = $this->name;
if ($variant !== null) {
if (array_key_exists($variant, $this->variant) === true) {
$path = $this->variant[$variant];
} else {
return response()->json(['message' => 'The resource was not found.'], HttpResponseCodes::HTTP_NOT_FOUND);
}
}
$disk = Storage::disk($this->storage);
if ($disk->exists($path) === true) {
$stream = $disk->readStream($path);
$response = response()->stream(
function () use ($stream) {
fpassthru($stream);
},
200,
[
'Content-Type' => $this->mime_type,
'Content-Length' => $disk->size($path),
'Content-Disposition' => 'attachment; filename="' . basename($path) . '"',
]
);
return $response;
}
return response()->json(['message' => 'The resource was not found.'], HttpResponseCodes::HTTP_NOT_FOUND);
} }
/** /**
@@ -209,7 +497,7 @@ class Media extends Model
* *
* @return integer * @return integer
*/ */
public static function maxUploadSize() public static function getMaxUploadSize()
{ {
$sizes = [ $sizes = [
ini_get('upload_max_filesize'), ini_get('upload_max_filesize'),
@@ -237,31 +525,123 @@ class Media extends Model
} }
/** /**
* Sanitize filename for upload * Generate a file name that is available within storage.
* *
* @param string $filename Filename to sanitize. * @param string $fileName The proposed file name.
* @return string|boolean The available file name or false if failed.
*/
public static function generateUniqueFileName(string $fileName)
{
$index = 1;
$maxTries = 100;
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$fileName = static::sanitizeFilename(pathinfo($fileName, PATHINFO_FILENAME));
if (static::fileNameHasSuffix($fileName) === true || static::fileExistsInStorage("$fileName.$extension") === true || Media::where('name', "$fileName.$extension")->where('status', 'not like', 'failed%')->exists() === true) {
$fileName .= '-';
for ($i = 1; $i < $maxTries; $i++) {
$fileNameIndex = $fileName . $index;
if (static::fileExistsInStorage("$fileNameIndex.$extension") !== true && Media::where('name', "$fileNameIndex.$extension")->where('status', 'not like', 'Failed%')->exists() !== true) {
return "$fileNameIndex.$extension";
}
++$index;
}
return false;
}
return "$fileName.$extension";
}
/**
* Determines if the file name exists in any of the storage disks.
*
* @param string $fileName The file name to check.
* @param boolean $ignoreCache Ignore the file list cache.
* @return boolean If the file exists on any storage disks.
*/
public static function fileExistsInStorage(string $fileName, bool $ignoreCache = false)
{
$disks = array_keys(Config::get('filesystems.disks'));
if ($ignoreCache === false) {
if (count(static::$storageFileListCache) === 0) {
$disks = array_keys(Config::get('filesystems.disks'));
foreach ($disks as $disk) {
try {
static::$storageFileListCache[$disk] = Storage::disk($disk)->allFiles();
} catch (\Exception $e) {
Log::error($e->getMessage());
throw new \Exception("Cannot get a file list for storage device '$disk'");
}
}
}
foreach (static::$storageFileListCache as $disk => $files) {
if (in_array($fileName, $files) === true) {
return true;
}
}
} else {
$disks = array_keys(Config::get('filesystems.disks'));
foreach ($disks as $disk) {
try {
if (Storage::disk($disk)->exists($fileName) === true) {
return true;
}
} catch (\Exception $e) {
Log::error($e->getMessage());
throw new \Exception("Cannot verify if file '$fileName' already exists in storage device '$disk'");
}
}
}//end if
return false;
}
/**
* Test if the file name contains a special suffix.
*
* @param string $fileName The file name to test.
* @return boolean If the file name contains the special suffix.
*/
public static function fileNameHasSuffix(string $fileName)
{
$suffix = '/(-\d+x\d+|-scaled)$/i';
$fileNameWithoutExtension = pathinfo($fileName, PATHINFO_FILENAME);
return preg_match($suffix, $fileNameWithoutExtension) === 1;
}
/**
* Sanitize fileName for upload
*
* @param string $fileName Filename to sanitize.
* @return string * @return string
*/ */
public static function sanitizeFilename(string $filename) private static function sanitizeFilename(string $fileName)
{ {
/* /*
# file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
[<>:"/\\\|?*]| [<>:"/\\\|?*]|
# control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
[\x00-\x1F]| [\x00-\x1F]|
# non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
[\x7F\xA0\xAD]| [\x7F\xA0\xAD]|
# URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2 # URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2
[#\[\]@!$&\'()+,;=]| [#\[\]@!$&\'()+,;=]|
# URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
[{}^\~`] [{}^\~`]
*/ */
$filename = preg_replace( $fileName = preg_replace(
'~ '~
[<>:"/\\\|?*]| [<>:"/\\\|?*]|
[\x00-\x1F]| [\x00-\x1F]|
@@ -270,37 +650,37 @@ class Media extends Model
[{}^\~`] [{}^\~`]
~x', ~x',
'-', '-',
$filename $fileName
); );
$filename = ltrim($filename, '.-'); $fileName = ltrim($fileName, '.-');
$filename = preg_replace([ $fileName = preg_replace([
// "file name.zip" becomes "file-name.zip" // "file name.zip" becomes "file-name.zip"
'/ +/', '/ +/',
// "file___name.zip" becomes "file-name.zip" // "file___name.zip" becomes "file-name.zip"
'/_+/', '/_+/',
// "file---name.zip" becomes "file-name.zip" // "file---name.zip" becomes "file-name.zip"
'/-+/' '/-+/'
], '-', $filename); ], '-', $fileName);
$filename = preg_replace([ $fileName = preg_replace([
// "file--.--.-.--name.zip" becomes "file.name.zip" // "file--.--.-.--name.zip" becomes "file.name.zip"
'/-*\.-*/', '/-*\.-*/',
// "file...name..zip" becomes "file.name.zip" // "file...name..zip" becomes "file.name.zip"
'/\.{2,}/' '/\.{2,}/'
], '.', $filename); ], '.', $fileName);
// lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 // lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625
$filename = mb_strtolower($filename, mb_detect_encoding($filename)); $fileName = mb_strtolower($fileName, mb_detect_encoding($fileName));
// ".file-name.-" becomes "file-name" // ".file-name.-" becomes "file-name"
$filename = trim($filename, '.-'); $fileName = trim($fileName, '.-');
$ext = pathinfo($filename, PATHINFO_EXTENSION); $ext = pathinfo($fileName, PATHINFO_EXTENSION);
$filename = mb_strcut( $fileName = mb_strcut(
pathinfo($filename, PATHINFO_FILENAME), pathinfo($fileName, PATHINFO_FILENAME),
0, 0,
(255 - ($ext !== '' ? strlen($ext) + 1 : 0)), (255 - ($ext !== '' ? strlen($ext) + 1 : 0)),
mb_detect_encoding($filename) mb_detect_encoding($fileName)
) . ($ext !== '' ? '.' . $ext : ''); ) . ($ext !== '' ? '.' . $ext : '');
return $filename; return $fileName;
} }
} }

39
app/Models/Shortlink.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use App\Enum\HttpResponseCodes;
use App\Jobs\MoveMediaJob;
use App\Jobs\OptimizeMediaJob;
use App\Jobs\StoreUploadedFileJob;
use App\Traits\Uuids;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Shortlink extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'code',
'url',
];
}

View File

@@ -6,6 +6,7 @@ namespace App\Models;
use App\Traits\Uuids; use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
@@ -25,12 +26,12 @@ class User extends Authenticatable implements Auditable
* @var array<int, string> * @var array<int, string>
*/ */
protected $fillable = [ protected $fillable = [
'username',
'first_name', 'first_name',
'last_name', 'last_name',
'email', 'email',
'phone', 'phone',
'password', 'password',
'display_name',
]; ];
/** /**
@@ -66,10 +67,14 @@ class User extends Authenticatable implements Auditable
'permissions' 'permissions'
]; ];
/**
// public function getPermissionsAttribute() { * The default attributes.
// return $this->permissions()->pluck('permission')->toArray(); *
// } * @var string[]
*/
protected $attributes = [
'phone' => '',
];
/** /**
@@ -111,7 +116,7 @@ class User extends Authenticatable implements Auditable
*/ */
public function givePermission($permissions) public function givePermission($permissions)
{ {
if (!is_array($permissions)) { if (is_array($permissions) === false) {
$permissions = [$permissions]; $permissions = [$permissions];
} }
@@ -132,11 +137,11 @@ class User extends Authenticatable implements Auditable
* Revoke permissions from the user * Revoke permissions from the user
* *
* @param string|array $permissions The permission(s) to revoke. * @param string|array $permissions The permission(s) to revoke.
* @return int * @return integer
*/ */
public function revokePermission($permissions) public function revokePermission($permissions)
{ {
if (!is_array($permissions)) { if (is_array($permissions) === false) {
$permissions = [$permissions]; $permissions = [$permissions];
} }
@@ -160,9 +165,9 @@ class User extends Authenticatable implements Auditable
* *
* @return HasMany * @return HasMany
*/ */
public function posts() public function articles()
{ {
return $this->hasMany(Post::class); return $this->hasMany(Article::class);
} }
/** /**
@@ -184,4 +189,14 @@ class User extends Authenticatable implements Auditable
{ {
return $this->hasMany(UserLogins::class); return $this->hasMany(UserLogins::class);
} }
/**
* Get the events associated with the user.
*
* @return BelongsToMany
*/
public function events()
{
return $this->belongsToMany(Event::class, 'event_user', 'user_id', 'event_id');
}
} }

View File

@@ -2,10 +2,15 @@
namespace App\Providers; namespace App\Providers;
use App\Rules\RequiredIfAny;
use App\Rules\Uniqueish;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Exception; use Exception;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use PDOException; use PDOException;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@@ -26,6 +31,9 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
// Storage::macro('public', function ($diskName) {
$public = config("filesystems.disks.{$diskName}.public", false);
return $public;
});
} }
} }

View File

@@ -31,22 +31,7 @@ class EventServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
Queue::after(function (JobProcessed $event) { //
// Log::info($event->connectionName);
// Log::info('ID: ' . $event->job->getJobId());
// Log::info('Attempts: ' . $event->job->attempts());
// Log::info('Name: ' . $event->job->getName());
// Log::info('ResolveNAme: ' . $event->job->resolveName());
// Log::info('Queue: ' . $event->job->getQueue());
// Log::info('Body: ' . $event->job->getRawBody());
// Log::info(print_r($event->job->payload(), true));
// $payload = $event->job->payload();
// $data = unserialize($payload['data']['command']);
// Log::info('MAIL: ' . $data->to);
// Log::info('MAIL: ' . get_class($data->mailable));
});
} }
/** /**

View File

@@ -7,6 +7,7 @@ use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvi
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
@@ -37,6 +38,22 @@ class RouteServiceProvider extends ServiceProvider
Route::middleware('web') Route::middleware('web')
->group(base_path('routes/web.php')); ->group(base_path('routes/web.php'));
}); });
Route::macro('apiAttachmentResource', function ($uri, $controller) {
$singularUri = Str::singular($uri);
Route::get("$uri/{{$singularUri}}/attachments", [$controller, 'getAttachments'])
->name("{{$singularUri}}.attachments.index");
Route::post("$uri/{{$singularUri}}/attachments", [$controller, 'storeAttachment'])
->name("{{$singularUri}}.attachments.store");
Route::match(['put', 'patch'], "$uri/{{$singularUri}}/attachments", [$controller, 'updateAttachments'])
->name("{{$singularUri}}.attachments.update");
Route::delete("$uri/{{$singularUri}}/attachments/{medium}", [$controller, 'deleteAttachment'])
->name("{{$singularUri}}.attachments.destroy");
});
} }
/** /**

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Rules;
use App\Models\Media;
use Illuminate\Contracts\Validation\Rule;
class UniqueFileName implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return boolean
*/
public function passes($attribute, $value)
{
return (Media::fileExists($value) === false);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The file name already exists.';
}
}

107
app/Rules/Uniqueish.php Normal file
View File

@@ -0,0 +1,107 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\DB;
use PDO;
class Uniqueish implements Rule
{
/**
* The table name to compare.
*
* @var string
*/
protected $table;
/**
* The column name to compare.
*
* @var string|null
*/
protected $column;
/**
* The ID of the record to be ignored.
*
* @var mixed
*/
protected $ignoreId;
/**
* Create a new rule instance.
*
* @param string $table The table name to compare.
* @param string $column The column name to compare.
* @return void
*/
public function __construct(string $table, string $column = null)
{
$this->table = $table;
$this->column = $column;
}
/**
* Set the ID of the record to be ignored.
*
* @param mixed $id The ID to ignore.
* @return $this
*/
public function ignore(mixed $id)
{
$this->ignoreId = $id;
return $this;
}
/**
* Determine if the validation rule passes.
*
* @param mixed $attribute Not used.
* @param mixed $value The value to compare.
* @return boolean
*/
public function passes(mixed $attribute, mixed $value)
{
$columnName = ($this->column ?? $attribute);
$similarValue = preg_replace('/[^A-Za-z]/', '', strtolower($value));
try {
$query = DB::table($this->table)
->where($columnName, 'like', '%' . $similarValue . '%');
if ($this->ignoreId !== null) {
$query->where('id', '<>', $this->ignoreId);
}
$query->whereRaw('LOWER(REGEXP_REPLACE(' . $columnName . ', \'[^A-Za-z0-9]\', \'\')) = ?', [$similarValue]);
$result = $query->first();
} catch (\Illuminate\Database\QueryException $e) {
// Fall back to performing the regex replace in PHP
// $results = $query->get();
$query = DB::table($this->table);
$results = $query->get();
foreach ($results as $result) {
$resultValue = preg_replace('/[^A-Za-z0-9]/', '', strtolower($result->{$columnName}));
if ($resultValue === $similarValue && $result->id != $this->ignoreId) {
return false; // Value already exists in the table
}
}
return true; // Value does not exist in the table
}//end try
return $result === null;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The :attribute is similar to one that already exists. Please choose another.';
}
}

View File

@@ -15,23 +15,26 @@
"laravel/framework": "^9.19", "laravel/framework": "^9.19",
"laravel/sanctum": "^3.0", "laravel/sanctum": "^3.0",
"laravel/tinker": "^2.7", "laravel/tinker": "^2.7",
"league/flysystem-aws-s3-v3": "^3.12",
"owen-it/laravel-auditing": "^13.0", "owen-it/laravel-auditing": "^13.0",
"php-ffmpeg/php-ffmpeg": "^1.1", "php-ffmpeg/php-ffmpeg": "^1.1",
"spatie/image-optimizer": "^1.6", "sunspikes/clamav-validator": "*",
"thiagoalessio/tesseract_ocr": "^2.12" "thiagoalessio/tesseract_ocr": "^2.12",
"vlucas/phpdotenv": "^5.5"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"laravel/pint": "^1.0", "laravel/pint": "^1.0",
"laravel/sail": "^1.0.1", "laravel/sail": "^1.0.1",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^6.1", "nunomaduro/collision": "^7.1",
"phpunit/phpunit": "^9.5.10", "phpunit/phpunit": "^10.1.3",
"spatie/laravel-ignition": "^1.0" "spatie/laravel-ignition": "^1.0"
}, },
"autoload": { "autoload": {
"files": [ "files": [
"app/Helpers/Array.php" "app/Helpers/Array.php",
"app/Helpers/Temp.php"
], ],
"psr-4": { "psr-4": {
"App\\": "app/", "App\\": "app/",

2046
composer.lock generated

File diff suppressed because it is too large Load Diff

68
config/clamav.php Normal file
View File

@@ -0,0 +1,68 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Preferred socket
|--------------------------------------------------------------------------
|
| This option controls the socket which is used, which is unix_socket or tcp_socket.
|
| Please note if the unix_socket is used and the socket-file is not found the tcp socket will be
| used as fallback.
*/
'preferred_socket' => env('CLAMAV_PREFERRED_SOCKET', 'unix_socket'),
/*
|--------------------------------------------------------------------------
| Unix Socket
|--------------------------------------------------------------------------
| This option defines the location to the unix socket-file. For example
| /var/run/clamav/clamd.ctl
*/
'unix_socket' => env('CLAMAV_UNIX_SOCKET', '/var/run/clamav/clamd.ctl'),
/*
|--------------------------------------------------------------------------
| TCP Socket
|--------------------------------------------------------------------------
| This option defines the TCP socket to the ClamAV instance.
*/
'tcp_socket' => env('CLAMAV_TCP_SOCKET', 'tcp://127.0.0.1:3310'),
/*
|--------------------------------------------------------------------------
| Socket connect timeout
|--------------------------------------------------------------------------
| This option defines the maximum time to wait in seconds for socket connection attempts before failure or timeout, default null = no limit.
*/
'socket_connect_timeout' => env('CLAMAV_SOCKET_CONNECT_TIMEOUT', null),
/*
|--------------------------------------------------------------------------
| Socket read timeout
|--------------------------------------------------------------------------
| This option defines the maximum time to wait in seconds for a read.
*/
'socket_read_timeout' => env('CLAMAV_SOCKET_READ_TIMEOUT', 30),
/*
|--------------------------------------------------------------------------
| Throw exceptions instead of returning failures when scan fails.
|--------------------------------------------------------------------------
| This makes it easier for a developer to find the source of a clamav
| failure, but an end user may only see a 500 error for the user
| if exceptions are not displayed.
*/
'client_exceptions' => env('CLAMAV_CLIENT_EXCEPTIONS', false),
/*
|--------------------------------------------------------------------------
| Skip validation
|--------------------------------------------------------------------------
| This skips the virus validation for current environment.
|
| Please note when true it won't connect to ClamAV and will skip the virus validation.
*/
'skip_validation' => env('CLAMAV_SKIP_VALIDATION', false),
];

View File

@@ -29,31 +29,43 @@ return [
*/ */
'disks' => [ 'disks' => [
'local' => [ 'local' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('app/uploads'), 'root' => storage_path('app/public'),
'throw' => false, 'url' => env('APP_URL') . "/storage",
'url' => env('STORAGE_LOCAL_URL'), 'public' => true,
], ],
'public' => [ 'cdn' => [
'driver' => 'local',
'root' => public_path('uploads'),
'throw' => false,
'url' => env('STORAGE_PUBLIC_URL'),
],
's3' => [
'driver' => 's3', 'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'), 'key' => env('AWS_PUBLIC_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'), 'secret' => env('AWS_PUBLIC_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'), 'region' => env('AWS_PUBLIC_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'), 'bucket' => env('AWS_PUBLIC_BUCKET'),
'url' => env('AWS_URL'), 'url' => env('AWS_PUBLIC_URL'),
'endpoint' => env('AWS_ENDPOINT'), 'endpoint' => env('AWS_PUBLIC_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 'use_path_style_endpoint' => env('AWS_PUBLIC_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false, 'throw' => false,
'public' => true,
'options' => [
'ACL' => '',
]
],
'private' => [
'driver' => 's3',
'key' => env('AWS_PRIVATE_ACCESS_KEY_ID'),
'secret' => env('AWS_PRIVATE_SECRET_ACCESS_KEY'),
'region' => env('AWS_PRIVATE_DEFAULT_REGION'),
'bucket' => env('AWS_PRIVATE_BUCKET'),
'url' => env('AWS_PRIVATE_URL'),
'endpoint' => env('AWS_PRIVATE_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_PRIVATE_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'public' => false,
'options' => [
'ACL' => '',
]
], ],
], ],

View File

@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event> * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
*/ */
class PostFactory extends Factory class ArticleFactory extends Factory
{ {
/** /**
* Define the model's default state. * Define the model's default state.

View File

@@ -20,10 +20,9 @@ class MediaFactory extends Factory
return [ return [
'title' => $this->faker->sentence(), 'title' => $this->faker->sentence(),
'name' => storage_path('app/public/') . $this->faker->slug() . '.' . $this->faker->fileExtension, 'name' => storage_path('app/public/') . $this->faker->slug() . '.' . $this->faker->fileExtension,
'mime' => $this->faker->mimeType, 'mime_type' => $this->faker->mimeType,
'user_id' => $this->faker->uuid, 'user_id' => $this->faker->uuid,
'size' => $this->faker->numberBetween(1000, 1000000), 'size' => $this->faker->numberBetween(1000, 1000000),
'permission' => null
]; ];
} }
} }

View File

@@ -20,15 +20,20 @@ class UserFactory extends Factory
$faker = \Faker\Factory::create(); $faker = \Faker\Factory::create();
$faker->addProvider(new \Faker\Provider\CustomInternetProvider($faker)); $faker->addProvider(new \Faker\Provider\CustomInternetProvider($faker));
$first_name = $faker->firstName();
$last_name = $faker->lastName();
$display_name = $first_name . ' ' . $last_name;
return [ return [
'username' => $faker->unique()->userNameWithMinLength(6), 'first_name' => $first_name,
'first_name' => $faker->firstName(), 'last_name' => $last_name,
'last_name' => $faker->lastName(),
'email' => $faker->safeEmail(), 'email' => $faker->safeEmail(),
'email_verified_at' => now(), 'email_verified_at' => now(),
'phone' => $faker->phoneNumber(), 'phone' => $faker->phoneNumber(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10), 'remember_token' => Str::random(10),
'display_name' => $display_name,
]; ];
} }

View File

@@ -22,8 +22,6 @@ return new class extends Migration
$table->uuid('hero'); $table->uuid('hero');
$table->text('content'); $table->text('content');
$table->timestamps(); $table->timestamps();
// $table->foreign('user_id')->references('id')->on('users');
}); });
} }

View File

@@ -22,8 +22,6 @@ return new class extends Migration
$table->string('permission')->nullable(); $table->string('permission')->nullable();
$table->bigInteger('size'); $table->bigInteger('size');
$table->timestamps(); $table->timestamps();
// $table->foreign('user_id')->references('id')->on('users');
}); });
} }

View File

@@ -30,6 +30,6 @@ return new class extends Migration
*/ */
public function down() public function down()
{ {
Schema::dropIfExists('media_attachments'); Schema::dropIfExists('attachments');
} }
}; };

View File

@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('media')->whereNull('mime')->update(['mime' => '']);
DB::table('media')->whereNull('permission')->update(['permission' => '']);
Schema::table('media', function (Blueprint $table) {
$table->string('storage')->default("cdn");
$table->string('description')->default("");
$table->string('status')->default("");
$table->string('dimensions')->default("");
$table->text('variants');
$table->bigInteger('size')->default(0)->change();
$table->string('permission')->default("")->nullable(false)->change();
$table->string('mime')->default("")->nullable(false)->change();
});
Schema::table('media', function(Blueprint $table) {
$table->renameColumn('mime', 'mime_type');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('media', function (Blueprint $table) {
$table->bigInteger('size')->change();
$table->string('mime_type')->nullable(true)->change();
$table->string('permission')->nullable(true)->change();
$table->renameColumn('mime_type', 'mime');
$table->dropColumn('status');
$table->dropColumn('dimensions');
$table->dropColumn('variants');
$table->dropColumn('description');
$table->dropColumn('storage');
});
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('users')->whereNull('phone')->update(['phone' => '']);
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->default("")->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->nullable(true)->change();
});
}
};

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('display_name')->default("");
});
// Update existing rows with display_name
DB::table('users')->select('id', 'username')->orderBy('id')->chunk(100, function ($users) {
foreach ($users as $user) {
DB::table('users')->where('id', $user->id)->update(['display_name' => $user->username]);
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('display_name');
});
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('subscriptions');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::create('subscriptions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('email');
$table->timestamp('confirmed_at')->nullable();
$table->timestamps();
});
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::rename('posts', 'articles');
// Update permissions to use articles instead of posts
DB::table('permissions')->select('id', 'permission')->where('permission', 'admin/posts')->update(['permission' => 'admin/articles']);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::rename('articles', 'posts');
// Update permissions to use posts instead of articles
DB::table('permissions')->select('id', 'permission')->where('permission', 'admin/articles')->update(['permission' => 'admin/posts']);
}
};

View File

@@ -0,0 +1,109 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('analytics', function (Blueprint $table) {
$table->bigInteger('session')->nullable(false);
$table->string('attribute')->default('')->change();
});
DB::table('analytics')
->where('type', 'pageview')
->update(['type' => 'apirequest']);
// Set first session
$session = 0;
do {
$rows = DB::table('analytics')
->whereNull('session')
->orWhere('session', 0)
->orderBy('created_at', 'asc')
->limit(1)
->get();
if($rows->isEmpty()) {
break;
}
$sessionRow = $rows->first();
DB::table('analytics')->where('id', $sessionRow->id)->update(['session' => ++$session]);
$lastSessionUpdate = $sessionRow->created_at;
do {
$sameSessionRows = DB::table('analytics')
->whereNull('session')
->orWhere('session', 0)
->where('useragent', $sessionRow->useragent)
->where('created_at', '<=', date('Y-m-d H:i:s', strtotime('30 minutes', strtotime($lastSessionUpdate))))
->orderBy('created_at', 'desc')
->get();
if($sameSessionRows->isEmpty()) {
break;
}
$ids = $sameSessionRows->pluck('id')->toArray();
DB::table('analytics')->whereIn('id', $ids)->update(['session' => $session]);
$lastSessionUpdate = $sameSessionRows->first()->created_at;
} while(true);
} while(true);
// Loop through the rows and update `session` based on the logic you described
// foreach ($rows as $row) {
// // Check if this is the first row
// if ($row->created_at === $rows->first()->created_at) {
// DB::table('analytics')
// ->where('id', $row->id)
// ->update(['session' => $session]);
// } else {
// // Look for a previous row with the same useragent and ip within the last 30 minutes
// $previousRow = DB::table('analytics')
// ->where('useragent', $row->useragent)
// ->where('ip', $row->ip)
// ->where('created_at', '>=', date('Y-m-d H:i:s', strtotime('-30 minutes', strtotime($row->created_at))))
// ->whereNotNull('session')
// ->orderBy('created_at', 'desc')
// ->first();
// if ($previousRow) {
// // If a previous row is found, set the session to the same value
// DB::table('analytics')
// ->where('id', $row->id)
// ->update(['session' => $previousRow->session]);
// } else {
// // If no previous row is found, increment the session value
// $session++;
// DB::table('analytics')
// ->where('id', $row->id)
// ->update(['session' => $session]);
// }
// }
// }
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('analytics', function (Blueprint $table) {
$table->dropColumn('session');
});
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('username');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('username')->unique();
});
DB::table('users')->update(['username' => DB::raw('display_name')]);
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('shortlinks', function (Blueprint $table) {
$table->id();
$table->string('code', 4)->unique();
$table->string('url');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('shortlinks');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('shortlinks', function (Blueprint $table) {
$table->bigInteger('used')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('shortlinks', function (Blueprint $table) {
//
});
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('first_name')->default('')->change();
$table->string('last_name')->default('')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('first_name')->nullable(false)->change();
$table->string('last_name')->nullable(false)->change();
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('events', function (Blueprint $table) {
$table->string('location_url')->default('');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('events', function (Blueprint $table) {
$table->dropColumn('location_url');
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('attachments', function (Blueprint $table) {
$table->boolean('private')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('attachments', function (Blueprint $table) {
$table->dropColumn('private');
});
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('event_users', function (Blueprint $table) {
$table->id();
$table->uuid('event_id');
$table->uuid('user_id');
$table->timestamps();
$table->foreign('event_id')->references('id')->on('events')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('event_users');
}
};

View File

@@ -20,7 +20,7 @@ class DatabaseSeeder extends Seeder
\App\Models\User::factory(40)->create(); \App\Models\User::factory(40)->create();
\App\Models\User::factory()->create([ \App\Models\User::factory()->create([
'username' => 'nomadjimbob', 'display_name' => 'James Collins',
'first_name' => 'James', 'first_name' => 'James',
'last_name' => 'Collins', 'last_name' => 'Collins',
'email' => 'james@stemmechanics.com.au', 'email' => 'james@stemmechanics.com.au',

1657
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,21 +13,22 @@
"@typescript-eslint/parser": "^5.48.1", "@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.31.0", "eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^8.6.0",
"eslint-plugin-jsdoc": "^39.6.4", "eslint-plugin-jsdoc": "^44.2.5",
"eslint-plugin-vue": "^9.8.0", "eslint-plugin-vue": "^9.8.0",
"fontaine": "^0.3.1",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^0.7.2",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"postcss": "^8.1.14", "postcss": "^8.1.14",
"prettier": "2.8.2", "prettier": "2.8.8",
"rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-analyzer": "^4.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vite": "^4.0.0", "vite": "^4.0.0",
"vite-plugin-compression2": "^0.8.2", "vite-plugin-compression2": "^0.9.1",
"vitest": "^0.28.5" "vitest": "^0.28.5"
}, },
"dependencies": { "dependencies": {
"@tinymce/tinymce-vue": "^4.0.7", "@tinymce/tinymce-vue": "^5.1.0",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vuepic/vue-datepicker": "^3.6.4", "@vuepic/vue-datepicker": "^3.6.4",
"dompurify": "^3.0.0", "dompurify": "^3.0.0",
@@ -39,9 +40,8 @@
"tinymce": "^6.3.1", "tinymce": "^6.3.1",
"vue": "^3.2.36", "vue": "^3.2.36",
"vue-dompurify-html": "^3.1.2", "vue-dompurify-html": "^3.1.2",
"vue-final-modal": "^3.4.11", "vue-final-modal": "^4.4.2",
"vue-loader": "^17.0.1", "vue-loader": "^17.0.1",
"vue-recaptcha-v3": "^2.0.1",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue3-easy-data-table": "^1.5.24" "vue3-easy-data-table": "^1.5.24"
} }

View File

@@ -19,6 +19,7 @@
</coverage> </coverage>
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="true"/>
<env name="BCRYPT_ROUNDS" value="4"/> <env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/> <env name="CACHE_DRIVER" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/> <env name="DB_CONNECTION" value="sqlite"/>

View File

@@ -13,6 +13,14 @@
RewriteEngine On RewriteEngine On
# Force HTTPS
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Support shortlinks
RewriteCond %{HTTP_HOST} ^(www\.)?stemmech\.com\.au$ [NC]
RewriteRule ^(.*)$ shortlink.php?code=$1 [L,QSA]
# Add www subdomain if missing # Add www subdomain if missing
RewriteCond %{HTTP_HOST} ^stemmechanics.com.au$ [NC] RewriteCond %{HTTP_HOST} ^stemmechanics.com.au$ [NC]
RewriteRule (.*) https://www.stemmechanics.com.au/$1 [R=301,L] RewriteRule (.*) https://www.stemmechanics.com.au/$1 [R=301,L]
@@ -27,9 +35,9 @@
RewriteRule ^ %1 [L,R=301] RewriteRule ^ %1 [L,R=301]
# Pass to media handler if the media request has query # Pass to media handler if the media request has query
RewriteCond %{REQUEST_FILENAME} -f #RewriteCond %{REQUEST_FILENAME} -f
RewriteCond %{QUERY_STRING} . #RewriteCond %{QUERY_STRING} .
RewriteRule ^uploads/(.+)\.(jpe?g|png)$ media.php?url=uploads/$1.$2 [NC,QSA,L] #RewriteRule ^uploads/(.+)\.(jpe?g|png)$ media.php?url=uploads/$1.$2 [NC,QSA,L]
# AddEncoding allows you to have certain browsers uncompress information on the fly. # AddEncoding allows you to have certain browsers uncompress information on the fly.
AddEncoding gzip .gz AddEncoding gzip .gz
@@ -52,4 +60,5 @@
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L] RewriteRule ^ index.php [L]
</IfModule> </IfModule>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

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