mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-28 10:24:45 +02:00
Compare commits
333 Commits
v1.0.0-bet
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
35ce1d34ab | ||
![]() |
17555a1d09 | ||
![]() |
837121b1fb | ||
![]() |
af9f2c653e | ||
![]() |
c22e7456b5 | ||
![]() |
97fb66f5d6 | ||
![]() |
51037c5c20 | ||
![]() |
23d13d9e83 | ||
![]() |
6c20426757 | ||
![]() |
1224210668 | ||
![]() |
258c97bf14 | ||
![]() |
7034c4d013 | ||
![]() |
e5cba893e4 | ||
![]() |
fd49f472c3 | ||
![]() |
c8556a4c56 | ||
![]() |
6de6306a19 | ||
![]() |
1f8a5cdd1d | ||
![]() |
30ae860d69 | ||
![]() |
f400e2db76 | ||
![]() |
1f7562563a | ||
![]() |
2296e41a8b | ||
![]() |
7971dc13fc | ||
![]() |
8406f4686c | ||
![]() |
67705b14b4 | ||
![]() |
bc115af5fd | ||
![]() |
da35703f75 | ||
![]() |
c54bfd714b | ||
![]() |
b83e3657d6 | ||
![]() |
e2c87a8206 | ||
![]() |
e38a736b61 | ||
![]() |
26e20453bf | ||
![]() |
292523d153 | ||
![]() |
85d625d118 | ||
![]() |
c8230771ec | ||
![]() |
79691ba663 | ||
![]() |
a6326f64fb | ||
![]() |
03745eb4be | ||
![]() |
c0fda71e20 | ||
![]() |
f2f1026a97 | ||
![]() |
e1eaf805ea | ||
![]() |
03ec20e3a0 | ||
![]() |
a5ffff8c8c | ||
![]() |
82ef6c1408 | ||
![]() |
2d581c7cbd | ||
![]() |
7f0266be5e | ||
![]() |
1ae9490b8f | ||
![]() |
a53b3fda10 | ||
![]() |
e9ddf80d10 | ||
![]() |
3f1e99f1df | ||
![]() |
435c615ff1 | ||
![]() |
3effd98013 | ||
![]() |
e354bc9be7 | ||
![]() |
14d351103c | ||
![]() |
92c23451af | ||
![]() |
2046fa453a | ||
![]() |
b39a8186ae | ||
![]() |
8ae3c88c91 | ||
![]() |
329a29f7da | ||
![]() |
98a2cab5ca | ||
![]() |
8407547574 | ||
![]() |
fccd7e5e75 | ||
![]() |
c0225b9e10 | ||
![]() |
544aaab960 | ||
![]() |
914e215bc0 | ||
![]() |
90fd73f6a4 | ||
![]() |
0037b4a1d4 | ||
![]() |
3deada57c6 | ||
![]() |
6427903f9f | ||
![]() |
b16e19b4fb | ||
![]() |
7e99d5cd8e | ||
![]() |
05b1a44a34 | ||
![]() |
058b613c98 | ||
![]() |
0e2ab4b711 | ||
![]() |
ee838316e6 | ||
![]() |
ffd94b8892 | ||
![]() |
a186900262 | ||
![]() |
bf14755287 | ||
![]() |
038504fbec | ||
![]() |
22a0a52f7b | ||
![]() |
862afaa0e9 | ||
![]() |
a4dd8cca4c | ||
![]() |
e67e0830eb | ||
![]() |
b444112085 | ||
![]() |
f23d4d6971 | ||
![]() |
2a3781f5a8 | ||
![]() |
cb245dc722 | ||
![]() |
3ffbf9e46a | ||
![]() |
8221c80ec2 | ||
![]() |
702a6bb750 | ||
![]() |
02d7ad04ad | ||
![]() |
7409f020ba | ||
![]() |
98d8510f11 | ||
![]() |
6c6d458445 | ||
![]() |
51fda2eaf4 | ||
![]() |
92fbd75772 | ||
![]() |
fa8ae0aea5 | ||
![]() |
377b3f170d | ||
![]() |
566e7c1b24 | ||
![]() |
b9d4773bd7 | ||
![]() |
49638e75e5 | ||
![]() |
80c404a48c | ||
![]() |
befe6be80b | ||
![]() |
3639d7ccec | ||
![]() |
20f271041a | ||
![]() |
c3b8b71f9c | ||
![]() |
c73d0544d9 | ||
![]() |
484a3b445a | ||
![]() |
c0fa8c1cd8 | ||
![]() |
e562a35057 | ||
![]() |
636279c6eb | ||
![]() |
ed88ce9ae3 | ||
![]() |
0cce716e2c | ||
![]() |
3639f0cb50 | ||
![]() |
9c3f47590c | ||
![]() |
630031e1c2 | ||
![]() |
2c00f90ba6 | ||
![]() |
875dca54f5 | ||
![]() |
a03b604f2d | ||
![]() |
8261184b57 | ||
![]() |
bca02ced86 | ||
![]() |
a768fadaea | ||
![]() |
7471347b55 | ||
![]() |
1457c4bd06 | ||
![]() |
8b943fa160 | ||
![]() |
5c5c9654b4 | ||
![]() |
dd20cb0f11 | ||
![]() |
88deb35dc8 | ||
![]() |
0f92632c06 | ||
![]() |
a85fc5c88e | ||
![]() |
8d7eff13fb | ||
![]() |
c39c29e50b | ||
![]() |
db3b16e609 | ||
![]() |
72b9c309d3 | ||
![]() |
68a6dc45cb | ||
![]() |
9a258efe53 | ||
![]() |
3310746107 | ||
![]() |
42706dba14 | ||
![]() |
ec6529ac4c | ||
![]() |
bced93c5be | ||
![]() |
cb1c953540 | ||
![]() |
c689f6860b | ||
![]() |
a73404c1b4 | ||
![]() |
61cbe5465f | ||
![]() |
5bea1ea80a | ||
![]() |
b69136d7a4 | ||
![]() |
a8c3082b79 | ||
![]() |
a47ad071c9 | ||
![]() |
ab953b2f4d | ||
![]() |
03d6c88f65 | ||
![]() |
b4eab02254 | ||
![]() |
23f39acd4e | ||
![]() |
82b0aff105 | ||
![]() |
adca50a372 | ||
![]() |
c5230efad6 | ||
![]() |
e5d9d53aa3 | ||
![]() |
29f3defc73 | ||
![]() |
2dbb9a5f9b | ||
![]() |
a05e330b19 | ||
![]() |
4a7951995e | ||
![]() |
3d29243cf0 | ||
![]() |
c52439132d | ||
![]() |
517f17cbcc | ||
![]() |
f8d119b458 | ||
![]() |
fbeb747fc3 | ||
![]() |
f563128237 | ||
![]() |
f2f3ee548f | ||
![]() |
0b3dce132f | ||
![]() |
5bf23b972d | ||
![]() |
22d02c0df5 | ||
![]() |
253abf65b1 | ||
![]() |
d452e3d2f2 | ||
![]() |
0051370f24 | ||
![]() |
4e85180b3d | ||
![]() |
9f4a3b1c0d | ||
![]() |
45db06a1bd | ||
![]() |
3e26a1cf09 | ||
![]() |
44111696df | ||
![]() |
e04abcbcf9 | ||
![]() |
ea5914f362 | ||
![]() |
98c36c4cc3 | ||
![]() |
6bc55b1039 | ||
![]() |
11b153d23c | ||
![]() |
998ad2ee31 | ||
![]() |
7f0c7da37f | ||
![]() |
e93d122a27 | ||
![]() |
9aaf6b3798 | ||
![]() |
fd6e7eb314 | ||
![]() |
4e694b50ca | ||
![]() |
3a24edfe1d | ||
![]() |
0179ade557 | ||
![]() |
05d74232af | ||
![]() |
a2b2e373be | ||
![]() |
0a17e78f33 | ||
![]() |
c3a65aed07 | ||
![]() |
d438e29154 | ||
![]() |
1fdc428f3e | ||
![]() |
a9e4495c91 | ||
![]() |
98ddb65509 | ||
![]() |
6caa741798 | ||
![]() |
5512c10ee1 | ||
![]() |
5331c5abfa | ||
![]() |
36a38ab947 | ||
![]() |
da195fd2fe | ||
![]() |
82409f2fba | ||
![]() |
839be53231 | ||
![]() |
d79d461e7c | ||
![]() |
d8e8240756 | ||
![]() |
0b84b0c08c | ||
![]() |
e2045e334f | ||
![]() |
5e2d106bb9 | ||
![]() |
40c138f086 | ||
![]() |
ab543a399b | ||
![]() |
0308045738 | ||
![]() |
cd9cbf20ce | ||
![]() |
e1308cb04d | ||
![]() |
2d937229fb | ||
![]() |
3d764a89f7 | ||
![]() |
2f56ca5ed5 | ||
![]() |
fe8e6fcfda | ||
![]() |
1e7a901371 | ||
![]() |
d53820bbdc | ||
![]() |
d03366cf3d | ||
![]() |
7d68da41f4 | ||
![]() |
599d53b4f2 | ||
![]() |
f0f04fd86a | ||
![]() |
324fc4b7d5 | ||
![]() |
5be4e22a0c | ||
![]() |
75aae3e45b | ||
![]() |
c1704eef3b | ||
![]() |
09abec6ee6 | ||
![]() |
206cc76a8b | ||
![]() |
b355830db4 | ||
![]() |
09375df8a7 | ||
![]() |
96ec2eb3c2 | ||
![]() |
b464bb4d25 | ||
![]() |
c561035c75 | ||
![]() |
48d1ef5d26 | ||
![]() |
1f6b659546 | ||
![]() |
8f47ccfbf7 | ||
![]() |
35d25d216e | ||
![]() |
a6963ad802 | ||
![]() |
d48cf6b722 | ||
![]() |
cba4cf11aa | ||
![]() |
96c09acc52 | ||
![]() |
7f697017a7 | ||
![]() |
f8ad720f52 | ||
![]() |
513117cc42 | ||
![]() |
5797b790fd | ||
![]() |
9ec2f6eae1 | ||
![]() |
77bf70b063 | ||
![]() |
b8c1b68328 | ||
![]() |
431c1977e3 | ||
![]() |
f8ad9a1805 | ||
![]() |
635cc6a029 | ||
![]() |
20125dbc6f | ||
![]() |
d5b8a4c501 | ||
![]() |
dde5305b3f | ||
![]() |
e352754e6f | ||
![]() |
7cde90a39a | ||
![]() |
3202a59b07 | ||
![]() |
71f3abe464 | ||
![]() |
401026efa1 | ||
![]() |
654143addc | ||
![]() |
37f9725f27 | ||
![]() |
98c915490d | ||
![]() |
6fb54e32f1 | ||
![]() |
fef19b9fdd | ||
![]() |
6a4963200c | ||
![]() |
37ba62410f | ||
![]() |
262e2fd09a | ||
![]() |
9e8b9cd599 | ||
![]() |
3411e5e65c | ||
![]() |
7e6769c96e | ||
![]() |
03eaddb126 | ||
![]() |
61bdf0dcd7 | ||
![]() |
cbacc18e56 | ||
![]() |
ad1a9cd33f | ||
![]() |
02c4eb19f0 | ||
![]() |
3a25d0f976 | ||
![]() |
634b8dec55 | ||
![]() |
43d0b78742 | ||
![]() |
6b77e69e43 | ||
![]() |
efbf4df2a2 | ||
![]() |
4ec9171017 | ||
![]() |
885e03ee06 | ||
![]() |
7c6b3a03db | ||
![]() |
fe43539ea7 | ||
![]() |
e145fcdc56 | ||
![]() |
8078f2ca4e | ||
![]() |
d1007ad2fe | ||
![]() |
7f3b1fd758 | ||
![]() |
d088e79e5e | ||
![]() |
9cfd87090f | ||
![]() |
a7a7c5ba4d | ||
![]() |
b14e8fd724 | ||
![]() |
c93a836ad8 | ||
![]() |
6fcf4173d3 | ||
![]() |
7449b82f41 | ||
![]() |
af4ac1db92 | ||
![]() |
6707d1ccf6 | ||
![]() |
b197e73173 | ||
![]() |
e5418491c8 | ||
![]() |
98ebc75965 | ||
![]() |
121ebe6017 | ||
![]() |
fc27b24783 | ||
![]() |
8049ef462e | ||
![]() |
17bb23b5b8 | ||
![]() |
8926f9712f | ||
![]() |
e4849d89d7 | ||
![]() |
af11888b82 | ||
![]() |
1845f2955f | ||
![]() |
a2b315ba74 | ||
![]() |
76c3632d14 | ||
![]() |
4facaecea0 | ||
![]() |
a55a2cce6e | ||
![]() |
448fe41e78 | ||
![]() |
7f37b3b099 | ||
![]() |
ef54d52866 | ||
![]() |
7bd66c3d85 | ||
![]() |
74efc6e8c1 | ||
![]() |
a7b767ae78 | ||
![]() |
a3ecf3994b | ||
![]() |
158fa24fff | ||
![]() |
e5069e754d | ||
![]() |
cdd46de274 | ||
![]() |
ff5812e87b | ||
![]() |
20ce0ca8e6 | ||
![]() |
66ec86694f | ||
![]() |
295134fb6c | ||
![]() |
ae445840f7 | ||
![]() |
77fd54fdc2 | ||
![]() |
18fe4f1123 | ||
![]() |
2525af8f02 |
@ -1,10 +1,29 @@
|
||||
**.DS_Store
|
||||
.env
|
||||
.devcontainer
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.git
|
||||
node_modules
|
||||
vendor
|
||||
.github
|
||||
**.gitignore
|
||||
.php-cs-fixer.dist.php
|
||||
.prettierrc.json
|
||||
.vscode
|
||||
Dockerfile
|
||||
bounties.md
|
||||
compose.yml
|
||||
contributing.md
|
||||
contributor_license_agreement.md
|
||||
database/database.sqlite
|
||||
docker/README.md
|
||||
node_modules
|
||||
phpstan.neon
|
||||
phpunit.xml
|
||||
readme.md
|
||||
storage/debugbar/*.json
|
||||
storage/logs/*.log
|
||||
storage/framework/cache/data/*
|
||||
storage/framework/sessions/*
|
||||
storage/framework/testing
|
||||
storage/framework/views/*.php
|
||||
storage/logs/*.log
|
||||
vendor
|
||||
|
@ -3,5 +3,4 @@ APP_DEBUG=false
|
||||
APP_KEY=
|
||||
APP_URL=http://panel.test
|
||||
APP_INSTALLED=false
|
||||
APP_TIMEZONE=UTC
|
||||
APP_LOCALE=en
|
||||
|
@ -1,6 +0,0 @@
|
||||
public
|
||||
node_modules
|
||||
resources/views
|
||||
babel.config.js
|
||||
tailwind.config.js
|
||||
webpack.config.js
|
52
.eslintrc.js
52
.eslintrc.js
@ -1,52 +0,0 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: './',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
pragma: 'React',
|
||||
version: 'detect',
|
||||
},
|
||||
linkComponents: [
|
||||
{ name: 'Link', linkAttribute: 'to' },
|
||||
{ name: 'NavLink', linkAttribute: 'to' },
|
||||
],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
},
|
||||
plugins: ['react', 'react-hooks', 'prettier', '@typescript-eslint'],
|
||||
extends: [
|
||||
// 'standard',
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:jest-dom/recommended',
|
||||
],
|
||||
rules: {
|
||||
eqeqeq: 'error',
|
||||
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
|
||||
// TypeScript can infer this significantly better than eslint ever can.
|
||||
'react/prop-types': 0,
|
||||
'react/display-name': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
// 'react/no-unknown-property': ['error', { ignore: ['css'] }],
|
||||
// This setup is required to avoid a spam of errors when running eslint about React being
|
||||
// used before it is defined.
|
||||
//
|
||||
// @see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
|
||||
'no-use-before-define': 0,
|
||||
'@typescript-eslint/no-use-before-define': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }],
|
||||
},
|
||||
};
|
76
.github/docker/README.md
vendored
76
.github/docker/README.md
vendored
@ -1,76 +0,0 @@
|
||||
# Pelican Panel - Docker Image
|
||||
This is a ready to use docker image for the panel.
|
||||
|
||||
## Requirements
|
||||
This docker image requires some additional software to function. The software can either be provided in other containers (see the [docker-compose.yml](https://github.com/pelican-dev/panel/blob/develop/docker-compose.example.yml) as an example) or as existing instances.
|
||||
|
||||
A mysql database is required. We recommend the stock [MariaDB Image](https://hub.docker.com/_/mariadb/) image if you prefer to run it in a docker container. As a non-containerized option we recommend mariadb.
|
||||
|
||||
A caching software is required as well. We recommend the stock [Redis Image](https://hub.docker.com/_/redis/) image. You can choose any of the [supported options](#cache-drivers).
|
||||
|
||||
You can provide additional settings using a custom `.env` file or by setting the appropriate environment variables in the docker-compose file.
|
||||
|
||||
## Setup
|
||||
|
||||
Start the docker container and the required dependencies (either provide existing ones or start containers as well, see the [docker-compose.yml](https://github.com/pelican-dev/panel/blob/develop/docker-compose.example.yml) file as an example.
|
||||
|
||||
After the startup is complete you'll need to create a user.
|
||||
If you are running the docker container without docker-compose, use:
|
||||
```
|
||||
docker exec -it <container id> php artisan p:user:make
|
||||
```
|
||||
If you are using docker compose use
|
||||
```
|
||||
docker-compose exec panel php artisan p:user:make
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
There are multiple environment variables to configure the panel when not providing your own `.env` file, see the following table for details on each available option.
|
||||
|
||||
Note: If your `APP_URL` starts with `https://` you need to provide an `LE_EMAIL` as well so Certificates can be generated.
|
||||
|
||||
| Variable | Description | Required |
|
||||
|-------------------| ------------------------------------------------------------------------------ | -------- |
|
||||
| `APP_URL` | The URL the panel will be reachable with (including protocol) | yes |
|
||||
| `APP_TIMEZONE` | The timezone to use for the panel | yes |
|
||||
| `LE_EMAIL` | The email used for letsencrypt certificate generation | yes |
|
||||
| `DB_HOST` | The host of the mysql instance | yes |
|
||||
| `DB_PORT` | The port of the mysql instance | yes |
|
||||
| `DB_DATABASE` | The name of the mysql database | yes |
|
||||
| `DB_USERNAME` | The mysql user | yes |
|
||||
| `DB_PASSWORD` | The mysql password for the specified user | yes |
|
||||
| `CACHE_STORE` | The cache driver (see [Cache drivers](#cache-drivers) for detais) | yes |
|
||||
| `SESSION_DRIVER` | | yes |
|
||||
| `QUEUE_DRIVER` | | yes |
|
||||
| `REDIS_HOST` | The hostname or IP address of the redis database | yes |
|
||||
| `REDIS_PASSWORD` | The password used to secure the redis database | maybe |
|
||||
| `REDIS_PORT` | The port the redis database is using on the host | maybe |
|
||||
| `MAIL_DRIVER` | The email driver (see [Mail drivers](#mail-drivers) for details) | yes |
|
||||
| `MAIL_FROM` | The email that should be used as the sender email | yes |
|
||||
| `MAIL_HOST` | The host of your mail driver instance | maybe |
|
||||
| `MAIL_PORT` | The port of your mail driver instance | maybe |
|
||||
| `MAIL_USERNAME` | The username for your mail driver | maybe |
|
||||
| `MAIL_PASSWORD` | The password for your mail driver | maybe |
|
||||
|
||||
|
||||
### Cache drivers
|
||||
You can choose between different cache drivers depending on what you prefer.
|
||||
We recommend redis when using docker as it can be started in a container easily.
|
||||
|
||||
| Driver | Description | Required variables |
|
||||
| -------- | ------------------------------------ | ------------------------------------------------------ |
|
||||
| redis | host where redis is running | `REDIS_HOST` |
|
||||
| redis | port redis is running on | `REDIS_PORT` |
|
||||
| redis | redis database password | `REDIS_PASSWORD` |
|
||||
|
||||
### Mail drivers
|
||||
You can choose between different mail drivers according to your needs.
|
||||
Every driver requires `MAIL_FROM` to be set.
|
||||
|
||||
| Driver | Description | Required variables |
|
||||
| -------- | ------------------------------------ | ------------------------------------------------------------- |
|
||||
| mail | uses the installed php mail | |
|
||||
| mandrill | [Mandrill](http://www.mandrill.com/) | `MAIL_USERNAME` |
|
||||
| postmark | [Postmark](https://postmarkapp.com/) | `MAIL_USERNAME` |
|
||||
| mailgun | [Mailgun](https://www.mailgun.com/) | `MAIL_USERNAME`, `MAIL_HOST` |
|
||||
| smtp | Any SMTP server can be configured | `MAIL_USERNAME`, `MAIL_HOST`, `MAIL_PASSWORD`, `MAIL_PORT` |
|
75
.github/docker/default.conf
vendored
75
.github/docker/default.conf
vendored
@ -1,75 +0,0 @@
|
||||
# If using Ubuntu this file should be placed in:
|
||||
# /etc/nginx/sites-available/
|
||||
#
|
||||
# If using CentOS this file should be placed in:
|
||||
# /etc/nginx/conf.d/
|
||||
#
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Pterodactyl®
|
||||
# Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /app/public;
|
||||
index index.html index.htm index.php;
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
access_log off;
|
||||
error_log /var/log/nginx/panel.app-error.log error;
|
||||
|
||||
# allow larger file uploads and longer script runtimes
|
||||
client_max_body_size 100m;
|
||||
client_body_timeout 120s;
|
||||
|
||||
sendfile off;
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
# the fastcgi_pass path needs to be changed accordingly when using CentOS
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param HTTP_PROXY "";
|
||||
fastcgi_intercept_errors off;
|
||||
fastcgi_buffer_size 16k;
|
||||
fastcgi_buffers 4 16k;
|
||||
fastcgi_connect_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_read_timeout 300;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
70
.github/docker/default_ssl.conf
vendored
70
.github/docker/default_ssl.conf
vendored
@ -1,70 +0,0 @@
|
||||
# If using Ubuntu this file should be placed in:
|
||||
# /etc/nginx/sites-available/
|
||||
#
|
||||
server {
|
||||
listen 80;
|
||||
server_name <domain>;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name <domain>;
|
||||
|
||||
root /app/public;
|
||||
index index.php;
|
||||
|
||||
access_log /var/log/nginx/panel.app-access.log;
|
||||
error_log /var/log/nginx/panel.app-error.log error;
|
||||
|
||||
# allow larger file uploads and longer script runtimes
|
||||
client_max_body_size 100m;
|
||||
client_body_timeout 120s;
|
||||
|
||||
sendfile off;
|
||||
|
||||
# strengthen ssl security
|
||||
ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
|
||||
|
||||
# See the link below for more SSL information:
|
||||
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||
#
|
||||
# ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||
|
||||
# Add headers to serve security related headers
|
||||
add_header Strict-Transport-Security "max-age=15768000; preload;";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Robots-Tag none;
|
||||
add_header Content-Security-Policy "frame-ancestors 'self'";
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param HTTP_PROXY "";
|
||||
fastcgi_intercept_errors off;
|
||||
fastcgi_buffer_size 16k;
|
||||
fastcgi_buffers 4 16k;
|
||||
fastcgi_connect_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_read_timeout 300;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
16
.github/docker/www.conf
vendored
16
.github/docker/www.conf
vendored
@ -1,16 +0,0 @@
|
||||
[www]
|
||||
|
||||
user = nginx
|
||||
group = nginx
|
||||
|
||||
listen = 127.0.0.1:9000
|
||||
listen.owner = nginx
|
||||
listen.group = nginx
|
||||
listen.mode = 0750
|
||||
|
||||
pm = ondemand
|
||||
pm.max_children = 9
|
||||
pm.process_idle_timeout = 10s
|
||||
pm.max_requests = 200
|
||||
|
||||
clear_env = no
|
19
.github/workflows/build.yaml
vendored
19
.github/workflows/build.yaml
vendored
@ -3,10 +3,8 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
ui:
|
||||
@ -20,14 +18,25 @@ jobs:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts --no-dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install JS dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: yarn build:production
|
||||
run: yarn build
|
||||
|
100
.github/workflows/ci.yaml
vendored
100
.github/workflows/ci.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
php: [8.2, 8.3, 8.4]
|
||||
database: ["mysql:8"]
|
||||
services:
|
||||
database:
|
||||
@ -66,16 +66,16 @@ jobs:
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/phpunit tests/Unit
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/phpunit tests/Integration
|
||||
run: vendor/bin/pest tests/Integration
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
@ -86,7 +86,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
php: [8.2, 8.3, 8.4]
|
||||
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
|
||||
services:
|
||||
database:
|
||||
@ -139,16 +139,16 @@ jobs:
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/phpunit tests/Unit
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/phpunit tests/Integration
|
||||
run: vendor/bin/pest tests/Integration
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
@ -159,7 +159,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
php: [8.2, 8.3, 8.4]
|
||||
env:
|
||||
APP_ENV: testing
|
||||
APP_DEBUG: "false"
|
||||
@ -200,16 +200,92 @@ jobs:
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: Create SQLite file
|
||||
run: touch database/testing.sqlite
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/phpunit tests/Unit
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/phpunit tests/Integration
|
||||
run: vendor/bin/pest tests/Integration
|
||||
|
||||
postgresql:
|
||||
name: PostgreSQL
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4]
|
||||
database: ["postgres:14"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
env:
|
||||
POSTGRES_DB: testing
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
APP_ENV: testing
|
||||
APP_DEBUG: "false"
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
DB_CONNECTION: pgsql
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: postgres
|
||||
DB_PASSWORD: postgres
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration
|
||||
|
107
.github/workflows/docker-publish.yml
vendored
107
.github/workflows/docker-publish.yml
vendored
@ -1,6 +1,5 @@
|
||||
name: Docker
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@ -14,18 +13,73 @@ env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
runs-on: ubuntu-latest
|
||||
build-php-base:
|
||||
name: Build PHP base image on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
arch: amd64
|
||||
platform: linux/amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
platform: linux/arm64
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build the base PHP image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.base
|
||||
push: false
|
||||
load: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
tags: base-php:${{ matrix.arch }}
|
||||
cache-from: type=gha,scope=base-php${{ matrix.arch }}
|
||||
cache-to: type=gha,scope=base-php${{ matrix.arch }}
|
||||
|
||||
- name: Export image to file
|
||||
run: docker save -o base-php-${{ matrix.arch }}.tar base-php:${{ matrix.arch }}
|
||||
|
||||
- name: Push the docker build to the artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: base-php-${{ matrix.arch }}.tar
|
||||
path: base-php-${{ matrix.arch }}.tar
|
||||
retention-days: 7
|
||||
|
||||
|
||||
build-and-push:
|
||||
name: Build and Push ubuntu-24.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-php-base
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Start a temp local registry because workflow can not pull from localy loaded images
|
||||
services:
|
||||
registry:
|
||||
image: registry:2
|
||||
ports:
|
||||
- 5000:5000
|
||||
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
|
||||
if: "!contains(github.ref, 'main') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
@ -38,11 +92,14 @@ jobs:
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
|
||||
- name: Setup QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
|
||||
# We Need to start it in host mode else it can't acces the local registry on port 5000
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@ -57,30 +114,52 @@ jobs:
|
||||
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Download the base PHP image AMD64
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: base-php-amd64.tar
|
||||
|
||||
# Download the base PHP image ARM64
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: base-php-arm64.tar
|
||||
|
||||
- name: Load base images into local registry
|
||||
run: |
|
||||
docker load -i base-php-amd64.tar
|
||||
docker load -i base-php-arm64.tar
|
||||
docker tag base-php:amd64 localhost:5000/base-php:amd64
|
||||
docker tag base-php:arm64 localhost:5000/base-php:arm64
|
||||
docker push localhost:5000/base-php:amd64
|
||||
docker push localhost:5000/base-php:arm64
|
||||
rm base-php-arm64.tar base-php-amd64.tar
|
||||
|
||||
- name: Build and Push (tag)
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
if: "github.event_name == 'release' && github.event.action == 'published'"
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: 'linux/amd64,linux/arm64'
|
||||
build-args: |
|
||||
VERSION=${{ steps.build_info.outputs.version_tag }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
|
||||
cache-from: type=gha,scope=tagged${{ matrix.os }}
|
||||
cache-to: type=gha,scope=tagged${{ matrix.os }},mode=max
|
||||
|
||||
- name: Build and Push (main)
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
if: "github.event_name == 'push' && contains(github.ref, 'main')"
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: 'linux/amd64,linux/arm64'
|
||||
build-args: |
|
||||
VERSION=dev-${{ steps.build_info.outputs.short_sha }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=gha,scope=${{ matrix.os }}
|
||||
cache-to: type=gha,scope=${{ matrix.os }},mode=max
|
||||
|
25
.github/workflows/lint.yaml
vendored
25
.github/workflows/lint.yaml
vendored
@ -25,21 +25,38 @@ jobs:
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts
|
||||
|
||||
- name: Pint
|
||||
run: vendor/bin/pint --test
|
||||
phpstan:
|
||||
name: PHPStan
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [ 8.2, 8.3, 8.4 ]
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-${{ matrix.php }}-
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.3"
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
@ -48,7 +65,7 @@ jobs:
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: PHPStan
|
||||
run: vendor/bin/phpstan --memory-limit=-1
|
||||
run: vendor/bin/phpstan --memory-limit=-1 --error-format=github
|
||||
|
21
.github/workflows/release.yaml
vendored
21
.github/workflows/release.yaml
vendored
@ -11,22 +11,33 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts --no-dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install JS dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: yarn build:production
|
||||
run: yarn build
|
||||
|
||||
- name: Create release branch and bump version
|
||||
env:
|
||||
@ -44,8 +55,8 @@ jobs:
|
||||
|
||||
- name: Create release archive
|
||||
run: |
|
||||
rm -rf node_modules tests CODE_OF_CONDUCT.md CONTRIBUTING.md flake.lock flake.nix phpunit.xml shell.nix
|
||||
tar -czf panel.tar.gz * .editorconfig .env.example .eslintignore .eslintrc.js .gitignore .prettierrc.json
|
||||
rm -rf node_modules vendor tests CODE_OF_CONDUCT.md CONTRIBUTING.md phpunit.xml shell.nix
|
||||
tar -czf panel.tar.gz * .env.example
|
||||
|
||||
- name: Create checksum
|
||||
run: |
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,9 +1,9 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/storage/clockwork/*
|
||||
/vendor
|
||||
*.DS_Store*
|
||||
@ -19,10 +19,11 @@ npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.vscode
|
||||
|
||||
public/assets/manifest.json
|
||||
/database/*.sqlite
|
||||
/database/*.sqlite*
|
||||
filament-monaco-editor/
|
||||
_ide_helper*
|
||||
/.phpstorm.meta.php
|
||||
|
@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Finder;
|
||||
|
||||
$finder = (new Finder())
|
||||
->in(__DIR__)
|
||||
->exclude([
|
||||
'vendor',
|
||||
'node_modules',
|
||||
'storage',
|
||||
'bootstrap/cache',
|
||||
])
|
||||
->notName(['_ide_helper*']);
|
||||
|
||||
return (new Config())
|
||||
->setRiskyAllowed(true)
|
||||
->setFinder($finder)
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
'@PSR1' => true,
|
||||
'@PSR2' => true,
|
||||
'@PSR12' => true,
|
||||
'align_multiline_comment' => ['comment_type' => 'phpdocs_like'],
|
||||
'combine_consecutive_unsets' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'heredoc_to_nowdoc' => true,
|
||||
'no_alias_functions' => true,
|
||||
'no_unreachable_default_argument_value' => true,
|
||||
'no_useless_return' => true,
|
||||
'ordered_imports' => [
|
||||
'sort_algorithm' => 'length',
|
||||
],
|
||||
'phpdoc_align' => [
|
||||
'align' => 'left',
|
||||
'tags' => [
|
||||
'param',
|
||||
'property',
|
||||
'return',
|
||||
'throws',
|
||||
'type',
|
||||
'var',
|
||||
],
|
||||
],
|
||||
'random_api_migration' => true,
|
||||
'ternary_to_null_coalescing' => true,
|
||||
'yoda_style' => [
|
||||
'equal' => false,
|
||||
'identical' => false,
|
||||
'less_and_greater' => false,
|
||||
],
|
||||
]);
|
115
Dockerfile
115
Dockerfile
@ -1,52 +1,99 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.13-labs
|
||||
# Pelican Production Dockerfile
|
||||
|
||||
FROM node:20-alpine AS yarn
|
||||
#FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
##
|
||||
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
|
||||
##
|
||||
|
||||
# ================================
|
||||
# Stage 1-1: Composer Install
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS composer
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . ./
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Copy bare minimum to install Composer dependencies
|
||||
COPY composer.json composer.lock ./
|
||||
|
||||
RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
|
||||
|
||||
# ================================
|
||||
# Stage 1-2: Yarn Install
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy bare minimum to install Yarn dependencies
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN yarn config set network-timeout 300000 \
|
||||
&& yarn install --frozen-lockfile \
|
||||
&& yarn run build:production
|
||||
&& yarn install --frozen-lockfile
|
||||
|
||||
FROM php:8.3-fpm-alpine
|
||||
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
# ================================
|
||||
# Stage 2-1: Composer Optimize
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
|
||||
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
# Copy full code to optimize autoload
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
|
||||
RUN composer dump-autoload --optimize
|
||||
|
||||
# ================================
|
||||
# Stage 2-2: Build Frontend Assets
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy full code
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
COPY --from=composer /build .
|
||||
|
||||
RUN yarn run build
|
||||
|
||||
# ================================
|
||||
# Stage 5: Build Final Application Image
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS final
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install dependencies
|
||||
# Install additional required libraries
|
||||
RUN apk update && apk add --no-cache \
|
||||
libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev icu-dev \
|
||||
zip unzip curl \
|
||||
caddy ca-certificates supervisor \
|
||||
&& docker-php-ext-install bcmath gd intl zip opcache pcntl posix pdo_mysql
|
||||
caddy ca-certificates supervisor supercronic
|
||||
|
||||
# Copy the Caddyfile to the container
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
|
||||
|
||||
# Copy the application code to the container
|
||||
COPY . .
|
||||
# Set permissions
|
||||
# First ensure all files are owned by root and restrict www-data to read access
|
||||
RUN chown root:www-data ./ \
|
||||
&& chmod 750 ./ \
|
||||
# Files should not have execute set, but directories need it
|
||||
&& find ./ -type d -exec chmod 750 {} \; \
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
|
||||
# Symlinks for env, database, and avatars
|
||||
&& ln -s /pelican-data/.env ./.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
|
||||
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
|
||||
|
||||
COPY --from=yarn /build/public/assets ./public/assets
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||
# Add Laravel scheduler to crontab
|
||||
COPY docker/crontab /etc/supercronic/crontab
|
||||
|
||||
RUN touch .env
|
||||
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Set file permissions
|
||||
RUN chmod -R 755 storage bootstrap/cache \
|
||||
&& chown -R www-data:www-data ./
|
||||
|
||||
# Add scheduler to cron
|
||||
RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -
|
||||
|
||||
## supervisord config and log dir
|
||||
RUN cp .github/docker/supervisord.conf /etc/supervisord.conf && \
|
||||
mkdir /var/log/supervisord/
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
@ -55,5 +102,7 @@ EXPOSE 80 443
|
||||
|
||||
VOLUME /pelican-data
|
||||
|
||||
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
|
||||
USER www-data
|
||||
|
||||
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
||||
|
10
Dockerfile.base
Normal file
10
Dockerfile.base
Normal file
@ -0,0 +1,10 @@
|
||||
# ================================
|
||||
# Stage 0: Build PHP Base Image
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
|
||||
|
||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
|
||||
|
||||
RUN rm /usr/local/bin/install-php-extensions
|
112
Dockerfile.dev
Normal file
112
Dockerfile.dev
Normal file
@ -0,0 +1,112 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.13-labs
|
||||
# Pelican Development Dockerfile
|
||||
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
|
||||
|
||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
|
||||
|
||||
RUN rm /usr/local/bin/install-php-extensions
|
||||
|
||||
# ================================
|
||||
# Stage 1-1: Composer Install
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH base AS composer
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Copy bare minimum to install Composer dependencies
|
||||
COPY composer.json composer.lock ./
|
||||
|
||||
RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
|
||||
|
||||
# ================================
|
||||
# Stage 1-2: Yarn Install
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy bare minimum to install Yarn dependencies
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN yarn config set network-timeout 300000 \
|
||||
&& yarn install --frozen-lockfile
|
||||
|
||||
# ================================
|
||||
# Stage 2-1: Composer Optimize
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
|
||||
|
||||
# Copy full code to optimize autoload
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
|
||||
RUN composer dump-autoload --optimize
|
||||
|
||||
# ================================
|
||||
# Stage 2-2: Build Frontend Assets
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy full code
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
COPY --from=composer /build .
|
||||
|
||||
RUN yarn run build
|
||||
|
||||
# ================================
|
||||
# Stage 5: Build Final Application Image
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH base AS final
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install additional required libraries
|
||||
RUN apk update && apk add --no-cache \
|
||||
caddy ca-certificates supervisor supercronic
|
||||
|
||||
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
|
||||
|
||||
# Set permissions
|
||||
# First ensure all files are owned by root and restrict www-data to read access
|
||||
RUN chown root:www-data ./ \
|
||||
&& chmod 750 ./ \
|
||||
# Files should not have execute set, but directories need it
|
||||
&& find ./ -type d -exec chmod 750 {} \; \
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
|
||||
# Symlinks for env, database, and avatars
|
||||
&& ln -s /pelican-data/.env ./.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
|
||||
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
|
||||
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||
# Add Laravel scheduler to crontab
|
||||
COPY docker/crontab /etc/supercronic/crontab
|
||||
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
VOLUME /pelican-data
|
||||
|
||||
USER www-data
|
||||
|
||||
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
58
app/Checks/CacheCheck.php
Normal file
58
app/Checks/CacheCheck.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Checks;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Health\Checks\Check;
|
||||
use Spatie\Health\Checks\Result;
|
||||
|
||||
class CacheCheck extends Check
|
||||
{
|
||||
protected ?string $driver = null;
|
||||
|
||||
public function driver(string $driver): self
|
||||
{
|
||||
$this->driver = $driver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run(): Result
|
||||
{
|
||||
$driver = $this->driver ?? $this->defaultDriver();
|
||||
|
||||
$result = Result::make()->meta([
|
||||
'driver' => $driver,
|
||||
]);
|
||||
|
||||
try {
|
||||
return $this->canWriteValuesToCache($driver)
|
||||
? $result->ok(trans('admin/health.results.cache.ok'))
|
||||
: $result->failed(trans('admin/health.results.cache.failed_retrieve'));
|
||||
} catch (Exception $exception) {
|
||||
return $result->failed(trans('admin/health.results.cache.failed', ['error' => $exception->getMessage()]));
|
||||
}
|
||||
}
|
||||
|
||||
protected function defaultDriver(): ?string
|
||||
{
|
||||
return config('cache.default', 'file');
|
||||
}
|
||||
|
||||
protected function canWriteValuesToCache(?string $driver): bool
|
||||
{
|
||||
$expectedValue = Str::random(5);
|
||||
|
||||
$cacheName = "laravel-health:check-{$expectedValue}";
|
||||
|
||||
Cache::driver($driver)->put($cacheName, $expectedValue, 10);
|
||||
|
||||
$actualValue = Cache::driver($driver)->get($cacheName);
|
||||
|
||||
Cache::driver($driver)->forget($cacheName);
|
||||
|
||||
return $actualValue === $expectedValue;
|
||||
}
|
||||
}
|
42
app/Checks/DatabaseCheck.php
Normal file
42
app/Checks/DatabaseCheck.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Checks;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Health\Checks\Check;
|
||||
use Spatie\Health\Checks\Result;
|
||||
|
||||
class DatabaseCheck extends Check
|
||||
{
|
||||
protected ?string $connectionName = null;
|
||||
|
||||
public function connectionName(string $connectionName): self
|
||||
{
|
||||
$this->connectionName = $connectionName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run(): Result
|
||||
{
|
||||
$connectionName = $this->connectionName ?? $this->getDefaultConnectionName();
|
||||
|
||||
$result = Result::make()->meta([
|
||||
'connection_name' => $connectionName,
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::connection($connectionName)->getPdo();
|
||||
|
||||
return $result->ok(trans('admin/health.results.database.ok'));
|
||||
} catch (Exception $exception) {
|
||||
return $result->failed(trans('admin/health.results.database.failed', ['error' => $exception->getMessage()]));
|
||||
}
|
||||
}
|
||||
|
||||
protected function getDefaultConnectionName(): string
|
||||
{
|
||||
return config('database.default');
|
||||
}
|
||||
}
|
44
app/Checks/DebugModeCheck.php
Normal file
44
app/Checks/DebugModeCheck.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Checks;
|
||||
|
||||
use Spatie\Health\Checks\Check;
|
||||
use Spatie\Health\Checks\Result;
|
||||
|
||||
use function config;
|
||||
|
||||
class DebugModeCheck extends Check
|
||||
{
|
||||
protected bool $expected = false;
|
||||
|
||||
public function expectedToBe(bool $bool): self
|
||||
{
|
||||
$this->expected = $bool;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run(): Result
|
||||
{
|
||||
$actual = config('app.debug');
|
||||
|
||||
$result = Result::make()
|
||||
->meta([
|
||||
'actual' => $actual,
|
||||
'expected' => $this->expected,
|
||||
])
|
||||
->shortSummary($this->convertToWord($actual));
|
||||
|
||||
return $this->expected === $actual
|
||||
? $result->ok()
|
||||
: $result->failed(trans('admin/health.results.debugmode.failed', [
|
||||
'actual' => $this->convertToWord($actual),
|
||||
'expected' => $this->convertToWord($this->expected),
|
||||
]));
|
||||
}
|
||||
|
||||
protected function convertToWord(bool $boolean): string
|
||||
{
|
||||
return $boolean ? 'true' : 'false';
|
||||
}
|
||||
}
|
38
app/Checks/EnvironmentCheck.php
Normal file
38
app/Checks/EnvironmentCheck.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Checks;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Spatie\Health\Checks\Check;
|
||||
use Spatie\Health\Checks\Result;
|
||||
|
||||
class EnvironmentCheck extends Check
|
||||
{
|
||||
protected string $expectedEnvironment = 'production';
|
||||
|
||||
public function expectEnvironment(string $expectedEnvironment): self
|
||||
{
|
||||
$this->expectedEnvironment = $expectedEnvironment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run(): Result
|
||||
{
|
||||
$actualEnvironment = (string) App::environment();
|
||||
|
||||
$result = Result::make()
|
||||
->meta([
|
||||
'actual' => $actualEnvironment,
|
||||
'expected' => $this->expectedEnvironment,
|
||||
])
|
||||
->shortSummary($actualEnvironment);
|
||||
|
||||
return $this->expectedEnvironment === $actualEnvironment
|
||||
? $result->ok(trans('admin/health.results.environment.ok'))
|
||||
: $result->failed(trans('admin/health.results.environment.failed', [
|
||||
'actual' => $actualEnvironment,
|
||||
'expected' => $this->expectedEnvironment,
|
||||
]));
|
||||
}
|
||||
}
|
@ -14,30 +14,34 @@ class NodeVersionsCheck extends Check
|
||||
|
||||
public function run(): Result
|
||||
{
|
||||
$all = Node::query()->count();
|
||||
$all = Node::all();
|
||||
|
||||
if ($all === 0) {
|
||||
$result = Result::make()->notificationMessage('No Nodes created')->shortSummary('No Nodes');
|
||||
if ($all->isEmpty()) {
|
||||
$result = Result::make()
|
||||
->notificationMessage(trans('admin/health.results.nodeversions.no_nodes_created'))
|
||||
->shortSummary(trans('admin/health.results.nodeversions.no_nodes'));
|
||||
$result->status = Status::skipped();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$latestVersion = $this->versionService->latestWingsVersion();
|
||||
|
||||
$outdated = Node::query()->get()
|
||||
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && $node->systemInformation()['version'] !== $latestVersion)
|
||||
$outdated = $all
|
||||
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && !$this->versionService->isLatestWings($node->systemInformation()['version']))
|
||||
->count();
|
||||
|
||||
$all = $all->count();
|
||||
$latestVersion = $this->versionService->latestWingsVersion();
|
||||
|
||||
$result = Result::make()
|
||||
->meta([
|
||||
'all' => $all,
|
||||
'outdated' => $outdated,
|
||||
'latestVersion' => $latestVersion,
|
||||
])
|
||||
->shortSummary($outdated === 0 ? 'All up-to-date' : "{$outdated}/{$all} outdated");
|
||||
->shortSummary($outdated === 0 ? trans('admin/health.results.nodeversions.all_up_to_date') : trans('admin/health.results.nodeversions.outdated', ['outdated' => $outdated, 'all' => $all]));
|
||||
|
||||
return $outdated === 0
|
||||
? $result->ok('All Nodes are up-to-date.')
|
||||
: $result->failed(':outdated/:all Nodes are outdated.');
|
||||
? $result->ok(trans('admin/health.results.nodeversions.ok'))
|
||||
: $result->failed(trans('admin/health.results.nodeversions.failed', ['outdated' => $outdated, 'all' => $all]));
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,13 @@ class PanelVersionCheck extends Check
|
||||
'currentVersion' => $currentVersion,
|
||||
'latestVersion' => $latestVersion,
|
||||
])
|
||||
->shortSummary($isLatest ? 'up-to-date' : 'outdated');
|
||||
->shortSummary($isLatest ? trans('admin/health.results.panelversion.up_to_date') : trans('admin/health.results.panelversion.outdated'));
|
||||
|
||||
return $isLatest
|
||||
? $result->ok('Panel is up-to-date.')
|
||||
: $result->failed('Installed version is `:currentVersion` but latest is `:latestVersion`.');
|
||||
? $result->ok(trans('admin/health.results.panelversion.ok'))
|
||||
: $result->failed(trans('admin/health.results.panelversion.failed', [
|
||||
'currentVersion' => $currentVersion,
|
||||
'latestVersion' => $latestVersion,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
41
app/Checks/ScheduleCheck.php
Normal file
41
app/Checks/ScheduleCheck.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Checks;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Composer\InstalledVersions;
|
||||
use Spatie\Health\Checks\Checks\ScheduleCheck as BaseCheck;
|
||||
use Spatie\Health\Checks\Result;
|
||||
|
||||
class ScheduleCheck extends BaseCheck
|
||||
{
|
||||
public function run(): Result
|
||||
{
|
||||
$result = Result::make()->ok(trans('admin/health.results.schedule.ok'));
|
||||
|
||||
$lastHeartbeatTimestamp = cache()->store($this->cacheStoreName)->get($this->cacheKey);
|
||||
|
||||
if (!$lastHeartbeatTimestamp) {
|
||||
return $result->failed(trans('admin/health.results.schedule.failed_not_ran'));
|
||||
}
|
||||
|
||||
$latestHeartbeatAt = Carbon::createFromTimestamp($lastHeartbeatTimestamp);
|
||||
|
||||
$carbonVersion = InstalledVersions::getVersion('nesbot/carbon');
|
||||
|
||||
$minutesAgo = $latestHeartbeatAt->diffInMinutes();
|
||||
|
||||
if (version_compare($carbonVersion,
|
||||
'3.0.0', '<')) {
|
||||
$minutesAgo += 1;
|
||||
}
|
||||
|
||||
if ($minutesAgo > $this->heartbeatMaxAgeInMinutes) {
|
||||
return $result->failed(trans('admin/health.results.schedule.failed_last_ran', [
|
||||
'time' => $minutesAgo,
|
||||
]));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -16,28 +16,37 @@ class CheckEggUpdatesCommand extends Command
|
||||
$eggs = Egg::all();
|
||||
foreach ($eggs as $egg) {
|
||||
try {
|
||||
if (is_null($egg->update_url)) {
|
||||
$this->comment("{$egg->name}: Skipping (no update url set)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentJson = json_decode($exporterService->handle($egg->id));
|
||||
unset($currentJson->exported_at);
|
||||
|
||||
$updatedJson = json_decode(file_get_contents($egg->update_url));
|
||||
unset($updatedJson->exported_at);
|
||||
|
||||
if (md5(json_encode($currentJson)) === md5(json_encode($updatedJson))) {
|
||||
$this->info("{$egg->name}: Up-to-date");
|
||||
cache()->put("eggs.{$egg->uuid}.update", false, now()->addHour());
|
||||
} else {
|
||||
$this->warn("{$egg->name}: Found update");
|
||||
cache()->put("eggs.{$egg->uuid}.update", true, now()->addHour());
|
||||
}
|
||||
$this->check($egg, $exporterService);
|
||||
} catch (Exception $exception) {
|
||||
$this->error("{$egg->name}: Error ({$exception->getMessage()})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function check(Egg $egg, EggExporterService $exporterService): void
|
||||
{
|
||||
if (is_null($egg->update_url)) {
|
||||
$this->comment("$egg->name: Skipping (no update url set)");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$currentJson = json_decode($exporterService->handle($egg->id));
|
||||
unset($currentJson->exported_at);
|
||||
|
||||
$updatedEgg = file_get_contents($egg->update_url);
|
||||
assert($updatedEgg !== false);
|
||||
$updatedJson = json_decode($updatedEgg);
|
||||
unset($updatedJson->exported_at);
|
||||
|
||||
if (md5(json_encode($currentJson, JSON_THROW_ON_ERROR)) === md5(json_encode($updatedJson, JSON_THROW_ON_ERROR))) {
|
||||
$this->info("$egg->name: Up-to-date");
|
||||
cache()->put("eggs.$egg->uuid.update", false, now()->addHour());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->warn("$egg->name: Found update");
|
||||
cache()->put("eggs.$egg->uuid.update", true, now()->addHour());
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AppSettingsCommand extends Command
|
||||
{
|
||||
@ -21,9 +20,13 @@ class AppSettingsCommand extends Command
|
||||
|
||||
if (!config('app.key')) {
|
||||
$this->comment('Generating app key');
|
||||
Artisan::call('key:generate');
|
||||
$this->call('key:generate');
|
||||
}
|
||||
|
||||
Artisan::call('filament:optimize');
|
||||
$this->comment('Creating storage link');
|
||||
$this->call('storage:link');
|
||||
|
||||
$this->comment('Caching components & icons');
|
||||
$this->call('filament:optimize');
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,6 @@ class CacheSettingsCommand extends Command
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* CacheSettingsCommand constructor.
|
||||
*/
|
||||
|
@ -27,6 +27,7 @@ class DatabaseSettingsCommand extends Command
|
||||
{--username= : Username to use when connecting to the MySQL/ MariaDB server.}
|
||||
{--password= : Password to use for the MySQL/ MariaDB database.}';
|
||||
|
||||
/** @var array<array-key, mixed> */
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
@ -57,7 +58,7 @@ class DatabaseSettingsCommand extends Command
|
||||
);
|
||||
|
||||
if ($this->variables['DB_CONNECTION'] === 'mysql') {
|
||||
$this->output->note(__('commands.database_settings.DB_HOST_note'));
|
||||
$this->output->note(trans('commands.database_settings.DB_HOST_note'));
|
||||
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
|
||||
'Database Host',
|
||||
config('database.connections.mysql.host', '127.0.0.1')
|
||||
@ -73,7 +74,7 @@ class DatabaseSettingsCommand extends Command
|
||||
config('database.connections.mysql.database', 'panel')
|
||||
);
|
||||
|
||||
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
|
||||
$this->output->note(trans('commands.database_settings.DB_USERNAME_note'));
|
||||
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
|
||||
'Database Username',
|
||||
config('database.connections.mysql.username', 'pelican')
|
||||
@ -82,7 +83,7 @@ class DatabaseSettingsCommand extends Command
|
||||
$askForMySQLPassword = true;
|
||||
if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) {
|
||||
$this->variables['DB_PASSWORD'] = config('database.connections.mysql.password');
|
||||
$askForMySQLPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
|
||||
$askForMySQLPassword = $this->confirm(trans('commands.database_settings.DB_PASSWORD_note'));
|
||||
}
|
||||
|
||||
if ($askForMySQLPassword) {
|
||||
@ -106,9 +107,9 @@ class DatabaseSettingsCommand extends Command
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
} catch (\PDOException $exception) {
|
||||
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
||||
$this->output->error(__('commands.database_settings.DB_error_2'));
|
||||
$this->output->error(trans('commands.database_settings.DB_error_2'));
|
||||
|
||||
if ($this->confirm(__('commands.database_settings.go_back'))) {
|
||||
if ($this->confirm(trans('commands.database_settings.go_back'))) {
|
||||
$this->database->disconnect('_panel_command_test');
|
||||
|
||||
return $this->handle();
|
||||
@ -117,7 +118,7 @@ class DatabaseSettingsCommand extends Command
|
||||
return 1;
|
||||
}
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'mariadb') {
|
||||
$this->output->note(__('commands.database_settings.DB_HOST_note'));
|
||||
$this->output->note(trans('commands.database_settings.DB_HOST_note'));
|
||||
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
|
||||
'Database Host',
|
||||
config('database.connections.mariadb.host', '127.0.0.1')
|
||||
@ -133,7 +134,7 @@ class DatabaseSettingsCommand extends Command
|
||||
config('database.connections.mariadb.database', 'panel')
|
||||
);
|
||||
|
||||
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
|
||||
$this->output->note(trans('commands.database_settings.DB_USERNAME_note'));
|
||||
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
|
||||
'Database Username',
|
||||
config('database.connections.mariadb.username', 'pelican')
|
||||
@ -142,7 +143,7 @@ class DatabaseSettingsCommand extends Command
|
||||
$askForMariaDBPassword = true;
|
||||
if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) {
|
||||
$this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password');
|
||||
$askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
|
||||
$askForMariaDBPassword = $this->confirm(trans('commands.database_settings.DB_PASSWORD_note'));
|
||||
}
|
||||
|
||||
if ($askForMariaDBPassword) {
|
||||
@ -166,9 +167,9 @@ class DatabaseSettingsCommand extends Command
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
} catch (\PDOException $exception) {
|
||||
$this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
||||
$this->output->error(__('commands.database_settings.DB_error_2'));
|
||||
$this->output->error(trans('commands.database_settings.DB_error_2'));
|
||||
|
||||
if ($this->confirm(__('commands.database_settings.go_back'))) {
|
||||
if ($this->confirm(trans('commands.database_settings.go_back'))) {
|
||||
$this->database->disconnect('_panel_command_test');
|
||||
|
||||
return $this->handle();
|
||||
@ -179,7 +180,7 @@ class DatabaseSettingsCommand extends Command
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
|
||||
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||
'Database Path',
|
||||
env('DB_DATABASE', 'database.sqlite')
|
||||
(string) env('DB_DATABASE', 'database.sqlite')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ class EmailSettingsCommand extends Command
|
||||
{--username=}
|
||||
{--password=}';
|
||||
|
||||
/** @var array<array-key, mixed> */
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
@ -91,7 +92,7 @@ class EmailSettingsCommand extends Command
|
||||
trans('command/messages.environment.mail.ask_smtp_password')
|
||||
);
|
||||
|
||||
$this->variables['MAIL_ENCRYPTION'] = $this->option('encryption') ?? $this->choice(
|
||||
$this->variables['MAIL_SCHEME'] = $this->option('encryption') ?? $this->choice(
|
||||
trans('command/messages.environment.mail.ask_encryption'),
|
||||
['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'],
|
||||
config('mail.mailers.smtp.encryption', 'tls')
|
||||
|
@ -27,8 +27,6 @@ class QueueSettingsCommand extends Command
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* QueueSettingsCommand constructor.
|
||||
*/
|
||||
|
@ -21,7 +21,7 @@ class QueueWorkerServiceCommand extends Command
|
||||
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
|
||||
$path = '/etc/systemd/system/' . $serviceName . '.service';
|
||||
|
||||
$fileExists = file_exists($path);
|
||||
$fileExists = @file_exists($path);
|
||||
if ($fileExists && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
|
||||
$this->line('Creation of queue worker service file aborted because service file already exists.');
|
||||
|
||||
|
@ -20,8 +20,6 @@ class RedisSetupCommand extends Command
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* RedisSetupCommand constructor.
|
||||
*/
|
||||
@ -37,7 +35,7 @@ class RedisSetupCommand extends Command
|
||||
{
|
||||
$this->variables['CACHE_STORE'] = 'redis';
|
||||
$this->variables['QUEUE_CONNECTION'] = 'redis';
|
||||
$this->variables['SESSION_DRIVERS'] = 'redis';
|
||||
$this->variables['SESSION_DRIVER'] = 'redis';
|
||||
|
||||
$this->requestRedisSettings();
|
||||
|
||||
|
@ -28,8 +28,6 @@ class SessionSettingsCommand extends Command
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* SessionSettingsCommand constructor.
|
||||
*/
|
||||
|
@ -6,6 +6,7 @@ use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
|
||||
use SplFileInfo;
|
||||
|
||||
class CleanServiceBackupFilesCommand extends Command
|
||||
{
|
||||
@ -32,9 +33,10 @@ class CleanServiceBackupFilesCommand extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
/** @var SplFileInfo[] */
|
||||
$files = $this->disk->files('services/.bak');
|
||||
|
||||
collect($files)->each(function (\SplFileInfo $file) {
|
||||
collect($files)->each(function ($file) {
|
||||
$lastModified = Carbon::createFromTimestamp($this->disk->lastModified($file->getPath()));
|
||||
if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) {
|
||||
$this->disk->delete($file->getPath());
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Console\Commands\Node;
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\Nodes\NodeCreationService;
|
||||
|
||||
class MakeNodeCommand extends Command
|
||||
{
|
||||
@ -30,14 +30,6 @@ class MakeNodeCommand extends Command
|
||||
|
||||
protected $description = 'Creates a new node on the system via the CLI.';
|
||||
|
||||
/**
|
||||
* MakeNodeCommand constructor.
|
||||
*/
|
||||
public function __construct(private NodeCreationService $creationService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command execution process.
|
||||
*
|
||||
@ -45,31 +37,31 @@ class MakeNodeCommand extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$data['name'] = $this->option('name') ?? $this->ask(__('commands.make_node.name'));
|
||||
$data['description'] = $this->option('description') ?? $this->ask(__('commands.make_node.description'));
|
||||
$data['name'] = $this->option('name') ?? $this->ask(trans('commands.make_node.name'));
|
||||
$data['description'] = $this->option('description') ?? $this->ask(trans('commands.make_node.description'));
|
||||
$data['scheme'] = $this->option('scheme') ?? $this->anticipate(
|
||||
__('commands.make_node.scheme'),
|
||||
trans('commands.make_node.scheme'),
|
||||
['https', 'http'],
|
||||
'https'
|
||||
);
|
||||
|
||||
$data['fqdn'] = $this->option('fqdn') ?? $this->ask(__('commands.make_node.fqdn'));
|
||||
$data['public'] = $this->option('public') ?? $this->confirm(__('commands.make_node.public'), true);
|
||||
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm(__('commands.make_node.behind_proxy'));
|
||||
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm(__('commands.make_node.maintenance_mode'));
|
||||
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'), '0');
|
||||
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'), '-1');
|
||||
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'), '0');
|
||||
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'), '-1');
|
||||
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'), '0');
|
||||
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'), '-1');
|
||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '256');
|
||||
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
|
||||
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');
|
||||
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(__('commands.make_node.daemonSFTPAlias'), '');
|
||||
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
|
||||
$data['fqdn'] = $this->option('fqdn') ?? $this->ask(trans('commands.make_node.fqdn'));
|
||||
$data['public'] = $this->option('public') ?? $this->confirm(trans('commands.make_node.public'), true);
|
||||
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm(trans('commands.make_node.behind_proxy'));
|
||||
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm(trans('commands.make_node.maintenance_mode'));
|
||||
$data['memory'] = $this->option('maxMemory') ?? $this->ask(trans('commands.make_node.memory'), '0');
|
||||
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(trans('commands.make_node.memory_overallocate'), '-1');
|
||||
$data['disk'] = $this->option('maxDisk') ?? $this->ask(trans('commands.make_node.disk'), '0');
|
||||
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(trans('commands.make_node.disk_overallocate'), '-1');
|
||||
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(trans('commands.make_node.cpu'), '0');
|
||||
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1');
|
||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256');
|
||||
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080');
|
||||
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022');
|
||||
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
|
||||
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
|
||||
|
||||
$node = $this->creationService->handle($data);
|
||||
$this->line(__('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
|
||||
$node = Node::create($data);
|
||||
$this->line(trans('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
|
||||
}
|
||||
}
|
||||
|
@ -19,14 +19,14 @@ class NodeConfigurationCommand extends Command
|
||||
|
||||
/** @var \App\Models\Node $node */
|
||||
$node = Node::query()->where($column, $this->argument('node'))->firstOr(function () {
|
||||
$this->error(__('commands.node_config.error_not_exist'));
|
||||
$this->error(trans('commands.node_config.error_not_exist'));
|
||||
|
||||
exit(1);
|
||||
});
|
||||
|
||||
$format = $this->option('format');
|
||||
if (!in_array($format, ['yaml', 'yml', 'json'])) {
|
||||
$this->error(__('commands.node_config.error_invalid_format'));
|
||||
$this->error(trans('commands.node_config.error_invalid_format'));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
@ -13,12 +13,12 @@ class KeyGenerateCommand extends BaseKeyGenerateCommand
|
||||
public function handle(): void
|
||||
{
|
||||
if (!empty(config('app.key')) && $this->input->isInteractive()) {
|
||||
$this->output->warning(__('commands.key_generate.error_already_exist'));
|
||||
if (!$this->confirm(__('commands.key_generate.understand'))) {
|
||||
$this->output->warning(trans('commands.key_generate.error_already_exist'));
|
||||
if (!$this->confirm(trans('commands.key_generate.understand'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->confirm(__('commands.key_generate.continue'))) {
|
||||
if (!$this->confirm(trans('commands.key_generate.continue'))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use Illuminate\Console\Command;
|
||||
use App\Models\Schedule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Services\Schedules\ProcessScheduleService;
|
||||
use Throwable;
|
||||
|
||||
class ProcessRunnableCommand extends Command
|
||||
{
|
||||
@ -13,10 +14,7 @@ class ProcessRunnableCommand extends Command
|
||||
|
||||
protected $description = 'Process schedules in the database and determine which are ready to run.';
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
public function handle(ProcessScheduleService $processScheduleService): int
|
||||
{
|
||||
$schedules = Schedule::query()
|
||||
->with('tasks')
|
||||
@ -27,7 +25,7 @@ class ProcessRunnableCommand extends Command
|
||||
->get();
|
||||
|
||||
if ($schedules->count() < 1) {
|
||||
$this->line(__('commands.schedule.process.no_tasks'));
|
||||
$this->line(trans('commands.schedule.process.no_tasks'));
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -35,7 +33,7 @@ class ProcessRunnableCommand extends Command
|
||||
$bar = $this->output->createProgressBar(count($schedules));
|
||||
foreach ($schedules as $schedule) {
|
||||
$bar->clear();
|
||||
$this->processSchedule($schedule);
|
||||
$this->processSchedule($processScheduleService, $schedule);
|
||||
$bar->advance();
|
||||
$bar->display();
|
||||
}
|
||||
@ -50,23 +48,23 @@ class ProcessRunnableCommand extends Command
|
||||
* never throw an exception out, otherwise you'll end up killing the entire run group causing
|
||||
* any other schedules to not process correctly.
|
||||
*/
|
||||
protected function processSchedule(Schedule $schedule): void
|
||||
protected function processSchedule(ProcessScheduleService $processScheduleService, Schedule $schedule): void
|
||||
{
|
||||
if ($schedule->tasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->getLaravel()->make(ProcessScheduleService::class)->handle($schedule);
|
||||
$processScheduleService->handle($schedule);
|
||||
|
||||
$this->line(trans('command/messages.schedule.output_line', [
|
||||
'schedule' => $schedule->name,
|
||||
'id' => $schedule->id,
|
||||
]));
|
||||
} catch (\Throwable|\Exception $exception) {
|
||||
} catch (Throwable $exception) {
|
||||
logger()->error($exception, ['schedule_id' => $schedule->id]);
|
||||
|
||||
$this->error(__('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
|
||||
$this->error(trans('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Validation\Factory as ValidatorFactory;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use Exception;
|
||||
|
||||
class BulkPowerActionCommand extends Command
|
||||
{
|
||||
@ -19,26 +19,13 @@ class BulkPowerActionCommand extends Command
|
||||
|
||||
protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
|
||||
|
||||
/**
|
||||
* BulkPowerActionCommand constructor.
|
||||
*/
|
||||
public function __construct(private DaemonPowerRepository $powerRepository, private ValidatorFactory $validator)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the bulk power request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function handle(): void
|
||||
public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
|
||||
{
|
||||
$action = $this->argument('action');
|
||||
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
|
||||
$servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers'));
|
||||
|
||||
$validator = $this->validator->make([
|
||||
$validator = $validator->make([
|
||||
'action' => $action,
|
||||
'nodes' => $nodes,
|
||||
'servers' => $servers,
|
||||
@ -64,14 +51,17 @@ class BulkPowerActionCommand extends Command
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$powerRepository = $this->powerRepository;
|
||||
// @phpstan-ignore-next-line
|
||||
$this->getQueryBuilder($servers, $nodes)->each(function (Server $server) use ($action, $powerRepository, &$bar) {
|
||||
|
||||
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed {
|
||||
$bar->clear();
|
||||
|
||||
if (!$server instanceof Server) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$powerRepository->setServer($server)->send($action);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
} catch (Exception $exception) {
|
||||
$this->output->error(trans('command/messages.server.power.action_failed', [
|
||||
'name' => $server->name,
|
||||
'id' => $server->id,
|
||||
@ -82,6 +72,8 @@ class BulkPowerActionCommand extends Command
|
||||
|
||||
$bar->advance();
|
||||
$bar->display();
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
$this->line('');
|
||||
@ -89,6 +81,9 @@ class BulkPowerActionCommand extends Command
|
||||
|
||||
/**
|
||||
* Returns the query builder instance that will return the servers that should be affected.
|
||||
*
|
||||
* @param string[]|int[] $servers
|
||||
* @param string[]|int[] $nodes
|
||||
*/
|
||||
protected function getQueryBuilder(array $servers, array $nodes): Builder
|
||||
{
|
||||
|
@ -34,30 +34,26 @@ class UpgradeCommand extends Command
|
||||
{
|
||||
$skipDownload = $this->option('skip-download');
|
||||
if (!$skipDownload) {
|
||||
$this->output->warning(__('commands.upgrade.integrity'));
|
||||
$this->output->comment(__('commands.upgrade.source_url'));
|
||||
$this->output->warning(trans('commands.upgrade.integrity'));
|
||||
$this->output->comment(trans('commands.upgrade.source_url'));
|
||||
$this->line($this->getUrl());
|
||||
}
|
||||
|
||||
if (version_compare(PHP_VERSION, '7.4.0') < 0) {
|
||||
$this->error(__('commands.upgrade.php_version') . ' [' . PHP_VERSION . '].');
|
||||
}
|
||||
|
||||
$user = 'www-data';
|
||||
$group = 'www-data';
|
||||
if ($this->input->isInteractive()) {
|
||||
if (!$skipDownload) {
|
||||
$skipDownload = !$this->confirm(__('commands.upgrade.skipDownload'), true);
|
||||
$skipDownload = !$this->confirm(trans('commands.upgrade.skipDownload'), true);
|
||||
}
|
||||
|
||||
if (is_null($this->option('user'))) {
|
||||
$userDetails = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner('public')) : [];
|
||||
$user = $userDetails['name'] ?? 'www-data';
|
||||
|
||||
$message = __('commands.upgrade.webserver_user', ['user' => $user]);
|
||||
$message = trans('commands.upgrade.webserver_user', ['user' => $user]);
|
||||
if (!$this->confirm($message, true)) {
|
||||
$user = $this->anticipate(
|
||||
__('commands.upgrade.name_webserver'),
|
||||
trans('commands.upgrade.name_webserver'),
|
||||
[
|
||||
'www-data',
|
||||
'nginx',
|
||||
@ -71,10 +67,10 @@ class UpgradeCommand extends Command
|
||||
$groupDetails = function_exists('posix_getgrgid') ? posix_getgrgid(filegroup('public')) : [];
|
||||
$group = $groupDetails['name'] ?? 'www-data';
|
||||
|
||||
$message = __('commands.upgrade.group_webserver', ['group' => $user]);
|
||||
$message = trans('commands.upgrade.group_webserver', ['group' => $user]);
|
||||
if (!$this->confirm($message, true)) {
|
||||
$group = $this->anticipate(
|
||||
__('commands.upgrade.group_webserver_question'),
|
||||
trans('commands.upgrade.group_webserver_question'),
|
||||
[
|
||||
'www-data',
|
||||
'nginx',
|
||||
@ -84,8 +80,8 @@ class UpgradeCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->confirm(__('commands.upgrade.are_your_sure'))) {
|
||||
$this->warn(__('commands.upgrade.terminated'));
|
||||
if (!$this->confirm(trans('commands.upgrade.are_your_sure'))) {
|
||||
$this->warn(trans('commands.upgrade.terminated'));
|
||||
|
||||
return;
|
||||
}
|
||||
@ -175,7 +171,7 @@ class UpgradeCommand extends Command
|
||||
});
|
||||
|
||||
$this->newLine(2);
|
||||
$this->info(__('commands.upgrade.success'));
|
||||
$this->info(trans('commands.upgrade.success'));
|
||||
}
|
||||
|
||||
protected function withProgress(ProgressBar $bar, \Closure $callback): void
|
||||
|
@ -19,7 +19,7 @@ class DisableTwoFactorCommand extends Command
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->input->isInteractive()) {
|
||||
$this->output->warning(trans('command/messages.user.2fa_help_text'));
|
||||
$this->output->warning(trans('command/messages.user.2fa_help_text.0') . trans('command/messages.user.2fa_help_text.1'));
|
||||
}
|
||||
|
||||
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));
|
||||
|
@ -31,8 +31,11 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
|
||||
$schedule->command('cache:prune-stale-tags')->hourly();
|
||||
if (config('cache.default') === 'redis') {
|
||||
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
|
||||
// This only needs to run when using redis. anything else throws an error.
|
||||
$schedule->command('cache:prune-stale-tags')->hourly();
|
||||
}
|
||||
|
||||
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
|
||||
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
||||
|
22
app/Contracts/Validatable.php
Normal file
22
app/Contracts/Validatable.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
interface Validatable
|
||||
{
|
||||
public function getValidator(): Validator;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getRules(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public static function getRulesForField(string $field): array;
|
||||
|
||||
public function validate(): void;
|
||||
}
|
24
app/Eloquent/BackupQueryBuilder.php
Normal file
24
app/Eloquent/BackupQueryBuilder.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Eloquent;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @extends Builder<TModel>
|
||||
*/
|
||||
class BackupQueryBuilder extends Builder
|
||||
{
|
||||
public function nonFailed(): self
|
||||
{
|
||||
$this->where(function (Builder $query) {
|
||||
$query
|
||||
->whereNull('completed_at')
|
||||
->orWhere('is_successful', true);
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
37
app/Enums/BackupStatus.php
Normal file
37
app/Enums/BackupStatus.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasIcon;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum BackupStatus: string implements HasColor, HasIcon, HasLabel
|
||||
{
|
||||
case InProgress = 'in_progress';
|
||||
case Successful = 'successful';
|
||||
case Failed = 'failed';
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::InProgress => 'tabler-circle-dashed',
|
||||
self::Successful => 'tabler-circle-check',
|
||||
self::Failed => 'tabler-circle-x',
|
||||
};
|
||||
}
|
||||
|
||||
public function getColor(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::InProgress => 'primary',
|
||||
self::Successful => 'success',
|
||||
self::Failed => 'danger',
|
||||
};
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return str($this->value)->headline();
|
||||
}
|
||||
}
|
11
app/Enums/ConsoleWidgetPosition.php
Normal file
11
app/Enums/ConsoleWidgetPosition.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ConsoleWidgetPosition: string
|
||||
{
|
||||
case Top = 'top';
|
||||
case AboveConsole = 'above_console';
|
||||
case BelowConsole = 'below_console';
|
||||
case Bottom = 'bottom';
|
||||
}
|
@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ContainerStatus: string
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasIcon;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
|
||||
{
|
||||
// Docker Based
|
||||
case Created = 'created';
|
||||
@ -19,7 +23,7 @@ enum ContainerStatus: string
|
||||
// HTTP Based
|
||||
case Missing = 'missing';
|
||||
|
||||
public function icon(): string
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
|
||||
@ -36,8 +40,17 @@ enum ContainerStatus: string
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
public function getColor(bool $hex = false): string
|
||||
{
|
||||
if ($hex) {
|
||||
return match ($this) {
|
||||
self::Created, self::Restarting => '#2563EB',
|
||||
self::Starting, self::Paused, self::Removing, self::Stopping => '#D97706',
|
||||
self::Running => '#22C55E',
|
||||
self::Exited, self::Missing, self::Dead, self::Offline => '#EF4444',
|
||||
};
|
||||
}
|
||||
|
||||
return match ($this) {
|
||||
self::Created => 'primary',
|
||||
self::Starting => 'warning',
|
||||
@ -49,18 +62,23 @@ enum ContainerStatus: string
|
||||
self::Removing => 'warning',
|
||||
self::Missing => 'danger',
|
||||
self::Stopping => 'warning',
|
||||
self::Offline => 'gray',
|
||||
self::Offline => 'danger',
|
||||
};
|
||||
}
|
||||
|
||||
public function colorHex(): string
|
||||
public function getLabel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Created, self::Restarting => '#2563EB',
|
||||
self::Starting, self::Paused, self::Removing, self::Stopping => '#D97706',
|
||||
self::Running => '#22C55E',
|
||||
self::Exited, self::Missing, self::Dead, self::Offline => '#EF4444',
|
||||
};
|
||||
return str($this->value)->title();
|
||||
}
|
||||
|
||||
public function isOffline(): bool
|
||||
{
|
||||
return in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
|
||||
}
|
||||
|
||||
public function isStartingOrRunning(): bool
|
||||
{
|
||||
return in_array($this, [ContainerStatus::Starting, ContainerStatus::Running]);
|
||||
}
|
||||
|
||||
public function isStartingOrStopping(): bool
|
||||
|
@ -36,6 +36,7 @@ enum EditorLanguages: string implements HasLabel
|
||||
case java = 'java';
|
||||
case javascript = 'javascript';
|
||||
case julia = 'julia';
|
||||
case json = 'json';
|
||||
case kotlin = 'kotlin';
|
||||
case less = 'less';
|
||||
case lexon = 'lexon';
|
||||
@ -89,9 +90,51 @@ enum EditorLanguages: string implements HasLabel
|
||||
case wgsl = 'wgsl';
|
||||
case xml = 'xml';
|
||||
case yaml = 'yaml';
|
||||
case json = 'json';
|
||||
|
||||
public function getLabel(): ?string
|
||||
public static function fromWithAlias(string $match): self
|
||||
{
|
||||
return match ($match) {
|
||||
'h' => self::c,
|
||||
|
||||
'cc', 'hpp' => self::cpp,
|
||||
|
||||
'cs' => self::csharp,
|
||||
|
||||
'class' => self::java,
|
||||
|
||||
'htm' => self::html,
|
||||
|
||||
'js', 'mjs', 'cjs' => self::javascript,
|
||||
|
||||
'kt', 'kts' => self::kotlin,
|
||||
|
||||
'md' => self::markdown,
|
||||
|
||||
'm' => self::objectivec,
|
||||
|
||||
'pl', 'pm' => self::perl,
|
||||
|
||||
'php3', 'php4', 'php5', 'phtml' => self::php,
|
||||
|
||||
'py', 'pyc', 'pyo', 'pyi' => self::python,
|
||||
|
||||
'rdata', 'rds' => self::r,
|
||||
|
||||
'rb', 'erb' => self::ruby,
|
||||
|
||||
'sc' => self::scala,
|
||||
|
||||
'sh', 'zsh' => self::shell,
|
||||
|
||||
'ts', 'tsx' => self::typescript,
|
||||
|
||||
'yml' => self::yaml,
|
||||
|
||||
default => self::tryFrom($match) ?? self::plaintext,
|
||||
};
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
@ -14,4 +14,24 @@ enum RolePermissionModels: string
|
||||
case Server = 'server';
|
||||
case User = 'user';
|
||||
case Webhook = 'webhook';
|
||||
|
||||
public function viewAny(): string
|
||||
{
|
||||
return RolePermissionPrefixes::ViewAny->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function view(): string
|
||||
{
|
||||
return RolePermissionPrefixes::View->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function create(): string
|
||||
{
|
||||
return RolePermissionPrefixes::Create->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function update(): string
|
||||
{
|
||||
return RolePermissionPrefixes::Update->value . ' ' . $this->value;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ServerState: string
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasIcon;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum ServerState: string implements HasColor, HasIcon, HasLabel
|
||||
{
|
||||
case Normal = 'normal';
|
||||
case Installing = 'installing';
|
||||
@ -11,7 +15,7 @@ enum ServerState: string
|
||||
case Suspended = 'suspended';
|
||||
case RestoringBackup = 'restoring_backup';
|
||||
|
||||
public function icon(): string
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Normal => 'tabler-heart',
|
||||
@ -23,7 +27,7 @@ enum ServerState: string
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
public function getColor(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Normal => 'primary',
|
||||
@ -34,4 +38,9 @@ enum ServerState: string
|
||||
self::RestoringBackup => 'primary',
|
||||
};
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return str($this->value)->headline();
|
||||
}
|
||||
}
|
||||
|
9
app/Enums/SuspendAction.php
Normal file
9
app/Enums/SuspendAction.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum SuspendAction: string
|
||||
{
|
||||
case Suspend = 'suspend';
|
||||
case Unsuspend = 'unsuspend';
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
|
||||
class DirectLogin extends Event
|
||||
{
|
||||
public function __construct(public User $user, public bool $remember) {}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Auth;
|
||||
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FailedPasswordReset extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public string $ip, public string $email) {}
|
||||
}
|
@ -12,6 +12,9 @@ use Illuminate\Http\Response;
|
||||
use Illuminate\Container\Container;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
{
|
||||
public const LEVEL_DEBUG = 'debug';
|
||||
@ -40,6 +43,9 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
return Response::HTTP_BAD_REQUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return [];
|
||||
|
@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@ -45,6 +46,8 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Maps exceptions to a specific response code. This handles special exception
|
||||
* types that don't have a defined response code.
|
||||
*
|
||||
* @var array<class-string, int>
|
||||
*/
|
||||
protected static array $exceptionResponseCodes = [
|
||||
AuthenticationException::class => 401,
|
||||
@ -180,9 +183,16 @@ class Handler extends ExceptionHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the exception as a JSONAPI representation for use on API requests.
|
||||
* @param array<string, mixed> $override
|
||||
* @return array{errors: array{
|
||||
* code: string,
|
||||
* status: string,
|
||||
* detail: string,
|
||||
* source?: array{line: int, file: string},
|
||||
* meta?: array{trace: string[], previous: string[]}
|
||||
* }}|array{errors: array{non-empty-array<string, mixed>}}
|
||||
*/
|
||||
protected function convertExceptionToArray(\Throwable $e, array $override = []): array
|
||||
public static function exceptionToArray(Throwable $e, array $override = []): array
|
||||
{
|
||||
$match = self::$exceptionResponseCodes[get_class($e)] ?? null;
|
||||
|
||||
@ -214,7 +224,7 @@ class Handler extends ExceptionHandler
|
||||
'trace' => Collection::make($e->getTrace())
|
||||
->map(fn ($trace) => Arr::except($trace, ['args']))
|
||||
->all(),
|
||||
'previous' => Collection::make($this->extractPrevious($e))
|
||||
'previous' => Collection::make(self::extractPrevious($e))
|
||||
->map(fn ($exception) => $exception->getTrace())
|
||||
->map(fn ($trace) => Arr::except($trace, ['args']))
|
||||
->all(),
|
||||
@ -225,6 +235,17 @@ class Handler extends ExceptionHandler
|
||||
return ['errors' => [array_merge($error, $override)]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the exception as a JSONAPI representation for use on API requests.
|
||||
*
|
||||
* @param array{detail?: mixed, source?: mixed, meta?: mixed} $override
|
||||
* @return array{errors?: array<mixed>}
|
||||
*/
|
||||
protected function convertExceptionToArray(Throwable $e, array $override = []): array
|
||||
{
|
||||
return self::exceptionToArray($e, $override);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of exceptions that should not be reported.
|
||||
*/
|
||||
@ -244,22 +265,19 @@ class Handler extends ExceptionHandler
|
||||
return new JsonResponse($this->convertExceptionToArray($exception), JsonResponse::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return redirect()->guest('/auth/login');
|
||||
return redirect()->guest(route('filament.app.auth.login'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the previous exceptions that lead to the one passed into this
|
||||
* function being thrown.
|
||||
*
|
||||
* @return \Throwable[]
|
||||
* @return Throwable[]
|
||||
*/
|
||||
protected function extractPrevious(\Throwable $e): array
|
||||
public static function extractPrevious(Throwable $e): array
|
||||
{
|
||||
$previous = [];
|
||||
while ($value = $e->getPrevious()) {
|
||||
if (!$value instanceof \Throwable) {
|
||||
break;
|
||||
}
|
||||
$previous[] = $value;
|
||||
$e = $value;
|
||||
}
|
||||
@ -270,10 +288,11 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Helper method to allow reaching into the handler to convert an exception
|
||||
* into the expected array response type.
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public static function toArray(\Throwable $e): array
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
return (new self(app()))->convertExceptionToArray($e);
|
||||
return self::exceptionToArray($e);
|
||||
}
|
||||
}
|
||||
|
@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Http\Connection;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Exceptions\DisplayException;
|
||||
use Illuminate\Support\Facades\Context;
|
||||
|
||||
class DaemonConnectionException extends DisplayException
|
||||
{
|
||||
private int $statusCode = Response::HTTP_GATEWAY_TIMEOUT;
|
||||
|
||||
/**
|
||||
* Every request to the daemon instance will return a unique X-Request-Id header
|
||||
* which allows for all errors to be efficiently tied to a specific request that
|
||||
* triggered them, and gives users a more direct method of informing hosts when
|
||||
* something goes wrong.
|
||||
*/
|
||||
private ?string $requestId;
|
||||
|
||||
/**
|
||||
* Throw a displayable exception caused by a daemon connection error.
|
||||
*/
|
||||
public function __construct(?Exception $previous, bool $useStatusCode = true)
|
||||
{
|
||||
/** @var \GuzzleHttp\Psr7\Response|null $response */
|
||||
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
|
||||
$this->requestId = $response?->getHeaderLine('X-Request-Id');
|
||||
|
||||
Context::add('request_id', $this->requestId);
|
||||
|
||||
if ($useStatusCode) {
|
||||
$this->statusCode = is_null($response) ? $this->statusCode : $response->getStatusCode();
|
||||
// There are rare conditions where daemon encounters a panic condition and crashes the
|
||||
// request being made after content has already been sent over the wire. In these cases
|
||||
// you can end up with a "successful" response code that is actual an error.
|
||||
//
|
||||
// Handle those better here since we shouldn't ever end up in this exception state and
|
||||
// be returning a 2XX level response.
|
||||
if ($this->statusCode < 400) {
|
||||
$this->statusCode = Response::HTTP_BAD_GATEWAY;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($response)) {
|
||||
$message = 'Could not establish a connection to the machine running this server. Please try again.';
|
||||
} else {
|
||||
$message = sprintf('There was an error while communicating with the machine running this server. This error has been logged, please try again. (code: %s) (request_id: %s)', $response->getStatusCode(), $this->requestId ?? '<nil>');
|
||||
}
|
||||
|
||||
// Attempt to pull the actual error message off the response and return that if it is not
|
||||
// a 500 level error.
|
||||
if ($this->statusCode < 500 && !is_null($response)) {
|
||||
$body = json_decode($response->getBody()->__toString(), true);
|
||||
$message = sprintf('An error occurred on the remote host: %s. (request id: %s)', $body['error'] ?? $message, $this->requestId ?? '<nil>');
|
||||
}
|
||||
|
||||
$level = $this->statusCode >= 500 && $this->statusCode !== 504
|
||||
? DisplayException::LEVEL_ERROR
|
||||
: DisplayException::LEVEL_WARNING;
|
||||
|
||||
parent::__construct($message, $previous, $level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HTTP status code for this exception.
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
}
|
@ -42,6 +42,9 @@ class DataValidationException extends PanelException implements HttpExceptionInt
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return [];
|
||||
|
7
app/Exceptions/Repository/FileNotEditableException.php
Normal file
7
app/Exceptions/Repository/FileNotEditableException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Repository;
|
||||
|
||||
use Exception;
|
||||
|
||||
class FileNotEditableException extends Exception {}
|
42
app/Extensions/Avatar/AvatarProvider.php
Normal file
42
app/Extensions/Avatar/AvatarProvider.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class AvatarProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
public static function getProvider(string $id): ?self
|
||||
{
|
||||
return Arr::get(static::$providers, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, static>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return static::$providers;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
abstract public function get(User $user): ?string;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::title($this->getId());
|
||||
}
|
||||
}
|
24
app/Extensions/Avatar/Providers/GravatarProvider.php
Normal file
24
app/Extensions/Avatar/Providers/GravatarProvider.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar\Providers;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Models\User;
|
||||
|
||||
class GravatarProvider extends AvatarProvider
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gravatar';
|
||||
}
|
||||
|
||||
public function get(User $user): string
|
||||
{
|
||||
return 'https://gravatar.com/avatar/' . md5($user->email);
|
||||
}
|
||||
|
||||
public static function register(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
30
app/Extensions/Avatar/Providers/UiAvatarsProvider.php
Normal file
30
app/Extensions/Avatar/Providers/UiAvatarsProvider.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar\Providers;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Models\User;
|
||||
|
||||
class UiAvatarsProvider extends AvatarProvider
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'uiavatars';
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'UI Avatars';
|
||||
}
|
||||
|
||||
public function get(User $user): ?string
|
||||
{
|
||||
// UI Avatars is the default of filament so just return null here
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function register(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
@ -16,17 +16,18 @@ class BackupManager
|
||||
{
|
||||
/**
|
||||
* The array of resolved backup drivers.
|
||||
*
|
||||
* @var array<string, FilesystemAdapter>
|
||||
*/
|
||||
protected array $adapters = [];
|
||||
|
||||
/**
|
||||
* The registered custom driver creators.
|
||||
*
|
||||
* @var array<string, callable>
|
||||
*/
|
||||
protected array $customCreators;
|
||||
|
||||
/**
|
||||
* BackupManager constructor.
|
||||
*/
|
||||
public function __construct(protected Application $app) {}
|
||||
|
||||
/**
|
||||
@ -86,6 +87,8 @@ class BackupManager
|
||||
|
||||
/**
|
||||
* Calls a custom creator for a given adapter type.
|
||||
*
|
||||
* @param array{adapter: string} $config
|
||||
*/
|
||||
protected function callCustomCreator(array $config): mixed
|
||||
{
|
||||
@ -94,6 +97,8 @@ class BackupManager
|
||||
|
||||
/**
|
||||
* Creates a new daemon adapter.
|
||||
*
|
||||
* @param array<string, string> $config
|
||||
*/
|
||||
public function createWingsAdapter(array $config): FilesystemAdapter
|
||||
{
|
||||
@ -102,6 +107,8 @@ class BackupManager
|
||||
|
||||
/**
|
||||
* Creates a new S3 adapter.
|
||||
*
|
||||
* @param array<string, string> $config
|
||||
*/
|
||||
public function createS3Adapter(array $config): FilesystemAdapter
|
||||
{
|
||||
@ -118,6 +125,8 @@ class BackupManager
|
||||
|
||||
/**
|
||||
* Returns the configuration associated with a given backup type.
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
protected function getConfig(string $name): array
|
||||
{
|
||||
@ -147,8 +156,9 @@ class BackupManager
|
||||
*/
|
||||
public function forget(array|string $adapter): self
|
||||
{
|
||||
$adapters = &$this->adapters;
|
||||
foreach ((array) $adapter as $adapterName) {
|
||||
unset($this->adapters[$adapterName]);
|
||||
unset($adapters[$adapterName]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
118
app/Extensions/Captcha/Providers/CaptchaProvider.php
Normal file
118
app/Extensions/Captcha/Providers/CaptchaProvider.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Providers;
|
||||
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class CaptchaProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
/**
|
||||
* @return self|static[]
|
||||
*/
|
||||
public static function get(?string $id = null): array|self
|
||||
{
|
||||
return $id ? static::$providers[$id] : static::$providers;
|
||||
}
|
||||
|
||||
protected function __construct(protected Application $app)
|
||||
{
|
||||
if (array_key_exists($this->getId(), static::$providers)) {
|
||||
if (!$this->app->runningUnitTests()) {
|
||||
logger()->warning("Tried to create duplicate Captcha provider with id '{$this->getId()}'");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
config()->set('captcha.' . Str::lower($this->getId()), $this->getConfig());
|
||||
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
abstract public function getComponent(): Component;
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return [
|
||||
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
|
||||
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return [
|
||||
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
|
||||
->label('Site Key')
|
||||
->placeholder('Site Key')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env("CAPTCHA_{$id}_SITE_KEY")),
|
||||
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
|
||||
->label('Secret Key')
|
||||
->placeholder('Secret Key')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
|
||||
];
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::title($this->getId());
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return env("CAPTCHA_{$id}_ENABLED", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
*/
|
||||
public function validateResponse(?string $captchaResponse = null): array
|
||||
{
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'validateResponse not defined',
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
106
app/Extensions/Captcha/Providers/TurnstileProvider.php
Normal file
106
app/Extensions/Captcha/Providers/TurnstileProvider.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Captcha\Providers;
|
||||
|
||||
use App\Filament\Components\Forms\Fields\TurnstileCaptcha;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class TurnstileProvider extends CaptchaProvider
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'turnstile';
|
||||
}
|
||||
|
||||
public function getComponent(): Component
|
||||
{
|
||||
return TurnstileCaptcha::make('turnstile');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return array_merge(parent::getConfig(), [
|
||||
'verify_domain' => env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
return array_merge(parent::getSettingsForm(), [
|
||||
Toggle::make('CAPTCHA_TURNSTILE_VERIFY_DOMAIN')
|
||||
->label(trans('admin/setting.captcha.verify'))
|
||||
->columnSpan(2)
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
|
||||
Placeholder::make('info')
|
||||
->label(trans('admin/setting.captcha.info_label'))
|
||||
->columnSpan(2)
|
||||
->content(new HtmlString(trans('admin/setting.captcha.info'))),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'tabler-brand-cloudflare';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
*/
|
||||
public function validateResponse(?string $captchaResponse = null): array
|
||||
{
|
||||
$captchaResponse ??= request()->get('cf-turnstile-response');
|
||||
|
||||
if (!$secret = env('CAPTCHA_TURNSTILE_SECRET_KEY')) {
|
||||
throw new Exception('Turnstile secret key is not defined.');
|
||||
}
|
||||
|
||||
$response = Http::asJson()
|
||||
->timeout(15)
|
||||
->connectTimeout(5)
|
||||
->throw()
|
||||
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||
'secret' => $secret,
|
||||
'response' => $captchaResponse,
|
||||
]);
|
||||
|
||||
return count($response->json()) ? $response->json() : [
|
||||
'success' => false,
|
||||
'message' => 'Unknown error occurred, please try again',
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
|
||||
{
|
||||
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$requestUrl ??= request()->url;
|
||||
$requestUrl = parse_url($requestUrl);
|
||||
|
||||
return $hostname === array_get($requestUrl, 'host');
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions;
|
||||
|
||||
use App\Models\DatabaseHost;
|
||||
|
||||
class DynamicDatabaseConnection
|
||||
{
|
||||
public const DB_CHARSET = 'utf8';
|
||||
|
||||
public const DB_COLLATION = 'utf8_unicode_ci';
|
||||
|
||||
public const DB_DRIVER = 'mysql';
|
||||
|
||||
/**
|
||||
* Adds a dynamic database connection entry to the runtime config.
|
||||
*/
|
||||
public function set(string $connection, DatabaseHost|int $host, string $database = 'mysql'): void
|
||||
{
|
||||
if (!$host instanceof DatabaseHost) {
|
||||
$host = DatabaseHost::query()->findOrFail($host);
|
||||
}
|
||||
|
||||
config()->set('database.connections.' . $connection, [
|
||||
'driver' => self::DB_DRIVER,
|
||||
'host' => $host->host,
|
||||
'port' => $host->port,
|
||||
'database' => $database,
|
||||
'username' => $host->username,
|
||||
'password' => $host->password,
|
||||
'charset' => self::DB_CHARSET,
|
||||
'collation' => self::DB_COLLATION,
|
||||
]);
|
||||
}
|
||||
}
|
51
app/Extensions/Features/FeatureProvider.php
Normal file
51
app/Extensions/Features/FeatureProvider.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
abstract class FeatureProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
/**
|
||||
* @param string[] $id
|
||||
* @return self|static[]
|
||||
*/
|
||||
public static function getProviders(string|array|null $id = null): array|self
|
||||
{
|
||||
if (is_array($id)) {
|
||||
return array_intersect_key(static::$providers, array_flip($id));
|
||||
}
|
||||
|
||||
return $id ? static::$providers[$id] : static::$providers;
|
||||
}
|
||||
|
||||
protected function __construct(protected Application $app)
|
||||
{
|
||||
if (array_key_exists($this->getId(), static::$providers)) {
|
||||
if (!$this->app->runningUnitTests()) {
|
||||
logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
/**
|
||||
* A matching subset string (case-insensitive) from the console output
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
abstract public function getListeners(): array;
|
||||
|
||||
abstract public function getAction(): Action;
|
||||
}
|
127
app/Extensions/Features/GSLToken.php
Normal file
127
app/Extensions/Features/GSLToken.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class GSLToken extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'(gsl token expired)',
|
||||
'(account not found)',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gsl_token';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
/** @var ServerVariable $serverVariable */
|
||||
$serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
|
||||
$query->where('env_variable', 'STEAM_ACC');
|
||||
})->first();
|
||||
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Invalid GSL token')
|
||||
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
|
||||
->modalSubmitActionLabel('Update GSL Token')
|
||||
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
|
||||
->form([
|
||||
Placeholder::make('info')
|
||||
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
|
||||
TextInput::make('gsltoken')
|
||||
->label('GSL Token')
|
||||
->rules([
|
||||
fn (): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $serverVariable->variable->rules,
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
])
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn () => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn () => implode('|', $serverVariable->variable->rules))
|
||||
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
|
||||
])
|
||||
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
try {
|
||||
$new = $data['gsltoken'] ?? '';
|
||||
$original = $serverVariable->variable_value;
|
||||
|
||||
$serverVariable->update([
|
||||
'variable_value' => $new,
|
||||
]);
|
||||
|
||||
if ($original !== $new) {
|
||||
|
||||
Activity::event('server:startup.edit')
|
||||
->property([
|
||||
'variable' => $serverVariable->variable->env_variable,
|
||||
'old' => $original,
|
||||
'new' => $new,
|
||||
])
|
||||
->log();
|
||||
}
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('GSL Token updated')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Could not update GSL Token')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
100
app/Extensions/Features/JavaVersion.php
Normal file
100
app/Extensions/Features/JavaVersion.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
class JavaVersion extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'java.lang.UnsupportedClassVersionError',
|
||||
'unsupported major.minor version',
|
||||
'has been compiled by a more recent version of the java runtime',
|
||||
'minecraft 1.17 requires running the server with java 16 or above',
|
||||
'minecraft 1.18 requires running the server with java 17 or above',
|
||||
'minecraft 1.19 requires running the server with java 17 or above',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'java_version';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Unsupported Java Version')
|
||||
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
|
||||
->modalSubmitActionLabel('Update Docker Image')
|
||||
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
|
||||
->form([
|
||||
Placeholder::make('java')
|
||||
->label('Please select a supported version from the list below to continue starting the server.'),
|
||||
Select::make('image')
|
||||
->label('Docker Image')
|
||||
->disabled(fn () => !in_array($server->image, $server->egg->docker_images))
|
||||
->options(fn () => collect($server->egg->docker_images)->mapWithKeys(fn ($key, $value) => [$key => $value]))
|
||||
->selectablePlaceholder(false)
|
||||
->default(fn () => $server->image)
|
||||
->notIn(fn () => $server->image)
|
||||
->required()
|
||||
->preload()
|
||||
->native(false),
|
||||
])
|
||||
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
|
||||
try {
|
||||
$new = $data['image'];
|
||||
$original = $server->image;
|
||||
$server->forceFill(['image' => $new])->saveOrFail();
|
||||
|
||||
if ($original !== $server->image) {
|
||||
Activity::event('server:startup.image')
|
||||
->property(['old' => $original, 'new' => $new])
|
||||
->log();
|
||||
}
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('Docker image updated')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Could not update docker image')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
71
app/Extensions/Features/MinecraftEula.php
Normal file
71
app/Extensions/Features/MinecraftEula.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class MinecraftEula extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'you need to agree to the eula in order to run the server',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'eula';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Minecraft EULA')
|
||||
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
|
||||
->modalSubmitActionLabel('I Accept')
|
||||
->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
|
||||
try {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('Minecraft EULA accepted')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Could not accept Minecraft EULA')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
76
app/Extensions/Features/PIDLimit.php
Normal file
76
app/Extensions/Features/PIDLimit.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class PIDLimit extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'pthread_create failed',
|
||||
'failed to create thread',
|
||||
'unable to create thread',
|
||||
'unable to create native thread',
|
||||
'unable to create new native thread',
|
||||
'exception in thread "craft async scheduler management thread"',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'pid_limit';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->icon('tabler-alert-triangle')
|
||||
->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
|
||||
->modalDescription(new HtmlString(Blade::render(
|
||||
auth()->user()->isAdmin() ? <<<'HTML'
|
||||
<p>
|
||||
This server has reached the maximum process or memory limit.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
Increasing <code>container_pid_limit</code> in the wings
|
||||
configuration, <code>config.yml</code>, might help resolve
|
||||
this issue.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
<b>Note: Wings must be restarted for the configuration file changes to take effect</b>
|
||||
</p>
|
||||
HTML
|
||||
:
|
||||
<<<'HTML'
|
||||
<p>
|
||||
This server is attempting to use more resources than allocated. Please contact the administrator
|
||||
and give them the error below.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
<code>
|
||||
pthread_create failed, Possibly out of memory or process/resource limits reached
|
||||
</code>
|
||||
</p>
|
||||
HTML
|
||||
)))
|
||||
->modalCancelActionLabel('Close')
|
||||
->action(fn () => null);
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
64
app/Extensions/Features/SteamDiskSpace.php
Normal file
64
app/Extensions/Features/SteamDiskSpace.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class SteamDiskSpace extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'steamcmd needs 250mb of free disk space to update',
|
||||
'0x202 after update job',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'steam_disk_space';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Out of available disk space...')
|
||||
->modalDescription(new HtmlString(Blade::render(
|
||||
auth()->user()->isAdmin() ? <<<'HTML'
|
||||
<p>
|
||||
This server has run out of available disk space and cannot complete the install or update
|
||||
process.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
Ensure the machine has enough disk space by typing{' '}
|
||||
<code class="rounded py-1 px-2">df -h</code> on the machine hosting
|
||||
this server. Delete files or increase the available disk space to resolve the issue.
|
||||
</p>
|
||||
HTML
|
||||
:
|
||||
<<<'HTML'
|
||||
<p>
|
||||
This server has run out of available disk space and cannot complete the install or update
|
||||
process. Please get in touch with the administrator(s) and inform them of disk space issues.
|
||||
</p>
|
||||
HTML
|
||||
)))
|
||||
->modalCancelActionLabel('Close')
|
||||
->action(fn () => null);
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,28 +1,5 @@
|
||||
<?php
|
||||
|
||||
/* The MIT License (MIT)
|
||||
|
||||
Pterodactyl®
|
||||
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE. */
|
||||
|
||||
namespace App\Extensions\Filesystem;
|
||||
|
||||
use Aws\S3\S3ClientInterface;
|
||||
@ -30,6 +7,9 @@ use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
|
||||
|
||||
class S3Filesystem extends AwsS3V3Adapter
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $options
|
||||
*/
|
||||
public function __construct(
|
||||
private S3ClientInterface $client,
|
||||
private string $bucket,
|
||||
|
@ -8,6 +8,9 @@ class PanelSerializer extends ArraySerializer
|
||||
{
|
||||
/**
|
||||
* Serialize an item.
|
||||
*
|
||||
* @param array<mixed> $data
|
||||
* @return array{object: ?string, attributes: array<mixed>}
|
||||
*/
|
||||
public function item(?string $resourceKey, array $data): array
|
||||
{
|
||||
@ -19,6 +22,9 @@ class PanelSerializer extends ArraySerializer
|
||||
|
||||
/**
|
||||
* Serialize a collection.
|
||||
*
|
||||
* @param array<mixed> $data
|
||||
* @return array{object: 'list', data: array<mixed>}
|
||||
*/
|
||||
public function collection(?string $resourceKey, array $data): array
|
||||
{
|
||||
@ -35,6 +41,8 @@ class PanelSerializer extends ArraySerializer
|
||||
|
||||
/**
|
||||
* Serialize a null resource.
|
||||
*
|
||||
* @return ?array{object: ?string, attributes: null}
|
||||
*/
|
||||
public function null(): ?array
|
||||
{
|
||||
@ -46,6 +54,10 @@ class PanelSerializer extends ArraySerializer
|
||||
|
||||
/**
|
||||
* Merge the included resources with the parent resource being serialized.
|
||||
*
|
||||
* @param array{relationships: array{string, mixed}} $transformedData
|
||||
* @param array{string, mixed} $includedData
|
||||
* @return array{relationships: array{string, mixed}}
|
||||
*/
|
||||
public function mergeIncludes(array $transformedData, array $includedData): array
|
||||
{
|
||||
|
74
app/Extensions/OAuth/Providers/AuthentikProvider.php
Normal file
74
app/Extensions/OAuth/Providers/AuthentikProvider.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Foundation\Application;
|
||||
use SocialiteProviders\Authentik\Provider;
|
||||
|
||||
final class AuthentikProvider extends OAuthProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'authentik';
|
||||
}
|
||||
|
||||
public function getProviderClass(): string
|
||||
{
|
||||
return Provider::class;
|
||||
}
|
||||
|
||||
public function getServiceConfig(): array
|
||||
{
|
||||
return [
|
||||
'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'),
|
||||
'client_id' => env('OAUTH_AUTHENTIK_CLIENT_ID'),
|
||||
'client_secret' => env('OAUTH_AUTHENTIK_CLIENT_SECRET'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
return array_merge(parent::getSettingsForm(), [
|
||||
TextInput::make('OAUTH_AUTHENTIK_BASE_URL')
|
||||
->label('Base URL')
|
||||
->placeholder('Base URL')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->url()
|
||||
->autocomplete(false)
|
||||
->default(env('OAUTH_AUTHENTIK_BASE_URL')),
|
||||
TextInput::make('OAUTH_AUTHENTIK_DISPLAY_NAME')
|
||||
->label('Display Name')
|
||||
->placeholder('Display Name')
|
||||
->autocomplete(false)
|
||||
->default(env('OAUTH_AUTHENTIK_DISPLAY_NAME', 'Authentik')),
|
||||
ColorPicker::make('OAUTH_AUTHENTIK_DISPLAY_COLOR')
|
||||
->label('Display Color')
|
||||
->placeholder('#fd4b2d')
|
||||
->default(env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d'))
|
||||
->hex(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return env('OAUTH_AUTHENTIK_DISPLAY_NAME', 'Authentik');
|
||||
}
|
||||
|
||||
public function getHexColor(): string
|
||||
{
|
||||
return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d');
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
38
app/Extensions/OAuth/Providers/CommonProvider.php
Normal file
38
app/Extensions/OAuth/Providers/CommonProvider.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
final class CommonProvider extends OAuthProvider
|
||||
{
|
||||
protected function __construct(protected Application $app, private string $id, private ?string $providerClass, private ?string $icon, private ?string $hexColor)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProviderClass(): ?string
|
||||
{
|
||||
return $this->providerClass;
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function getHexColor(): ?string
|
||||
{
|
||||
return $this->hexColor;
|
||||
}
|
||||
|
||||
public static function register(Application $app, string $id, ?string $providerClass = null, ?string $icon = null, ?string $hexColor = null): static
|
||||
{
|
||||
return new self($app, $id, $providerClass, $icon, $hexColor);
|
||||
}
|
||||
}
|
64
app/Extensions/OAuth/Providers/DiscordProvider.php
Normal file
64
app/Extensions/OAuth/Providers/DiscordProvider.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use SocialiteProviders\Discord\Provider;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
final class DiscordProvider extends OAuthProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'discord';
|
||||
}
|
||||
|
||||
public function getProviderClass(): string
|
||||
{
|
||||
return Provider::class;
|
||||
}
|
||||
|
||||
public function getSetupSteps(): array
|
||||
{
|
||||
return array_merge([
|
||||
Step::make('Register new Discord OAuth App')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))),
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
|
||||
TextInput::make('_noenv_callback')
|
||||
->label('Redirect URL')
|
||||
->dehydrated()
|
||||
->disabled()
|
||||
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
|
||||
]),
|
||||
], parent::getSetupSteps());
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'tabler-brand-discord-f';
|
||||
}
|
||||
|
||||
public function getHexColor(): string
|
||||
{
|
||||
return '#5865F2';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
63
app/Extensions/OAuth/Providers/GithubProvider.php
Normal file
63
app/Extensions/OAuth/Providers/GithubProvider.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
final class GithubProvider extends OAuthProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'github';
|
||||
}
|
||||
|
||||
public function getSetupSteps(): array
|
||||
{
|
||||
return array_merge([
|
||||
Step::make('Register new Github OAuth App')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
|
||||
TextInput::make('_noenv_callback')
|
||||
->label('Authorization callback URL')
|
||||
->dehydrated()
|
||||
->disabled()
|
||||
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->default(fn () => url('/auth/oauth/callback/github')),
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
|
||||
]),
|
||||
Step::make('Create Client Secret')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString('<p>Once you registered your app, generate a new <b>Client Secret</b>.</p><p>You will also need the <b>Client ID</b>.</p>')),
|
||||
]),
|
||||
], parent::getSetupSteps());
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'tabler-brand-github-f';
|
||||
}
|
||||
|
||||
public function getHexColor(): string
|
||||
{
|
||||
return '#4078c0';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
76
app/Extensions/OAuth/Providers/GitlabProvider.php
Normal file
76
app/Extensions/OAuth/Providers/GitlabProvider.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
final class GitlabProvider extends OAuthProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gitlab';
|
||||
}
|
||||
|
||||
public function getServiceConfig(): array
|
||||
{
|
||||
return array_merge(parent::getServiceConfig(), [
|
||||
'host' => env('OAUTH_GITLAB_HOST'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
return array_merge(parent::getSettingsForm(), [
|
||||
TextInput::make('OAUTH_GITLAB_HOST')
|
||||
->label('Custom Host')
|
||||
->placeholder('Only set a custom host if you are self hosting gitlab')
|
||||
->columnSpan(2)
|
||||
->url()
|
||||
->autocomplete(false)
|
||||
->default(env('OAUTH_GITLAB_HOST')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSetupSteps(): array
|
||||
{
|
||||
return array_merge([
|
||||
Step::make('Register new Gitlab OAuth App')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(Blade::render('Check out the <x-filament::link href="https://docs.gitlab.com/integration/oauth_provider/" target="_blank">Gitlab docs</x-filament::link> on how to create the oauth app.'))),
|
||||
TextInput::make('_noenv_callback')
|
||||
->label('Redirect URI')
|
||||
->dehydrated()
|
||||
->disabled()
|
||||
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->default(fn () => url('/auth/oauth/callback/gitlab')),
|
||||
]),
|
||||
], parent::getSetupSteps());
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'tabler-brand-gitlab';
|
||||
}
|
||||
|
||||
public function getHexColor(): string
|
||||
{
|
||||
return '#fca326';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
131
app/Extensions/OAuth/Providers/OAuthProvider.php
Normal file
131
app/Extensions/OAuth/Providers/OAuthProvider.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
abstract class OAuthProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
/**
|
||||
* @return self|static[]
|
||||
*/
|
||||
public static function get(?string $id = null): array|self
|
||||
{
|
||||
return $id ? static::$providers[$id] : static::$providers;
|
||||
}
|
||||
|
||||
protected function __construct(protected Application $app)
|
||||
{
|
||||
if (array_key_exists($this->getId(), static::$providers)) {
|
||||
if (!$this->app->runningUnitTests()) {
|
||||
logger()->warning("Tried to create duplicate OAuth provider with id '{$this->getId()}'");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
config()->set('services.' . $this->getId(), array_merge($this->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $this->getId()]));
|
||||
|
||||
if ($this->getProviderClass()) {
|
||||
Event::listen(function (SocialiteWasCalled $event) {
|
||||
$event->extendSocialite($this->getId(), $this->getProviderClass());
|
||||
});
|
||||
}
|
||||
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
public function getProviderClass(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]|bool|null>
|
||||
*/
|
||||
public function getServiceConfig(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return [
|
||||
'client_id' => env("OAUTH_{$id}_CLIENT_ID"),
|
||||
'client_secret' => env("OAUTH_{$id}_CLIENT_SECRET"),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return [
|
||||
TextInput::make("OAUTH_{$id}_CLIENT_ID")
|
||||
->label('Client ID')
|
||||
->placeholder('Client ID')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env("OAUTH_{$id}_CLIENT_ID")),
|
||||
TextInput::make("OAUTH_{$id}_CLIENT_SECRET")
|
||||
->label('Client Secret')
|
||||
->placeholder('Client Secret')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env("OAUTH_{$id}_CLIENT_SECRET")),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Step[]
|
||||
*/
|
||||
public function getSetupSteps(): array
|
||||
{
|
||||
return [
|
||||
Step::make('OAuth Config')
|
||||
->columns(4)
|
||||
->schema($this->getSettingsForm()),
|
||||
];
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::title($this->getId());
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getHexColor(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return env("OAUTH_{$id}_ENABLED", false);
|
||||
}
|
||||
}
|
81
app/Extensions/OAuth/Providers/SteamProvider.php
Normal file
81
app/Extensions/OAuth/Providers/SteamProvider.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use SocialiteProviders\Steam\Provider;
|
||||
|
||||
final class SteamProvider extends OAuthProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'steam';
|
||||
}
|
||||
|
||||
public function getProviderClass(): string
|
||||
{
|
||||
return Provider::class;
|
||||
}
|
||||
|
||||
public function getServiceConfig(): array
|
||||
{
|
||||
return [
|
||||
'client_id' => null,
|
||||
'client_secret' => env('OAUTH_STEAM_CLIENT_SECRET'),
|
||||
'allowed_hosts' => [
|
||||
str_replace(['http://', 'https://'], '', env('APP_URL')),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('OAUTH_STEAM_CLIENT_SECRET')
|
||||
->label('Web API Key')
|
||||
->placeholder('Web API Key')
|
||||
->columnSpan(4)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->default(env('OAUTH_STEAM_CLIENT_SECRET')),
|
||||
];
|
||||
}
|
||||
|
||||
public function getSetupSteps(): array
|
||||
{
|
||||
return array_merge([
|
||||
Step::make('Create API Key')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(Blade::render('Visit <x-filament::link href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</x-filament::link> to generate an API key.'))),
|
||||
]),
|
||||
], parent::getSetupSteps());
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'tabler-brand-steam-f';
|
||||
}
|
||||
|
||||
public function getHexColor(): string
|
||||
{
|
||||
return '#00adee';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -2,33 +2,13 @@
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
|
||||
use App\Filament\Admin\Resources\NodeResource\Pages\ListNodes;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Pages\Dashboard as BaseDashboard;
|
||||
|
||||
class Dashboard extends Page
|
||||
class Dashboard extends BaseDashboard
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-layout-dashboard';
|
||||
|
||||
protected static string $view = 'filament.pages.dashboard';
|
||||
|
||||
protected ?string $heading = '';
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('strings.dashboard');
|
||||
}
|
||||
|
||||
protected static ?string $slug = '/';
|
||||
|
||||
public string $activeTab = 'nodes';
|
||||
|
||||
private SoftwareVersionService $softwareVersionService;
|
||||
|
||||
public function mount(SoftwareVersionService $softwareVersionService): void
|
||||
@ -36,51 +16,18 @@ class Dashboard extends Page
|
||||
$this->softwareVersionService = $softwareVersionService;
|
||||
}
|
||||
|
||||
public function getViewData(): array
|
||||
public function getColumns(): int
|
||||
{
|
||||
return [
|
||||
'inDevelopment' => config('app.version') === 'canary',
|
||||
'version' => $this->softwareVersionService->currentPanelVersion(),
|
||||
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
|
||||
'isLatest' => $this->softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
'serversCount' => Server::query()->count(),
|
||||
'usersCount' => User::query()->count(),
|
||||
return 1;
|
||||
}
|
||||
|
||||
'devActions' => [
|
||||
CreateAction::make()
|
||||
->label('Bugs & Features')
|
||||
->icon('tabler-brand-github')
|
||||
->url('https://github.com/pelican-dev/panel/discussions', true),
|
||||
],
|
||||
'updateActions' => [
|
||||
CreateAction::make()
|
||||
->label('Read Documentation')
|
||||
->icon('tabler-clipboard-text')
|
||||
->url('https://pelican.dev/docs/panel/update', true)
|
||||
->color('warning'),
|
||||
],
|
||||
'nodeActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
|
||||
->icon('tabler-server-2')
|
||||
->url(CreateNode::getUrl()),
|
||||
],
|
||||
'supportActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-help.button_docs'))
|
||||
->icon('tabler-speedboat')
|
||||
->url('https://pelican.dev/docs', true),
|
||||
],
|
||||
];
|
||||
public function getHeading(): string
|
||||
{
|
||||
return trans('admin/dashboard.heading');
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
return trans('admin/dashboard.version', ['version' => $this->softwareVersionService->currentPanelVersion()]);
|
||||
}
|
||||
}
|
||||
|
@ -8,25 +8,45 @@ use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Spatie\Health\Commands\RunHealthChecksCommand;
|
||||
use Spatie\Health\Enums\Status;
|
||||
use Spatie\Health\ResultStores\ResultStore;
|
||||
|
||||
class Health extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-heart';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static string $view = 'filament.pages.health';
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
/** @var array<string, string> */
|
||||
protected $listeners = [
|
||||
'refresh-component' => '$refresh',
|
||||
];
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('admin/health.title');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('admin/health.title');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return trans('admin/dashboard.advanced');
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view health');
|
||||
}
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('refresh')
|
||||
->label(trans('admin/health.refresh'))
|
||||
->button()
|
||||
->action('refresh'),
|
||||
];
|
||||
@ -34,7 +54,7 @@ class Health extends Page
|
||||
|
||||
protected function getViewData(): array
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$checkResults = app(ResultStore::class)->latestResults();
|
||||
|
||||
if ($checkResults === null) {
|
||||
@ -56,14 +76,14 @@ class Health extends Page
|
||||
$this->dispatch('refresh-component');
|
||||
|
||||
Notification::make()
|
||||
->title('Health check results refreshed')
|
||||
->title(trans('admin/health.results_refreshed'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$results = app(ResultStore::class)->latestResults();
|
||||
|
||||
if ($results === null) {
|
||||
@ -86,7 +106,7 @@ class Health extends Page
|
||||
|
||||
public static function getNavigationBadgeTooltip(): ?string
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$results = app(ResultStore::class)->latestResults();
|
||||
|
||||
if ($results === null) {
|
||||
@ -103,12 +123,12 @@ class Health extends Page
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
return 'Failed: ' . implode(', ', $failedNames);
|
||||
return trans('admin/health.checks.failed') . implode(', ', $failedNames);
|
||||
}
|
||||
|
||||
public static function getNavigationIcon(): string
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$results = app(ResultStore::class)->latestResults();
|
||||
|
||||
if ($results === null) {
|
||||
@ -117,4 +137,37 @@ class Health extends Page
|
||||
|
||||
return $results->containsFailingCheck() ? 'tabler-heart-exclamation' : 'tabler-heart-check';
|
||||
}
|
||||
|
||||
public function backgroundColor(string $str): string
|
||||
{
|
||||
return match ($str) {
|
||||
Status::ok()->value => 'bg-success-100 dark:bg-success-200',
|
||||
Status::warning()->value => 'bg-warning-100 dark:bg-warning-200',
|
||||
Status::skipped()->value => 'bg-info-100 dark:bg-info-200',
|
||||
Status::failed()->value, Status::crashed()->value => 'bg-danger-100 dark:bg-danger-200',
|
||||
default => 'bg-gray-100 dark:bg-gray-200'
|
||||
};
|
||||
}
|
||||
|
||||
public function iconColor(string $str): string
|
||||
{
|
||||
return match ($str) {
|
||||
Status::ok()->value => 'text-success-500 dark:text-success-600',
|
||||
Status::warning()->value => 'text-warning-500 dark:text-warning-600',
|
||||
Status::skipped()->value => 'text-info-500 dark:text-info-600',
|
||||
Status::failed()->value, Status::crashed()->value => 'text-danger-500 dark:text-danger-600',
|
||||
default => 'text-gray-500 dark:text-gray-600'
|
||||
};
|
||||
}
|
||||
|
||||
public function icon(string $str): string
|
||||
{
|
||||
return match ($str) {
|
||||
Status::ok()->value => 'tabler-circle-check',
|
||||
Status::warning()->value => 'tabler-exclamation-circle',
|
||||
Status::skipped()->value => 'tabler-circle-chevron-right',
|
||||
Status::failed()->value, Status::crashed()->value => 'tabler-circle-x',
|
||||
default => 'tabler-help-circle'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,20 @@
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\OAuth\Providers\OAuthProvider;
|
||||
use App\Models\Backup;
|
||||
use App\Notifications\MailTested;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
@ -27,10 +34,10 @@ use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Http\Client\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
@ -43,10 +50,9 @@ class Settings extends Page implements HasForms
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static string $view = 'filament.pages.settings';
|
||||
|
||||
/** @var array<mixed>|null */
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
@ -59,6 +65,16 @@ class Settings extends Page implements HasForms
|
||||
return auth()->user()->can('view settings');
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('admin/setting.title');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('admin/setting.title');
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
@ -68,104 +84,146 @@ class Settings extends Page implements HasForms
|
||||
->disabled(fn () => !auth()->user()->can('update settings'))
|
||||
->tabs([
|
||||
Tab::make('general')
|
||||
->label('General')
|
||||
->label(trans('admin/setting.navigation.general'))
|
||||
->icon('tabler-home')
|
||||
->schema($this->generalSettings()),
|
||||
Tab::make('captcha')
|
||||
->label('Captcha')
|
||||
->label(trans('admin/setting.navigation.captcha'))
|
||||
->icon('tabler-shield')
|
||||
->schema($this->captchaSettings())
|
||||
->columns(3),
|
||||
Tab::make('mail')
|
||||
->label('Mail')
|
||||
->label(trans('admin/setting.navigation.mail'))
|
||||
->icon('tabler-mail')
|
||||
->schema($this->mailSettings()),
|
||||
Tab::make('backup')
|
||||
->label('Backup')
|
||||
->label(trans('admin/setting.navigation.backup'))
|
||||
->icon('tabler-box')
|
||||
->schema($this->backupSettings()),
|
||||
Tab::make('OAuth')
|
||||
->label('OAuth')
|
||||
->label(trans('admin/setting.navigation.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->schema($this->oauthSettings()),
|
||||
Tab::make('misc')
|
||||
->label('Misc')
|
||||
->label(trans('admin/setting.navigation.misc'))
|
||||
->icon('tabler-tool')
|
||||
->schema($this->miscSettings()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return Component[] */
|
||||
private function generalSettings(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('APP_NAME')
|
||||
->label('App Name')
|
||||
->label(trans('admin/setting.general.app_name'))
|
||||
->required()
|
||||
->default(env('APP_NAME', 'Pelican')),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label('App Favicon')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.')
|
||||
->required()
|
||||
->default(env('APP_FAVICON', '/pelican.ico')),
|
||||
Toggle::make('APP_DEBUG')
|
||||
->label('Enable Debug Mode?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
|
||||
->default(env('APP_DEBUG', config('app.debug'))),
|
||||
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
|
||||
->label('Navigation')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Sidebar',
|
||||
true => 'Topbar',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
|
||||
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
|
||||
Group::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('APP_LOGO')
|
||||
->label(trans('admin/setting.general.app_logo'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('admin/setting.general.app_logo_help'))
|
||||
->default(env('APP_LOGO'))
|
||||
->placeholder('/pelican.svg'),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label(trans('admin/setting.general.app_favicon'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('admin/setting.general.app_favicon_help'))
|
||||
->required()
|
||||
->default(env('APP_FAVICON', '/pelican.ico'))
|
||||
->placeholder('/pelican.ico'),
|
||||
]),
|
||||
Group::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
Toggle::make('APP_DEBUG')
|
||||
->label(trans('admin/setting.general.debug_mode'))
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
|
||||
->default(env('APP_DEBUG', config('app.debug'))),
|
||||
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
|
||||
->label(trans('admin/setting.general.navigation'))
|
||||
->inline()
|
||||
->options([
|
||||
false => trans('admin/setting.general.sidebar'),
|
||||
true => trans('admin/setting.general.topbar'),
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
|
||||
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
|
||||
]),
|
||||
Group::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('FILAMENT_AVATAR_PROVIDER')
|
||||
->label(trans('admin/setting.general.avatar_provider'))
|
||||
->native(false)
|
||||
->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()]))
|
||||
->selectablePlaceholder(false)
|
||||
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
|
||||
Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
|
||||
->label(trans('admin/setting.general.uploadable_avatars'))
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->formatStateUsing(fn ($state) => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_UPLOADABLE_AVATARS', (bool) $state))
|
||||
->default(env('FILAMENT_UPLOADABLE_AVATARS', config('panel.filament.uploadable-avatars'))),
|
||||
]),
|
||||
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
|
||||
->label('Unit prefix')
|
||||
->label(trans('admin/setting.general.unit_prefix'))
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Decimal Prefix (MB/ GB)',
|
||||
true => 'Binary Prefix (MiB/ GiB)',
|
||||
false => trans('admin/setting.general.decimal_prefix'),
|
||||
true => trans('admin/setting.general.binary_prefix'),
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state))
|
||||
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
|
||||
ToggleButtons::make('APP_2FA_REQUIRED')
|
||||
->label('2FA Requirement')
|
||||
->label(trans('admin/setting.general.2fa_requirement'))
|
||||
->inline()
|
||||
->options([
|
||||
0 => 'Not required',
|
||||
1 => 'Required for only Admins',
|
||||
2 => 'Required for all Users',
|
||||
0 => trans('admin/setting.general.not_required'),
|
||||
1 => trans('admin/setting.general.admins_only'),
|
||||
2 => trans('admin/setting.general.all_users'),
|
||||
])
|
||||
->formatStateUsing(fn ($state): int => (int) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
|
||||
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
|
||||
Select::make('FILAMENT_WIDTH')
|
||||
->label(trans('admin/setting.general.display_width'))
|
||||
->native(false)
|
||||
->options(MaxWidth::class)
|
||||
->selectablePlaceholder(false)
|
||||
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
|
||||
TagsInput::make('TRUSTED_PROXIES')
|
||||
->label('Trusted Proxies')
|
||||
->label(trans('admin/setting.general.trusted_proxies'))
|
||||
->separator()
|
||||
->splitKeys(['Tab', ' '])
|
||||
->placeholder('New IP or IP Range')
|
||||
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
|
||||
->placeholder(trans('admin/setting.general.trusted_proxies_help'))
|
||||
->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies')))))
|
||||
->hintActions([
|
||||
FormAction::make('clear')
|
||||
->label('Clear')
|
||||
->label(trans('admin/setting.general.clear'))
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
||||
FormAction::make('cloudflare')
|
||||
->label('Set to Cloudflare IPs')
|
||||
->label(trans('admin/setting.general.set_to_cf'))
|
||||
->icon('tabler-brand-cloudflare')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(function (Factory $client, Set $set) {
|
||||
@ -190,63 +248,67 @@ class Settings extends Page implements HasForms
|
||||
$set('TRUSTED_PROXIES', $ips->values()->all());
|
||||
}),
|
||||
]),
|
||||
Select::make('FILAMENT_WIDTH')
|
||||
->label('Display Width')
|
||||
->native(false)
|
||||
->options(MaxWidth::class)
|
||||
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
private function captchaSettings(): array
|
||||
{
|
||||
return [
|
||||
Toggle::make('TURNSTILE_ENABLED')
|
||||
->label('Enable Turnstile Captcha?')
|
||||
->inline(false)
|
||||
->columnSpan(1)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
|
||||
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
|
||||
Placeholder::make('info')
|
||||
->columnSpan(2)
|
||||
->content(new HtmlString('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
|
||||
TextInput::make('TURNSTILE_SITE_KEY')
|
||||
->label('Site Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
|
||||
->placeholder('1x00000000000000000000AA'),
|
||||
TextInput::make('TURNSTILE_SECRET_KEY')
|
||||
->label('Secret Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key')))
|
||||
->placeholder('1x0000000000000000000000000000000AA'),
|
||||
Toggle::make('TURNSTILE_VERIFY_DOMAIN')
|
||||
->label('Verify domain?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state))
|
||||
->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))),
|
||||
];
|
||||
$formFields = [];
|
||||
|
||||
$captchaProviders = CaptchaProvider::get();
|
||||
foreach ($captchaProviders as $captchaProvider) {
|
||||
$id = Str::upper($captchaProvider->getId());
|
||||
$name = Str::title($captchaProvider->getId());
|
||||
|
||||
$formFields[] = Section::make($name)
|
||||
->columns(5)
|
||||
->icon($captchaProvider->getIcon() ?? 'tabler-shield')
|
||||
->collapsed(fn () => !env("CAPTCHA_{$id}_ENABLED", false))
|
||||
->collapsible()
|
||||
->schema([
|
||||
Hidden::make("CAPTCHA_{$id}_ENABLED")
|
||||
->live()
|
||||
->default(env("CAPTCHA_{$id}_ENABLED")),
|
||||
Actions::make([
|
||||
FormAction::make("disable_captcha_$id")
|
||||
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
|
||||
->label(trans('admin/setting.captcha.disable'))
|
||||
->color('danger')
|
||||
->action(function (Set $set) use ($id) {
|
||||
$set("CAPTCHA_{$id}_ENABLED", false);
|
||||
}),
|
||||
FormAction::make("enable_captcha_$id")
|
||||
->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED"))
|
||||
->label(trans('admin/setting.captcha.enable'))
|
||||
->color('success')
|
||||
->action(function (Set $set) use ($id, $captchaProviders) {
|
||||
foreach ($captchaProviders as $captchaProvider) {
|
||||
$loopId = Str::upper($captchaProvider->getId());
|
||||
$set("CAPTCHA_{$loopId}_ENABLED", $loopId === $id);
|
||||
}
|
||||
}),
|
||||
])->columnSpan(1),
|
||||
Group::make($captchaProvider->getSettingsForm())
|
||||
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
|
||||
->columns(4)
|
||||
->columnSpan(4),
|
||||
]);
|
||||
}
|
||||
|
||||
return $formFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
private function mailSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('MAIL_MAILER')
|
||||
->label('Mail Driver')
|
||||
->label(trans('admin/setting.mail.mail_driver'))
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
@ -261,96 +323,138 @@ class Settings extends Page implements HasForms
|
||||
->default(env('MAIL_MAILER', config('mail.default')))
|
||||
->hintAction(
|
||||
FormAction::make('test')
|
||||
->label('Send Test Mail')
|
||||
->label(trans('admin/setting.mail.test_mail'))
|
||||
->icon('tabler-send')
|
||||
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(function () {
|
||||
->action(function (Get $get) {
|
||||
// Store original mail configuration
|
||||
$originalConfig = [
|
||||
'mail.default' => config('mail.default'),
|
||||
'mail.mailers.smtp.host' => config('mail.mailers.smtp.host'),
|
||||
'mail.mailers.smtp.port' => config('mail.mailers.smtp.port'),
|
||||
'mail.mailers.smtp.username' => config('mail.mailers.smtp.username'),
|
||||
'mail.mailers.smtp.password' => config('mail.mailers.smtp.password'),
|
||||
'mail.mailers.smtp.scheme' => config('mail.mailers.smtp.scheme'),
|
||||
'mail.from.address' => config('mail.from.address'),
|
||||
'mail.from.name' => config('mail.from.name'),
|
||||
'services.mailgun.domain' => config('services.mailgun.domain'),
|
||||
'services.mailgun.secret' => config('services.mailgun.secret'),
|
||||
'services.mailgun.endpoint' => config('services.mailgun.endpoint'),
|
||||
];
|
||||
|
||||
try {
|
||||
// Update mail configuration dynamically
|
||||
config([
|
||||
'mail.default' => $get('MAIL_MAILER'),
|
||||
'mail.mailers.smtp.host' => $get('MAIL_HOST'),
|
||||
'mail.mailers.smtp.port' => $get('MAIL_PORT'),
|
||||
'mail.mailers.smtp.username' => $get('MAIL_USERNAME'),
|
||||
'mail.mailers.smtp.password' => $get('MAIL_PASSWORD'),
|
||||
'mail.mailers.smtp.scheme' => $get('MAIL_SCHEME'),
|
||||
'mail.from.address' => $get('MAIL_FROM_ADDRESS'),
|
||||
'mail.from.name' => $get('MAIL_FROM_NAME'),
|
||||
'services.mailgun.domain' => $get('MAILGUN_DOMAIN'),
|
||||
'services.mailgun.secret' => $get('MAILGUN_SECRET'),
|
||||
'services.mailgun.endpoint' => $get('MAILGUN_ENDPOINT'),
|
||||
]);
|
||||
|
||||
MailNotification::route('mail', auth()->user()->email)
|
||||
->notify(new MailTested(auth()->user()));
|
||||
|
||||
Notification::make()
|
||||
->title('Test Mail sent')
|
||||
->title(trans('admin/setting.mail.test_mail_sent'))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Test Mail failed')
|
||||
->title(trans('admin/setting.mail.test_mail_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
} finally {
|
||||
config($originalConfig);
|
||||
}
|
||||
})
|
||||
),
|
||||
Section::make('"From" Settings')
|
||||
->description('Set the Address and Name used as "From" in mails.')
|
||||
Section::make(trans('admin/setting.mail.from_settings'))
|
||||
->description(trans('admin/setting.mail.from_settings_help'))
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('MAIL_FROM_ADDRESS')
|
||||
->label('From Address')
|
||||
->label(trans('admin/setting.mail.from_address'))
|
||||
->required()
|
||||
->email()
|
||||
->default(env('MAIL_FROM_ADDRESS', config('mail.from.address'))),
|
||||
TextInput::make('MAIL_FROM_NAME')
|
||||
->label('From Name')
|
||||
->label(trans('admin/setting.mail.from_name'))
|
||||
->required()
|
||||
->default(env('MAIL_FROM_NAME', config('mail.from.name'))),
|
||||
]),
|
||||
Section::make('SMTP Configuration')
|
||||
Section::make(trans('admin/setting.mail.smtp.smtp_title'))
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
|
||||
->schema([
|
||||
TextInput::make('MAIL_HOST')
|
||||
->label('Host')
|
||||
->label(trans('admin/setting.mail.smtp.host'))
|
||||
->required()
|
||||
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
|
||||
TextInput::make('MAIL_PORT')
|
||||
->label('Port')
|
||||
->label(trans('admin/setting.mail.smtp.port'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
|
||||
TextInput::make('MAIL_USERNAME')
|
||||
->label('Username')
|
||||
->label(trans('admin/setting.mail.smtp.username'))
|
||||
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
|
||||
TextInput::make('MAIL_PASSWORD')
|
||||
->label('Password')
|
||||
->label(trans('admin/setting.mail.smtp.password'))
|
||||
->password()
|
||||
->revealable()
|
||||
->default(env('MAIL_PASSWORD')),
|
||||
ToggleButtons::make('MAIL_ENCRYPTION')
|
||||
->label('Encryption')
|
||||
ToggleButtons::make('MAIL_SCHEME')
|
||||
->label(trans('admin/setting.mail.smtp.scheme'))
|
||||
->inline()
|
||||
->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'])
|
||||
->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))),
|
||||
->options([
|
||||
'smtp' => 'SMTP',
|
||||
'smtps' => 'SMTPS',
|
||||
])
|
||||
->default(env('MAIL_SCHEME', config('mail.mailers.smtp.scheme')))
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$set('MAIL_PORT', $state === 'smtps' ? 587 : 2525);
|
||||
}),
|
||||
]),
|
||||
Section::make('Mailgun Configuration')
|
||||
Section::make(trans('admin/setting.mail.mailgun.mailgun_title'))
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
|
||||
->schema([
|
||||
TextInput::make('MAILGUN_DOMAIN')
|
||||
->label('Domain')
|
||||
->label(trans('admin/setting.mail.mailgun.domain'))
|
||||
->required()
|
||||
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
|
||||
TextInput::make('MAILGUN_SECRET')
|
||||
->label('Secret')
|
||||
->label(trans('admin/setting.mail.mailgun.secret'))
|
||||
->required()
|
||||
->default(env('MAILGUN_SECRET', config('services.mailgun.secret'))),
|
||||
TextInput::make('MAILGUN_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->label(trans('admin/setting.mail.mailgun.endpoint'))
|
||||
->required()
|
||||
->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
private function backupSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('APP_BACKUP_DRIVER')
|
||||
->label('Backup Driver')
|
||||
->label(trans('admin/setting.backup.backup_driver'))
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
@ -359,50 +463,50 @@ class Settings extends Page implements HasForms
|
||||
])
|
||||
->live()
|
||||
->default(env('APP_BACKUP_DRIVER', config('backups.default'))),
|
||||
Section::make('Throttles')
|
||||
->description('Configure how many backups can be created in a period. Set period to 0 to disable this throttle.')
|
||||
Section::make(trans('admin/setting.backup.throttle'))
|
||||
->description(trans('admin/setting.backup.throttle_help'))
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('BACKUP_THROTTLE_LIMIT')
|
||||
->label('Limit')
|
||||
->label(trans('admin/setting.backup.limit'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->default(config('backups.throttles.limit')),
|
||||
TextInput::make('BACKUP_THROTTLE_PERIOD')
|
||||
->label('Period')
|
||||
->label(trans('admin/setting.backup.period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->suffix('Seconds')
|
||||
->default(config('backups.throttles.period')),
|
||||
]),
|
||||
Section::make('S3 Configuration')
|
||||
Section::make(trans('admin/setting.backup.s3.s3_title'))
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3)
|
||||
->schema([
|
||||
TextInput::make('AWS_DEFAULT_REGION')
|
||||
->label('Default Region')
|
||||
->label(trans('admin/setting.backup.s3.default_region'))
|
||||
->required()
|
||||
->default(config('backups.disks.s3.region')),
|
||||
TextInput::make('AWS_ACCESS_KEY_ID')
|
||||
->label('Access Key ID')
|
||||
->label(trans('admin/setting.backup.s3.access_key'))
|
||||
->required()
|
||||
->default(config('backups.disks.s3.key')),
|
||||
TextInput::make('AWS_SECRET_ACCESS_KEY')
|
||||
->label('Secret Access Key')
|
||||
->label(trans('admin/setting.backup.s3.secret_key'))
|
||||
->required()
|
||||
->default(config('backups.disks.s3.secret')),
|
||||
TextInput::make('AWS_BACKUPS_BUCKET')
|
||||
->label('Bucket')
|
||||
->label(trans('admin/setting.backup.s3.bucket'))
|
||||
->required()
|
||||
->default(config('backups.disks.s3.bucket')),
|
||||
TextInput::make('AWS_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->label(trans('admin/setting.backup.s3.endpoint'))
|
||||
->required()
|
||||
->default(config('backups.disks.s3.endpoint')),
|
||||
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
|
||||
->label('Use path style endpoint?')
|
||||
->label(trans('admin/setting.backup.s3.use_path_style_endpoint'))
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
@ -416,85 +520,77 @@ class Settings extends Page implements HasForms
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
private function oauthSettings(): array
|
||||
{
|
||||
$oauthProviders = Config::get('auth.oauth');
|
||||
|
||||
$formFields = [];
|
||||
|
||||
foreach ($oauthProviders as $providerName => $providerConfig) {
|
||||
$providerEnvPrefix = strtoupper($providerName);
|
||||
$oauthProviders = OAuthProvider::get();
|
||||
foreach ($oauthProviders as $oauthProvider) {
|
||||
$id = Str::upper($oauthProvider->getId());
|
||||
$name = Str::title($oauthProvider->getId());
|
||||
|
||||
$fields = [
|
||||
Toggle::make("OAUTH_{$providerEnvPrefix}_ENABLED")
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->live()
|
||||
->columnSpan(1)
|
||||
->label('Enabled')
|
||||
->default(env("OAUTH_{$providerEnvPrefix}_ENABLED", false)),
|
||||
];
|
||||
|
||||
if (array_key_exists('client_id', $providerConfig['service'] ?? [])) {
|
||||
$fields[] = TextInput::make("OAUTH_{$providerEnvPrefix}_CLIENT_ID")
|
||||
->label('Client ID')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->hidden(fn (Get $get) => !$get("OAUTH_{$providerEnvPrefix}_ENABLED"))
|
||||
->default(env("OAUTH_{$providerEnvPrefix}_CLIENT_ID", $providerConfig['service']['client_id'] ?? ''))
|
||||
->placeholder('Client ID');
|
||||
}
|
||||
|
||||
if (array_key_exists('client_secret', $providerConfig['service'] ?? [])) {
|
||||
$fields[] = TextInput::make("OAUTH_{$providerEnvPrefix}_CLIENT_SECRET")
|
||||
->label('Client Secret')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete(false)
|
||||
->hidden(fn (Get $get) => !$get("OAUTH_{$providerEnvPrefix}_ENABLED"))
|
||||
->default(env("OAUTH_{$providerEnvPrefix}_CLIENT_SECRET", $providerConfig['service']['client_secret'] ?? ''))
|
||||
->placeholder('Client Secret');
|
||||
}
|
||||
|
||||
if (array_key_exists('base_url', $providerConfig['service'] ?? [])) {
|
||||
$fields[] = TextInput::make("OAUTH_{$providerEnvPrefix}_BASE_URL")
|
||||
->label('Base URL')
|
||||
->columnSpanFull()
|
||||
->autocomplete(false)
|
||||
->hidden(fn (Get $get) => !$get("OAUTH_{$providerEnvPrefix}_ENABLED"))
|
||||
->default(env("OAUTH_{$providerEnvPrefix}_BASE_URL", ''))
|
||||
->placeholder('Base URL');
|
||||
}
|
||||
|
||||
$formFields[] = Section::make(ucfirst($providerName))
|
||||
$formFields[] = Section::make($name)
|
||||
->columns(5)
|
||||
->icon($providerConfig['icon'] ?? 'tabler-brand-oauth')
|
||||
->collapsed(fn () => !env("OAUTH_{$providerEnvPrefix}_ENABLED", false))
|
||||
->icon($oauthProvider->getIcon() ?? 'tabler-brand-oauth')
|
||||
->collapsed(fn () => !env("OAUTH_{$id}_ENABLED", false))
|
||||
->collapsible()
|
||||
->schema($fields);
|
||||
->schema([
|
||||
Hidden::make("OAUTH_{$id}_ENABLED")
|
||||
->live()
|
||||
->default(env("OAUTH_{$id}_ENABLED")),
|
||||
Actions::make([
|
||||
FormAction::make("disable_oauth_$id")
|
||||
->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED"))
|
||||
->label(trans('admin/setting.oauth.disable'))
|
||||
->color('danger')
|
||||
->action(function (Set $set) use ($id) {
|
||||
$set("OAUTH_{$id}_ENABLED", false);
|
||||
}),
|
||||
FormAction::make("enable_oauth_$id")
|
||||
->visible(fn (Get $get) => !$get("OAUTH_{$id}_ENABLED"))
|
||||
->label(trans('admin/setting.oauth.enable'))
|
||||
->color('success')
|
||||
->steps($oauthProvider->getSetupSteps())
|
||||
->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $name)
|
||||
->modalSubmitActionLabel(trans('admin/setting.oauth.enable'))
|
||||
->modalCancelAction(false)
|
||||
->action(function ($data, Set $set) use ($id) {
|
||||
$data = array_merge([
|
||||
"OAUTH_{$id}_ENABLED" => 'true',
|
||||
], $data);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$set($key, $value);
|
||||
}
|
||||
}),
|
||||
])->columnSpan(1),
|
||||
Group::make($oauthProvider->getSettingsForm())
|
||||
->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED"))
|
||||
->columns(4)
|
||||
->columnSpan(4),
|
||||
]);
|
||||
}
|
||||
|
||||
return $formFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Component[]
|
||||
*/
|
||||
private function miscSettings(): array
|
||||
{
|
||||
return [
|
||||
Section::make('Automatic Allocation Creation')
|
||||
->description('Toggle if Users can create allocations via the client area.')
|
||||
Section::make(trans('admin/setting.misc.auto_allocation.title'))
|
||||
->description(trans('admin/setting.misc.auto_allocation.helper'))
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
|
||||
->label('Allow Users to create allocations?')
|
||||
->label(trans('admin/setting.misc.auto_allocation.question'))
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
@ -505,7 +601,7 @@ class Settings extends Page implements HasForms
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_CLIENT_ALLOCATIONS_ENABLED', (bool) $state))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
|
||||
->label('Starting Port')
|
||||
->label(trans('admin/setting.misc.auto_allocation.start'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
@ -513,7 +609,7 @@ class Settings extends Page implements HasForms
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_START')),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_END')
|
||||
->label('Ending Port')
|
||||
->label(trans('admin/setting.misc.auto_allocation.end'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
@ -521,14 +617,14 @@ class Settings extends Page implements HasForms
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_END')),
|
||||
]),
|
||||
Section::make('Mail Notifications')
|
||||
->description('Toggle which mail notifications should be sent to Users.')
|
||||
Section::make(trans('admin/setting.misc.mail_notifications.title'))
|
||||
->description(trans('admin/setting.misc.mail_notifications.helper'))
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
|
||||
->label('Server Installed')
|
||||
->label(trans('admin/setting.misc.mail_notifications.server_installed'))
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
@ -539,7 +635,7 @@ class Settings extends Page implements HasForms
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
|
||||
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
|
||||
->label('Server Reinstalled')
|
||||
->label(trans('admin/setting.misc.mail_notifications.server_reinstalled'))
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
@ -550,45 +646,45 @@ class Settings extends Page implements HasForms
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
]),
|
||||
Section::make('Connections')
|
||||
->description('Timeouts used when making requests.')
|
||||
Section::make(trans('admin/setting.misc.connections.title'))
|
||||
->description(trans('admin/setting.misc.connections.helper'))
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('GUZZLE_TIMEOUT')
|
||||
->label('Request Timeout')
|
||||
->label(trans('admin/setting.misc.connections.request_timeout'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(15)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->suffix(trans('admin/setting.misc.connections.seconds'))
|
||||
->default(env('GUZZLE_TIMEOUT', config('panel.guzzle.timeout'))),
|
||||
TextInput::make('GUZZLE_CONNECT_TIMEOUT')
|
||||
->label('Connect Timeout')
|
||||
->label(trans('admin/setting.misc.connections.connection_timeout'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(5)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->suffix(trans('admin/setting.misc.connections.seconds'))
|
||||
->default(env('GUZZLE_CONNECT_TIMEOUT', config('panel.guzzle.connect_timeout'))),
|
||||
]),
|
||||
Section::make('Activity Logs')
|
||||
->description('Configure how often old activity logs should be pruned and whether admin activities should be logged.')
|
||||
Section::make(trans('admin/setting.misc.activity_log.title'))
|
||||
->description(trans('admin/setting.misc.activity_log.helper'))
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_ACTIVITY_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->label(trans('admin/setting.misc.activity_log.prune_age'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->suffix(trans('admin/setting.misc.activity_log.days'))
|
||||
->default(env('APP_ACTIVITY_PRUNE_DAYS', config('activity.prune_days'))),
|
||||
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
|
||||
->label('Hide admin activities?')
|
||||
->label(trans('admin/setting.misc.activity_log.log_admin'))
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
@ -599,58 +695,65 @@ class Settings extends Page implements HasForms
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state))
|
||||
->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))),
|
||||
]),
|
||||
Section::make('API')
|
||||
->description('Defines the rate limit for the number of requests per minute that can be executed.')
|
||||
Section::make(trans('admin/setting.misc.api.title'))
|
||||
->description(trans('admin/setting.misc.api.helper'))
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_API_CLIENT_RATELIMIT')
|
||||
->label('Client API Rate Limit')
|
||||
->label(trans('admin/setting.misc.api.client_rate'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->suffix(trans('admin/setting.misc.api.rpm'))
|
||||
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
|
||||
TextInput::make('APP_API_APPLICATION_RATELIMIT')
|
||||
->label('Application API Rate Limit')
|
||||
->label(trans('admin/setting.misc.api.app_rate'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->suffix(trans('admin/setting.misc.api.rpm'))
|
||||
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
|
||||
]),
|
||||
Section::make('Server')
|
||||
->description('Settings for Servers.')
|
||||
Section::make(trans('admin/setting.misc.server.title'))
|
||||
->description(trans('admin/setting.misc.server.helper'))
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_EDITABLE_SERVER_DESCRIPTIONS')
|
||||
->label('Allow Users to edit Server Descriptions?')
|
||||
->label(trans('admin/setting.misc.server.edit_server_desc'))
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->columnSpan(1)
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
|
||||
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
|
||||
FileUpload::make('ConsoleFonts')
|
||||
->hint(trans('admin/setting.misc.server.console_font_hint'))
|
||||
->label(trans('admin/setting.misc.server.console_font_upload'))
|
||||
->directory('fonts')
|
||||
->columnSpan(1)
|
||||
->maxFiles(1)
|
||||
->preserveFilenames(),
|
||||
]),
|
||||
Section::make('Webhook')
|
||||
->description('Configure how often old webhook logs should be pruned.')
|
||||
Section::make(trans('admin/setting.misc.webhook.title'))
|
||||
->description(trans('admin/setting.misc.webhook.helper'))
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_WEBHOOK_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->label(trans('admin/setting.misc.webhook.prune_age'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->suffix(trans('admin/setting.misc.webhook.days'))
|
||||
->default(env('APP_WEBHOOK_PRUNE_DAYS', config('panel.webhook.prune_days'))),
|
||||
]),
|
||||
];
|
||||
@ -665,6 +768,7 @@ class Settings extends Page implements HasForms
|
||||
{
|
||||
try {
|
||||
$data = $this->form->getState();
|
||||
unset($data['ConsoleFonts']);
|
||||
|
||||
// Convert bools to a string, so they are correctly written to the .env file
|
||||
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data);
|
||||
@ -677,12 +781,12 @@ class Settings extends Page implements HasForms
|
||||
$this->redirect($this->getUrl());
|
||||
|
||||
Notification::make()
|
||||
->title('Settings saved')
|
||||
->title(trans('admin/setting.save_success'))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Save failed')
|
||||
->title(trans('admin/setting.save_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
@ -3,26 +3,143 @@
|
||||
namespace App\Filament\Admin\Resources;
|
||||
|
||||
use App\Filament\Admin\Resources\ApiKeyResource\Pages;
|
||||
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ApiKeyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ApiKey::class;
|
||||
|
||||
protected static ?string $modelLabel = 'Application API Key';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Application API Keys';
|
||||
|
||||
protected static ?string $navigationLabel = 'API Keys';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-key';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('admin/apikey.nav_title');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return trans('admin/apikey.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return trans('admin/apikey.model_label_plural');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::where('key_type', ApiKey::TYPE_APPLICATION)->count() ?: null;
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->where('key_type', ApiKey::TYPE_APPLICATION);
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return trans('admin/dashboard.advanced');
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('key')
|
||||
->label(trans('admin/apikey.table.key'))
|
||||
->icon('tabler-clipboard-text')
|
||||
->state(fn (ApiKey $key) => $key->identifier . $key->token)
|
||||
->copyable(),
|
||||
TextColumn::make('memo')
|
||||
->label(trans('admin/apikey.table.description'))
|
||||
->wrap()
|
||||
->limit(50),
|
||||
DateTimeColumn::make('last_used_at')
|
||||
->label(trans('admin/apikey.table.last_used'))
|
||||
->placeholder(trans('admin/apikey.table.never_used'))
|
||||
->sortable(),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label(trans('admin/apikey.table.created'))
|
||||
->sortable(),
|
||||
TextColumn::make('user.username')
|
||||
->label(trans('admin/apikey.table.created_by'))
|
||||
->icon('tabler-user')
|
||||
->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-key')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading(trans('admin/apikey.empty_table'))
|
||||
->emptyStateActions([
|
||||
CreateAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Fieldset::make('Permissions')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
])
|
||||
->schema(
|
||||
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
|
||||
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||
->options([
|
||||
0 => trans('admin/apikey.permissions.none'),
|
||||
1 => trans('admin/apikey.permissions.read'),
|
||||
3 => trans('admin/apikey.permissions.read_write'),
|
||||
])
|
||||
->icons([
|
||||
0 => 'tabler-book-off',
|
||||
1 => 'tabler-book',
|
||||
3 => 'tabler-writing',
|
||||
])
|
||||
->colors([
|
||||
0 => 'success',
|
||||
1 => 'warning',
|
||||
3 => 'danger',
|
||||
])
|
||||
->required()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
])
|
||||
->default(0),
|
||||
)->all(),
|
||||
),
|
||||
TagsInput::make('allowed_ips')
|
||||
->placeholder(trans('admin/apikey.whitelist_placeholder'))
|
||||
->label(trans('admin/apikey.whitelist'))
|
||||
->helperText(trans('admin/apikey.whitelist_help'))
|
||||
->columnSpanFull(),
|
||||
Textarea::make('memo')
|
||||
->required()
|
||||
->label(trans('admin/apikey.description'))
|
||||
->helperText(trans('admin/apikey.description_help'))
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
|
@ -4,12 +4,6 @@ namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\ApiKeyResource;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@ -31,78 +25,13 @@ class CreateApiKey extends CreateRecord
|
||||
return [];
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
|
||||
Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
|
||||
|
||||
Hidden::make('user_id')
|
||||
->default(auth()->user()->id)
|
||||
->required(),
|
||||
|
||||
Hidden::make('key_type')
|
||||
->inlineLabel()
|
||||
->default(ApiKey::TYPE_APPLICATION)
|
||||
->required(),
|
||||
|
||||
Fieldset::make('Permissions')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
])
|
||||
->schema(
|
||||
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
|
||||
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||
->options([
|
||||
0 => 'None',
|
||||
1 => 'Read',
|
||||
// 2 => 'Write',
|
||||
3 => 'Read & Write',
|
||||
])
|
||||
->icons([
|
||||
0 => 'tabler-book-off',
|
||||
1 => 'tabler-book',
|
||||
2 => 'tabler-writing',
|
||||
3 => 'tabler-writing',
|
||||
])
|
||||
->colors([
|
||||
0 => 'success',
|
||||
1 => 'warning',
|
||||
2 => 'danger',
|
||||
3 => 'danger',
|
||||
])
|
||||
->required()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
])
|
||||
->default(0),
|
||||
)->all(),
|
||||
),
|
||||
|
||||
TagsInput::make('allowed_ips')
|
||||
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
||||
->label('Whitelisted IPv4 Addresses')
|
||||
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
|
||||
->columnSpanFull(),
|
||||
|
||||
Textarea::make('memo')
|
||||
->required()
|
||||
->label('Description')
|
||||
->helperText('
|
||||
Once you have assigned permissions and created this set of credentials you will be unable to come back and edit it.
|
||||
If you need to make changes down the road you will need to create a new set of credentials.
|
||||
')
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$data['identifier'] = ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION);
|
||||
$data['token'] = str_random(ApiKey::KEY_LENGTH);
|
||||
$data['user_id'] = auth()->user()->id;
|
||||
$data['key_type'] = ApiKey::TYPE_APPLICATION;
|
||||
|
||||
$permissions = [];
|
||||
|
||||
foreach (ApiKey::getPermissionList() as $permission) {
|
||||
|
@ -3,70 +3,18 @@
|
||||
namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\ApiKeyResource;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListApiKeys extends ListRecords
|
||||
{
|
||||
protected static string $resource = ApiKeyResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->searchable(false)
|
||||
->modifyQueryUsing(fn ($query) => $query->where('key_type', ApiKey::TYPE_APPLICATION))
|
||||
->columns([
|
||||
TextColumn::make('key')
|
||||
->copyable()
|
||||
->icon('tabler-clipboard-text')
|
||||
->state(fn (ApiKey $key) => $key->identifier . $key->token),
|
||||
|
||||
TextColumn::make('memo')
|
||||
->label('Description')
|
||||
->wrap()
|
||||
->limit(50),
|
||||
|
||||
TextColumn::make('identifier')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
|
||||
DateTimeColumn::make('last_used_at')
|
||||
->label('Last Used')
|
||||
->placeholder('Not Used')
|
||||
->sortable(),
|
||||
|
||||
DateTimeColumn::make('created_at')
|
||||
->label('Created')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('user.username')
|
||||
->label('Created By')
|
||||
->url(fn (ApiKey $apiKey): string => route('filament.admin.resources.users.edit', ['record' => $apiKey->user])),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-key')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No API Keys')
|
||||
->emptyStateActions([
|
||||
CreateAction::make('create')
|
||||
->label('Create API Key')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->label('Create API Key')
|
||||
CreateAction::make()
|
||||
->hidden(fn () => ApiKey::where('key_type', ApiKey::TYPE_APPLICATION)->count() <= 0),
|
||||
];
|
||||
}
|
||||
|
@ -4,7 +4,19 @@ namespace App\Filament\Admin\Resources;
|
||||
|
||||
use App\Filament\Admin\Resources\DatabaseHostResource\Pages;
|
||||
use App\Models\DatabaseHost;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DatabaseHostResource extends Resource
|
||||
{
|
||||
@ -12,13 +24,130 @@ class DatabaseHostResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('admin/databasehost.nav_title');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return trans('admin/databasehost.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return trans('admin/databasehost.model_label_plural');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return trans('admin/dashboard.advanced');
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(trans('admin/databasehost.table.name')),
|
||||
TextColumn::make('host')
|
||||
->label(trans('admin/databasehost.table.host')),
|
||||
TextColumn::make('port')
|
||||
->label(trans('admin/databasehost.table.port')),
|
||||
TextColumn::make('username')
|
||||
->label(trans('admin/databasehost.table.username')),
|
||||
TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
->icon('tabler-database')
|
||||
->label(trans('admin/databasehost.databases')),
|
||||
TextColumn::make('nodes.name')
|
||||
->icon('tabler-server-2')
|
||||
->badge()
|
||||
->placeholder(trans('admin/databasehost.no_nodes')),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->hidden(fn ($record) => static::canEdit($record)),
|
||||
EditAction::make(),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-database')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading(trans('admin/databasehost.no_database_hosts'))
|
||||
->emptyStateActions([
|
||||
CreateAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->label(trans('admin/databasehost.host'))
|
||||
->helperText(trans('admin/databasehost.host_help'))
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('name', $state))
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/databasehost.port'))
|
||||
->helperText(trans('admin/databasehost.port_help'))
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
TextInput::make('max_databases')
|
||||
->label(trans('admin/databasehost.max_database'))
|
||||
->helpertext(trans('admin/databasehost.max_databases_help'))
|
||||
->numeric(),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/databasehost.display_name'))
|
||||
->helperText(trans('admin/databasehost.display_name_help'))
|
||||
->required()
|
||||
->maxLength(60),
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/databasehost.username'))
|
||||
->helperText(trans('admin/databasehost.username_help'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->label(trans('admin/databasehost.password'))
|
||||
->helperText(trans('admin/databasehost.password_help'))
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255)
|
||||
->required(fn ($operation) => $operation === 'create'),
|
||||
Select::make('node_ids')
|
||||
->multiple()
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText(trans('admin/databasehost.linked_nodes_help'))
|
||||
->label(trans('admin/databasehost.linked_nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@ -26,7 +155,19 @@ class DatabaseHostResource extends Resource
|
||||
return [
|
||||
'index' => Pages\ListDatabaseHosts::route('/'),
|
||||
'create' => Pages\CreateDatabaseHost::route('/create'),
|
||||
'view' => Pages\ViewDatabaseHost::route('/{record}'),
|
||||
'edit' => Pages\EditDatabaseHost::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->where(function (Builder $query) {
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,19 +4,30 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\DatabaseHostResource;
|
||||
use App\Services\Databases\Hosts\HostCreationService;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
use PDOException;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
use HasWizard;
|
||||
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
@ -28,82 +39,125 @@ class CreateDatabaseHost extends CreateRecord
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
Select::make('node_ids')
|
||||
->multiple()
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Nodes')
|
||||
->relationship('nodes', 'name'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
/** @return Step[] */
|
||||
public function getSteps(): array
|
||||
{
|
||||
return [
|
||||
$this->getCreateFormAction()->formId('form'),
|
||||
Step::make(trans('admin/databasehost.setup.preparations'))
|
||||
->columns()
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(trans('admin/databasehost.setup.note')),
|
||||
Toggle::make('different_server')
|
||||
->label(new HtmlString(trans('admin/databasehost.setup.different_server')))
|
||||
->dehydrated(false)
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn ($state, Set $set) => $state ? $set('panel_ip', gethostbyname(str(config('app.url'))->replace(['http:', 'https:', '/'], ''))) : '127.0.0.1'),
|
||||
Hidden::make('panel_ip')
|
||||
->default('127.0.0.1')
|
||||
->dehydrated(false),
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/databasehost.username'))
|
||||
->helperText(trans('admin/databasehost.username_help'))
|
||||
->required()
|
||||
->default('pelicanuser')
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->label(trans('admin/databasehost.password'))
|
||||
->helperText(trans('admin/databasehost.password_help'))
|
||||
->required()
|
||||
->default(Str::password(16))
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255),
|
||||
])
|
||||
->afterValidation(function (Get $get, Set $set) {
|
||||
$set('create_user', "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';");
|
||||
$set('assign_permissions', "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;");
|
||||
}),
|
||||
Step::make(trans('admin/databasehost.setup.database_setup'))
|
||||
->schema([
|
||||
Fieldset::make(trans('admin/databasehost.setup.database_user'))
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(trans('admin/databasehost.setup.cli_login')))
|
||||
->columnSpanFull(),
|
||||
TextInput::make('create_user')
|
||||
->label(trans('admin/databasehost.setup.command_create_user'))
|
||||
->default(fn (Get $get) => "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';")
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->columnSpanFull(),
|
||||
TextInput::make('assign_permissions')
|
||||
->label(trans('admin/databasehost.setup.command_assign_permissions'))
|
||||
->default(fn (Get $get) => "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;")
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->columnSpanFull(),
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(trans('admin/databasehost.setup.cli_exit')))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Fieldset::make(trans('admin/databasehost.setup.external_access'))
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(trans('admin/databasehost.setup.allow_external_access')))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Step::make(trans('admin/databasehost.setup.panel_setup'))
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->label(trans('admin/databasehost.host'))
|
||||
->helperText(trans('admin/databasehost.host_help'))
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('name', $state))
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->label(trans('admin/databasehost.port'))
|
||||
->helperText(trans('admin/databasehost.port_help'))
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
TextInput::make('max_databases')
|
||||
->label(trans('admin/databasehost.max_database'))
|
||||
->helpertext(trans('admin/databasehost.max_databases_help'))
|
||||
->placeholder(trans('admin/databasehost.unlimited'))
|
||||
->numeric(),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/databasehost.display_name'))
|
||||
->helperText(trans('admin/databasehost.display_name_help'))
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Select::make('node_ids')
|
||||
->multiple()
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText(trans('admin/databasehost.linked_nodes_help'))
|
||||
->label(trans('admin/databasehost.linked_nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
try {
|
||||
return $this->service->handle($data);
|
||||
} catch (PDOException $exception) {
|
||||
Notification::make()
|
||||
->title('Error connecting to database host')
|
||||
->title(trans('admin/databasehost.error'))
|
||||
->body($exception->getMessage())
|
||||
->color('danger')
|
||||
->icon('tabler-database')
|
||||
|
@ -6,12 +6,7 @@ use App\Filament\Admin\Resources\DatabaseHostResource;
|
||||
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Services\Databases\Hosts\HostUpdateService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
@ -29,66 +24,11 @@ class EditDatabaseHost extends EditRecord
|
||||
$this->hostUpdateService = $hostUpdateService;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255),
|
||||
Select::make('nodes')
|
||||
->multiple()
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Nodes')
|
||||
->relationship('nodes', 'name'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? 'Database Host Has Databases' : 'Delete')
|
||||
DeleteAction::make()
|
||||
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? trans('admin/databasehost.delete_help') : trans('filament-actions::delete.single.modal.actions.delete.label'))
|
||||
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
@ -120,7 +60,7 @@ class EditDatabaseHost extends EditRecord
|
||||
return $this->hostUpdateService->handle($record, $data);
|
||||
} catch (PDOException $exception) {
|
||||
Notification::make()
|
||||
->title('Error connecting to database host')
|
||||
->title(trans('admin/databasehost.error'))
|
||||
->body($exception->getMessage())
|
||||
->color('danger')
|
||||
->icon('tabler-database')
|
||||
|
@ -4,69 +4,17 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\DatabaseHostResource;
|
||||
use App\Models\DatabaseHost;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListDatabaseHosts extends ListRecords
|
||||
{
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected ?string $heading = 'Database Hosts';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('host')
|
||||
->searchable(),
|
||||
TextColumn::make('port')
|
||||
->sortable(),
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
->icon('tabler-database')
|
||||
->label('Databases'),
|
||||
TextColumn::make('nodes.name')
|
||||
->icon('tabler-server-2')
|
||||
->badge()
|
||||
->placeholder('No Nodes')
|
||||
->sortable(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete databasehost')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-database')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Database Hosts')
|
||||
->emptyStateActions([
|
||||
CreateAction::make('create')
|
||||
->label('Create Database Host')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make('create')
|
||||
->label('Create Database Host')
|
||||
CreateAction::make()
|
||||
->hidden(fn () => DatabaseHost::count() <= 0),
|
||||
];
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\DatabaseHostResource;
|
||||
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewDatabaseHost extends ViewRecord
|
||||
{
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) {
|
||||
return [
|
||||
DatabasesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@ -23,19 +23,22 @@ class DatabasesRelationManager extends RelationManager
|
||||
->schema([
|
||||
TextInput::make('database')
|
||||
->columnSpanFull(),
|
||||
TextInput::make('username'),
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/databasehost.table.username')),
|
||||
TextInput::make('password')
|
||||
->label(trans('admin/databasehost.table.password'))
|
||||
->password()
|
||||
->revealable()
|
||||
->hintAction(RotateDatabasePasswordAction::make())
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')
|
||||
->label('Connections From')
|
||||
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote),
|
||||
->label(trans('admin/databasehost.table.remote'))
|
||||
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
|
||||
TextInput::make('max_connections')
|
||||
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections),
|
||||
->label(trans('admin/databasehost.table.max_connections'))
|
||||
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
|
||||
TextInput::make('jdbc')
|
||||
->label('JDBC Connection String')
|
||||
->label(trans('admin/databasehost.table.connection_string'))
|
||||
->columnSpanFull()
|
||||
->password()
|
||||
->revealable()
|
||||
@ -47,26 +50,31 @@ class DatabasesRelationManager extends RelationManager
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->heading('')
|
||||
->columns([
|
||||
TextColumn::make('database')
|
||||
->icon('tabler-database'),
|
||||
TextColumn::make('username')
|
||||
->label(trans('admin/databasehost.table.username'))
|
||||
->icon('tabler-user'),
|
||||
TextColumn::make('remote')
|
||||
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote),
|
||||
->label(trans('admin/databasehost.table.remote'))
|
||||
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
|
||||
TextColumn::make('server.name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
|
||||
TextColumn::make('max_connections')
|
||||
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections),
|
||||
DateTimeColumn::make('created_at'),
|
||||
->label(trans('admin/databasehost.table.max_connections'))
|
||||
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label(trans('admin/databasehost.table.created_at')),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete', $database)),
|
||||
ViewAction::make()
|
||||
->color('primary')
|
||||
->hidden(fn () => !auth()->user()->can('viewList database')),
|
||||
->hidden(fn () => !auth()->user()->can('viewAny', Database::class)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,26 @@ class EggResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('admin/egg.nav_title');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return trans('admin/egg.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return trans('admin/egg.model_label_plural');
|
||||
}
|
||||
|
||||
public static function getGloballySearchableAttributes(): array
|
||||
{
|
||||
return ['name', 'tags', 'uuid', 'id'];
|
||||
|
@ -4,6 +4,8 @@ namespace App\Filament\Admin\Resources\EggResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Filament\Admin\Resources\EggResource;
|
||||
use App\Filament\Components\Forms\Fields\CopyFrom;
|
||||
use App\Models\EggVariable;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
@ -17,10 +19,12 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
class CreateEgg extends CreateRecord
|
||||
{
|
||||
@ -45,97 +49,101 @@ class CreateEgg extends CreateRecord
|
||||
return $form
|
||||
->schema([
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('Configuration')
|
||||
Tab::make(trans('admin/egg.tabs.configuration'))
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||
->helperText(trans('admin/egg.name_help')),
|
||||
TextInput::make('author')
|
||||
->label(trans('admin/egg.author'))
|
||||
->maxLength(255)
|
||||
->required()
|
||||
->email()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('The author of this version of the Egg.'),
|
||||
->helperText(trans('admin/egg.author_help')),
|
||||
Textarea::make('description')
|
||||
->rows(3)
|
||||
->label(trans('admin/egg.description'))
|
||||
->rows(2)
|
||||
->columnSpanFull()
|
||||
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
|
||||
->helperText(trans('admin/egg.description_help')),
|
||||
Textarea::make('startup')
|
||||
->label(trans('admin/egg.startup'))
|
||||
->rows(3)
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->placeholder(implode("\n", [
|
||||
'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}',
|
||||
]))
|
||||
->helperText('The default startup command that should be used for new servers using this Egg.'),
|
||||
TagsInput::make('features')
|
||||
->placeholder('Add Feature')
|
||||
->helperText('')
|
||||
->helperText(trans('admin/egg.startup_help')),
|
||||
TagsInput::make('file_denylist')
|
||||
->label(trans('admin/egg.file_denylist'))
|
||||
->placeholder('denied-file.txt')
|
||||
->helperText(trans('admin/egg.file_denylist_help'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TagsInput::make('features')
|
||||
->label(trans('admin/egg.features'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->label(trans('admin/egg.force_ip'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||
->hintIconTooltip(trans('admin/egg.force_ip_help')),
|
||||
Hidden::make('script_is_privileged')
|
||||
->default(1),
|
||||
TagsInput::make('tags')
|
||||
->placeholder('Add Tags')
|
||||
->helperText('')
|
||||
->label(trans('admin/egg.tags'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->label(trans('admin/egg.update_url'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->hintIconTooltip(trans('admin/egg.update_url_help'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->url(),
|
||||
KeyValue::make('docker_images')
|
||||
->label(trans('admin/egg.docker_images'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel('Add Image')
|
||||
->keyLabel('Name')
|
||||
->addActionLabel(trans('admin/egg.add_image'))
|
||||
->keyLabel(trans('admin/egg.docker_name'))
|
||||
->keyPlaceholder('Java 21')
|
||||
->valueLabel('Image URI')
|
||||
->valueLabel(trans('admin/egg.docker_uri'))
|
||||
->valuePlaceholder('ghcr.io/parkervcp/yolks:java_21')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
->helperText(trans('admin/egg.docker_help')),
|
||||
]),
|
||||
|
||||
Tab::make('Process Management')
|
||||
Tab::make(trans('admin/egg.tabs.process_management'))
|
||||
->columns()
|
||||
->schema([
|
||||
Hidden::make('config_from')
|
||||
->default(null)
|
||||
->label('Copy Settings From')
|
||||
// ->placeholder('None')
|
||||
// ->relationship('configFrom', 'name', ignoreRecord: true)
|
||||
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
|
||||
CopyFrom::make('copy_process_from')
|
||||
->process(),
|
||||
TextInput::make('config_stop')
|
||||
->label(trans('admin/egg.stop_command'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->label('Stop Command')
|
||||
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
|
||||
->helperText(trans('admin/egg.stop_command_help')),
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
->label('Start Configuration')
|
||||
->label(trans('admin/egg.start_config'))
|
||||
->default('{}')
|
||||
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
|
||||
->helperText(trans('admin/egg.start_config_help')),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label('Configuration Files')
|
||||
->label(trans('admin/egg.config_files'))
|
||||
->default('{}')
|
||||
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
|
||||
->helperText(trans('admin/egg.config_files_help')),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label('Log Configuration')
|
||||
->label(trans('admin/egg.log_config'))
|
||||
->default('{}')
|
||||
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
|
||||
->helperText(trans('admin/egg.log_config_help')),
|
||||
]),
|
||||
Tab::make('Egg Variables')
|
||||
Tab::make(trans('admin/egg.tabs.egg_variables'))
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
->label('')
|
||||
->addActionLabel('Add New Egg Variable')
|
||||
->addActionLabel(trans('admin/egg.add_new_variable'))
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
->name('name')
|
||||
@ -164,31 +172,42 @@ class CreateEgg extends CreateRecord
|
||||
})
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')), ignoreRecord: true)
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
])
|
||||
->required(),
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->label(trans('admin/egg.environment_variable'))
|
||||
->maxLength(255)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code')
|
||||
->hintIconTooltip(fn ($state) => "{{{$state}}}")
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')), ignoreRecord: true)
|
||||
->rules(EggVariable::getRulesForField('env_variable'))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
'required' => trans('admin/egg.error_required'),
|
||||
'*' => trans('admin/egg.error_reserved'),
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('default_value')->maxLength(255),
|
||||
Fieldset::make('User Permissions')
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255),
|
||||
Fieldset::make(trans('admin/egg.user_permissions'))
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
|
||||
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
|
||||
]),
|
||||
TagsInput::make('rules')
|
||||
->label(trans('admin/egg.rules'))
|
||||
->columnSpanFull()
|
||||
->placeholder('Add Rule')
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
@ -212,26 +231,25 @@ class CreateEgg extends CreateRecord
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
Tab::make(trans('admin/egg.tabs.install_script'))
|
||||
->columns(3)
|
||||
->schema([
|
||||
|
||||
Hidden::make('copy_script_from'),
|
||||
//->placeholder('None')
|
||||
//->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||
|
||||
CopyFrom::make('copy_script_from')
|
||||
->script(),
|
||||
TextInput::make('script_container')
|
||||
->label(trans('admin/egg.script_container'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('ghcr.io/pelican-eggs/installers:debian'),
|
||||
|
||||
Select::make('script_entry')
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->default('bash')
|
||||
->options(['bash', 'ash', '/bin/bash'])
|
||||
->required(),
|
||||
|
||||
MonacoEditor::make('script_install')
|
||||
->label(trans('admin/egg.script_install'))
|
||||
->columnSpanFull()
|
||||
->fontSize('16px')
|
||||
->language('shell')
|
||||
|
@ -7,7 +7,9 @@ use App\Filament\Admin\Resources\EggResource;
|
||||
use App\Filament\Admin\Resources\EggResource\RelationManagers\ServersRelationManager;
|
||||
use App\Filament\Components\Actions\ExportEggAction;
|
||||
use App\Filament\Components\Actions\ImportEggAction;
|
||||
use App\Filament\Components\Forms\Fields\CopyFrom;
|
||||
use App\Models\Egg;
|
||||
use App\Models\EggVariable;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
@ -22,8 +24,10 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
class EditEgg extends EditRecord
|
||||
{
|
||||
@ -34,99 +38,98 @@ class EditEgg extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('Configuration')
|
||||
Tab::make(trans('admin/egg.tabs.configuration'))
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->icon('tabler-egg')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||
->helperText(trans('admin/egg.name_help')),
|
||||
TextInput::make('uuid')
|
||||
->label('Egg UUID')
|
||||
->label(trans('admin/egg.egg_uuid'))
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'),
|
||||
->helperText(trans('admin/egg.uuid_help')),
|
||||
TextInput::make('id')
|
||||
->label('Egg ID')
|
||||
->label(trans('admin/egg.egg_id'))
|
||||
->disabled(),
|
||||
Textarea::make('description')
|
||||
->label(trans('admin/egg.description'))
|
||||
->rows(3)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
|
||||
->helperText(trans('admin/egg.description_help')),
|
||||
TextInput::make('author')
|
||||
->label(trans('admin/egg.author'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->email()
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'),
|
||||
->helperText(trans('admin/egg.author_help_edit')),
|
||||
Textarea::make('startup')
|
||||
->rows(2)
|
||||
->label(trans('admin/egg.startup'))
|
||||
->rows(3)
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->helperText('The default startup command that should be used for new servers using this Egg.'),
|
||||
->helperText(trans('admin/egg.startup_help')),
|
||||
TagsInput::make('file_denylist')
|
||||
->hidden() // latest wings breaks it.
|
||||
->label(trans('admin/egg.file_denylist'))
|
||||
->placeholder('denied-file.txt')
|
||||
->helperText('A list of files that the end user is not allowed to edit.')
|
||||
->helperText(trans('admin/egg.file_denylist_help'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TagsInput::make('features')
|
||||
->placeholder('Add Feature')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
->label(trans('admin/egg.features'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->inline(false)
|
||||
->label(trans('admin/egg.force_ip'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||
->hintIconTooltip(trans('admin/egg.force_ip_help')),
|
||||
Hidden::make('script_is_privileged')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
TagsInput::make('tags')
|
||||
->placeholder('Add Tags')
|
||||
->helperText('')
|
||||
->label(trans('admin/egg.tags'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->label('Update URL')
|
||||
->label(trans('admin/egg.update_url'))
|
||||
->url()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->hintIconTooltip(trans('admin/egg.update_url_help'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
KeyValue::make('docker_images')
|
||||
->label(trans('admin/egg.docker_images'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel('Add Image')
|
||||
->keyLabel('Name')
|
||||
->valueLabel('Image URI')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
->addActionLabel(trans('admin/egg.add_image'))
|
||||
->keyLabel(trans('admin/egg.docker_name'))
|
||||
->valueLabel(trans('admin/egg.docker_uri'))
|
||||
->helperText(trans('admin/egg.docker_help')),
|
||||
]),
|
||||
Tab::make('Process Management')
|
||||
Tab::make(trans('admin/egg.tabs.process_management'))
|
||||
->columns()
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
Select::make('config_from')
|
||||
->label('Copy Settings From')
|
||||
->placeholder('None')
|
||||
->relationship('configFrom', 'name', ignoreRecord: true)
|
||||
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
|
||||
CopyFrom::make('copy_process_from')
|
||||
->process(),
|
||||
TextInput::make('config_stop')
|
||||
->label(trans('admin/egg.stop_command'))
|
||||
->maxLength(255)
|
||||
->label('Stop Command')
|
||||
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
|
||||
->helperText(trans('admin/egg.stop_command_help')),
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
->label('Start Configuration')
|
||||
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
|
||||
->label(trans('admin/egg.start_config'))
|
||||
->helperText(trans('admin/egg.start_config_help')),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label('Configuration Files')
|
||||
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
|
||||
->label(trans('admin/egg.config_files'))
|
||||
->helperText(trans('admin/egg.config_files_help')),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label('Log Configuration')
|
||||
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
|
||||
->label(trans('admin/egg.log_config'))
|
||||
->helperText(trans('admin/egg.log_config_help')),
|
||||
]),
|
||||
Tab::make('Egg Variables')
|
||||
Tab::make(trans('admin/egg.tabs.egg_variables'))
|
||||
->columnSpanFull()
|
||||
->icon('tabler-variable')
|
||||
->schema([
|
||||
@ -138,7 +141,7 @@ class EditEgg extends EditRecord
|
||||
->reorderable()
|
||||
->collapsible()->collapsed()
|
||||
->orderColumn()
|
||||
->addActionLabel('New Variable')
|
||||
->addActionLabel(trans('admin/egg.add_new_variable'))
|
||||
->itemLabel(fn (array $state) => $state['name'])
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
@ -160,31 +163,42 @@ class EditEgg extends EditRecord
|
||||
})
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')), ignoreRecord: true)
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
])
|
||||
->required(),
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->label(trans('admin/egg.environment_variable'))
|
||||
->maxLength(255)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code')
|
||||
->hintIconTooltip(fn ($state) => "{{{$state}}}")
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')), ignoreRecord: true)
|
||||
->rules(EggVariable::getRulesForField('env_variable'))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
'required' => trans('admin/egg.error_required'),
|
||||
'*' => trans('admin/egg.error_reserved'),
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('default_value')->maxLength(255),
|
||||
Fieldset::make('User Permissions')
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255),
|
||||
Fieldset::make(trans('admin/egg.user_permissions'))
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
|
||||
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
|
||||
]),
|
||||
TagsInput::make('rules')
|
||||
->label(trans('admin/egg.rules'))
|
||||
->columnSpanFull()
|
||||
->placeholder('Add Rule')
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
@ -208,23 +222,25 @@ class EditEgg extends EditRecord
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
Tab::make(trans('admin/egg.tabs.install_script'))
|
||||
->columns(3)
|
||||
->icon('tabler-file-download')
|
||||
->schema([
|
||||
Select::make('copy_script_from')
|
||||
->placeholder('None')
|
||||
->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||
CopyFrom::make('copy_script_from')
|
||||
->script(),
|
||||
TextInput::make('script_container')
|
||||
->label(trans('admin/egg.script_container'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('alpine:3.4'),
|
||||
TextInput::make('script_entry')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('ash'),
|
||||
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
|
||||
Select::make('script_entry')
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->options(['bash', 'ash', '/bin/bash'])
|
||||
->required(),
|
||||
MonacoEditor::make('script_install')
|
||||
->label('Install Script')
|
||||
->label(trans('admin/egg.script_install'))
|
||||
->placeholderText('')
|
||||
->columnSpanFull()
|
||||
->fontSize('16px')
|
||||
@ -240,9 +256,10 @@ class EditEgg extends EditRecord
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
|
||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')),
|
||||
ExportEggAction::make(),
|
||||
ImportEggAction::make(),
|
||||
ImportEggAction::make()
|
||||
->multiple(false),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
@ -7,15 +7,19 @@ use App\Filament\Components\Actions\ImportEggAction as ImportEggHeaderAction;
|
||||
use App\Filament\Components\Tables\Actions\ExportEggAction;
|
||||
use App\Filament\Components\Tables\Actions\ImportEggAction;
|
||||
use App\Filament\Components\Tables\Actions\UpdateEggAction;
|
||||
use App\Filament\Components\Tables\Actions\UpdateEggBulkAction;
|
||||
use App\Filament\Components\Tables\Filters\TagsFilter;
|
||||
use App\Models\Egg;
|
||||
use Filament\Actions\CreateAction as CreateHeaderAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ReplicateAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ListEggs extends ListRecords
|
||||
{
|
||||
@ -26,12 +30,12 @@ class ListEggs extends ListRecords
|
||||
return $table
|
||||
->searchable(true)
|
||||
->defaultPaginationPageOption(25)
|
||||
->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0)
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label('Id')
|
||||
->hidden(),
|
||||
TextColumn::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->icon('tabler-egg')
|
||||
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
|
||||
->wrap()
|
||||
@ -40,35 +44,63 @@ class ListEggs extends ListRecords
|
||||
TextColumn::make('servers_count')
|
||||
->counts('servers')
|
||||
->icon('tabler-server')
|
||||
->label('Servers'),
|
||||
->label(trans('admin/egg.servers')),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
ExportEggAction::make(),
|
||||
UpdateEggAction::make(),
|
||||
EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip(trans('filament-actions::edit.single.label')),
|
||||
ExportEggAction::make()
|
||||
->iconButton()
|
||||
->tooltip(trans('filament-actions::export.modal.actions.export.label')),
|
||||
UpdateEggAction::make()
|
||||
->iconButton()
|
||||
->tooltip(trans('admin/egg.update')),
|
||||
ReplicateAction::make()
|
||||
->iconButton()
|
||||
->tooltip(trans('filament-actions::replicate.single.label'))
|
||||
->modal(false)
|
||||
->excludeAttributes(['author', 'uuid', 'update_url', 'servers_count', 'created_at', 'updated_at'])
|
||||
->beforeReplicaSaved(function (Egg $replica) {
|
||||
$replica->author = auth()->user()->email;
|
||||
$replica->name .= ' Copy';
|
||||
$replica->uuid = Str::uuid()->toString();
|
||||
})
|
||||
->after(fn (Egg $record, Egg $replica) => $record->variables->each(fn ($variable) => $variable->replicate()->fill(['egg_id' => $replica->id])->save()))
|
||||
->successRedirectUrl(fn (Egg $replica) => EditEgg::getUrl(['record' => $replica])),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete egg')),
|
||||
]),
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->before(fn (DeleteBulkAction $action, Collection $records) => $action->records($records->filter(function ($egg) {
|
||||
/** @var Egg $egg */
|
||||
return $egg->servers_count <= 0;
|
||||
}))),
|
||||
UpdateEggBulkAction::make()
|
||||
->before(fn (UpdateEggBulkAction $action, Collection $records) => $action->records($records->filter(function ($egg) {
|
||||
/** @var Egg $egg */
|
||||
return cache()->get("eggs.$egg->uuid.update", false);
|
||||
}))),
|
||||
])
|
||||
->emptyStateIcon('tabler-eggs')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Eggs')
|
||||
->emptyStateHeading(trans('admin/egg.no_eggs'))
|
||||
->emptyStateActions([
|
||||
CreateAction::make()
|
||||
->label('Create Egg'),
|
||||
ImportEggAction::make(),
|
||||
CreateAction::make(),
|
||||
ImportEggAction::make()
|
||||
->multiple(),
|
||||
])
|
||||
->filters([
|
||||
TagsFilter::make()
|
||||
->model(Egg::class),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ImportEggHeaderAction::make(),
|
||||
CreateHeaderAction::make()
|
||||
->label('Create Egg'),
|
||||
ImportEggHeaderAction::make()
|
||||
->multiple(),
|
||||
CreateHeaderAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,10 @@ class ServersRelationManager extends RelationManager
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned to this Egg.')
|
||||
->emptyStateDescription(trans('admin/egg.no_servers'))
|
||||
->emptyStateHeading(trans('admin/egg.no_servers_help'))
|
||||
->searchable(false)
|
||||
->heading(trans('admin/egg.servers'))
|
||||
->columns([
|
||||
TextColumn::make('user.username')
|
||||
->label('Owner')
|
||||
@ -25,6 +27,7 @@ class ServersRelationManager extends RelationManager
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
TextColumn::make('name')
|
||||
->label(trans('admin/server.name'))
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||
->sortable(),
|
||||
@ -32,9 +35,9 @@ class ServersRelationManager extends RelationManager
|
||||
->icon('tabler-server-2')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
|
||||
TextColumn::make('image')
|
||||
->label('Docker Image'),
|
||||
->label(trans('admin/server.docker_image')),
|
||||
SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
|
@ -4,7 +4,21 @@ namespace App\Filament\Admin\Resources;
|
||||
|
||||
use App\Filament\Admin\Resources\MountResource\Pages;
|
||||
use App\Models\Mount;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class MountResource extends Resource
|
||||
{
|
||||
@ -12,13 +26,140 @@ class MountResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('admin/mount.nav_title');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return trans('admin/mount.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return trans('admin/mount.model_label_plural');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return trans('admin/dashboard.advanced');
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(trans('admin/mount.table.name'))
|
||||
->description(fn (Mount $mount) => "$mount->source -> $mount->target")
|
||||
->sortable(),
|
||||
TextColumn::make('eggs.name')
|
||||
->icon('tabler-eggs')
|
||||
->label(trans('admin/mount.eggs'))
|
||||
->badge()
|
||||
->placeholder(trans('admin/mount.table.all_eggs')),
|
||||
TextColumn::make('nodes.name')
|
||||
->icon('tabler-server-2')
|
||||
->label(trans('admin/mount.nodes'))
|
||||
->badge()
|
||||
->placeholder(trans('admin/mount.table.all_nodes')),
|
||||
TextColumn::make('read_only')
|
||||
->label(trans('admin/mount.table.read_only'))
|
||||
->badge()
|
||||
->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing')
|
||||
->color(fn ($state) => $state ? 'success' : 'warning')
|
||||
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->hidden(fn ($record) => static::canEdit($record)),
|
||||
EditAction::make(),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-layers-linked')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading(trans('admin/mount.no_mounts'))
|
||||
->emptyStateActions([
|
||||
CreateAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/mount.name'))
|
||||
->required()
|
||||
->helperText(trans('admin/mount.name_help'))
|
||||
->maxLength(64),
|
||||
ToggleButtons::make('read_only')
|
||||
->label(trans('admin/mount.read_only'))
|
||||
->helperText(trans('admin/mount.read_only_help'))
|
||||
->options([
|
||||
false => trans('admin/mount.toggles.writable'),
|
||||
true => trans('admin/mount.toggles.read_only'),
|
||||
])
|
||||
->icons([
|
||||
false => 'tabler-writing',
|
||||
true => 'tabler-writing-off',
|
||||
])
|
||||
->colors([
|
||||
false => 'warning',
|
||||
true => 'success',
|
||||
])
|
||||
->inline()
|
||||
->default(false)
|
||||
->required(),
|
||||
TextInput::make('source')
|
||||
->label(trans('admin/mount.source'))
|
||||
->required()
|
||||
->helperText(trans('admin/mount.source_help'))
|
||||
->maxLength(255),
|
||||
TextInput::make('target')
|
||||
->label(trans('admin/mount.target'))
|
||||
->required()
|
||||
->helperText(trans('admin/mount.target_help'))
|
||||
->maxLength(255),
|
||||
Textarea::make('description')
|
||||
->label(trans('admin/mount.description'))
|
||||
->helperText(trans('admin/mount.description_help'))
|
||||
->columnSpanFull(),
|
||||
])->columnSpan(1)->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Group::make()->schema([
|
||||
Section::make()->schema([
|
||||
Select::make('eggs')->multiple()
|
||||
->label(trans('admin/mount.eggs'))
|
||||
->relationship('eggs', 'name')
|
||||
->preload(),
|
||||
Select::make('nodes')->multiple()
|
||||
->label(trans('admin/mount.nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->searchable(['name', 'fqdn'])
|
||||
->preload(),
|
||||
]),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@ -26,7 +167,19 @@ class MountResource extends Resource
|
||||
return [
|
||||
'index' => Pages\ListMounts::route('/'),
|
||||
'create' => Pages\CreateMount::route('/create'),
|
||||
'view' => Pages\ViewMount::route('/{record}'),
|
||||
'edit' => Pages\EditMount::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->where(function (Builder $query) {
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,6 @@
|
||||
namespace App\Filament\Admin\Resources\MountResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MountResource;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
@ -33,90 +25,10 @@ class CreateMount extends CreateRecord
|
||||
return [];
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->helperText('Unique name used to separate this mount from another.')
|
||||
->maxLength(64),
|
||||
ToggleButtons::make('read_only')
|
||||
->label('Read only?')
|
||||
->helperText('Is the mount read only inside the container?')
|
||||
->options([
|
||||
false => 'Writeable',
|
||||
true => 'Read only',
|
||||
])
|
||||
->icons([
|
||||
false => 'tabler-writing',
|
||||
true => 'tabler-writing-off',
|
||||
])
|
||||
->colors([
|
||||
false => 'warning',
|
||||
true => 'success',
|
||||
])
|
||||
->inline()
|
||||
->default(false)
|
||||
->required(),
|
||||
TextInput::make('source')
|
||||
->required()
|
||||
->helperText('File path on the host system to mount to a container.')
|
||||
->maxLength(255),
|
||||
TextInput::make('target')
|
||||
->required()
|
||||
->helperText('Where the mount will be accessible inside a container.')
|
||||
->maxLength(255),
|
||||
ToggleButtons::make('user_mountable')
|
||||
->hidden()
|
||||
->label('User mountable?')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Yes',
|
||||
])
|
||||
->icons([
|
||||
false => 'tabler-user-cancel',
|
||||
true => 'tabler-user-bolt',
|
||||
])
|
||||
->colors([
|
||||
false => 'success',
|
||||
true => 'warning',
|
||||
])
|
||||
->default(false)
|
||||
->inline()
|
||||
->required(),
|
||||
Textarea::make('description')
|
||||
->helperText('A longer description for this mount.')
|
||||
->columnSpanFull(),
|
||||
Hidden::make('user_mountable')->default(1),
|
||||
])->columnSpan(1)->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Group::make()->schema([
|
||||
Section::make()->schema([
|
||||
Select::make('eggs')->multiple()
|
||||
->relationship('eggs', 'name')
|
||||
->preload(),
|
||||
Select::make('nodes')->multiple()
|
||||
->relationship('nodes', 'name')
|
||||
->searchable(['name', 'fqdn'])
|
||||
->preload(),
|
||||
]),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$data['uuid'] ??= Str::uuid()->toString();
|
||||
$data['user_mountable'] = 1;
|
||||
|
||||
return parent::handleRecordCreation($data);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user