├── public
├── favicon.ico
├── robots.txt
├── mix-manifest.json
├── images
│ └── running.svg
├── .htaccess
├── js
│ └── main.js.LICENSE.txt
└── web.config
├── database
├── .gitignore
├── seeders
│ ├── DatabaseSeeder.php
│ └── ServerSeeder.php
├── migrations
│ ├── 2021_08_23_043010_create_backups_table.php
│ ├── 2021_06_25_011315_create_iso_images_table.php
│ ├── 2014_10_12_100000_create_password_resets_table.php
│ ├── 2021_06_25_011311_create_templates_table.php
│ ├── 2021_08_23_053450_create_ip_addresses_table.php
│ ├── 2021_08_23_043232_create_disks_table.php
│ ├── 2019_08_19_000000_create_failed_jobs_table.php
│ ├── 2021_05_24_042531_create_sessions_table.php
│ ├── 2021_06_09_203702_create_plans_table.php
│ ├── 2019_12_14_000001_create_personal_access_tokens_table.php
│ ├── 2021_08_23_042957_create_snapshots_table.php
│ ├── 2014_10_12_200000_add_two_factor_columns_to_users_table.php
│ ├── 2014_10_12_000000_create_users_table.php
│ ├── 2021_06_09_202301_create_nodes_table.php
│ └── 2021_06_09_202437_create_servers_table.php
└── factories
│ ├── ServerFactory.php
│ └── NodeFactory.php
├── resources
├── js
│ ├── components
│ │ ├── SecondaryButton.vue
│ │ ├── InputError.vue
│ │ ├── NavLink.vue
│ │ ├── Card.vue
│ │ ├── Label.vue
│ │ ├── SettingContainer.vue
│ │ ├── Overlay.vue
│ │ ├── UserHeader.vue
│ │ ├── Button.vue
│ │ ├── IconButton.vue
│ │ ├── Input.vue
│ │ ├── NestedTabs.vue
│ │ ├── DialogModal.vue
│ │ ├── settings
│ │ │ ├── BackupSetting.vue
│ │ │ ├── SnapshotSetting.vue
│ │ │ └── SecuritySetting.vue
│ │ ├── GoBackButton.vue
│ │ ├── Radio.vue
│ │ ├── Setting.vue
│ │ ├── CardStatistic.vue
│ │ ├── Table.vue
│ │ ├── FormSection.vue
│ │ ├── UsageBox.vue
│ │ └── Pagination.vue
│ ├── heroicons.d.ts
│ ├── util
│ │ ├── bytesToMegabytes.ts
│ │ ├── serverInterface.ts
│ │ ├── sendAlert.ts
│ │ ├── dateTimeCalculator.ts
│ │ └── paginationTypes.ts
│ ├── api
│ │ ├── node
│ │ │ ├── runHealthTests.ts
│ │ │ └── deleteNode.ts
│ │ ├── server
│ │ │ ├── editPowerState.ts
│ │ │ ├── snapshots
│ │ │ │ ├── createSnapshot.ts
│ │ │ │ ├── deleteSnapshot.ts
│ │ │ │ └── rollbackSnapshot.ts
│ │ │ └── getStatus.ts
│ │ └── user
│ │ │ ├── searchUsers.ts
│ │ │ ├── getUser.ts
│ │ │ └── getAllUsers.ts
│ ├── shims-vue.d.ts
│ ├── Jetstream
│ │ ├── SectionBorder.vue
│ │ ├── InputError.vue
│ │ ├── Label.vue
│ │ ├── AuthenticationCard.vue
│ │ ├── ActionMessage.vue
│ │ ├── ApplicationMark.vue
│ │ ├── Input.vue
│ │ ├── AuthenticationCardLogo.vue
│ │ ├── DangerButton.vue
│ │ ├── SectionTitle.vue
│ │ ├── ValidationErrors.vue
│ │ ├── ActionSection.vue
│ │ ├── Button.vue
│ │ ├── SecondaryButton.vue
│ │ ├── NavLink.vue
│ │ ├── Checkbox.vue
│ │ ├── DropdownLink.vue
│ │ ├── ResponsiveNavLink.vue
│ │ ├── DialogModal.vue
│ │ └── FormSection.vue
│ ├── Pages
│ │ ├── Admin
│ │ │ ├── Nodes
│ │ │ │ └── nestedLinks.ts
│ │ │ ├── Servers
│ │ │ │ ├── Edit.vue
│ │ │ │ └── Index.vue
│ │ │ ├── Dashboard.vue
│ │ │ └── Users
│ │ │ │ ├── Index.vue
│ │ │ │ └── Show.vue
│ │ ├── Servers
│ │ │ ├── Backups
│ │ │ │ └── Index.vue
│ │ │ ├── Security
│ │ │ │ └── Index.vue
│ │ │ ├── Settings
│ │ │ │ └── Index.vue
│ │ │ └── General.vue
│ │ ├── PrivacyPolicy.vue
│ │ ├── TermsOfService.vue
│ │ ├── API
│ │ │ └── Index.vue
│ │ └── Dashboard.vue
│ ├── state
│ │ ├── index.ts
│ │ ├── server
│ │ │ └── status.ts
│ │ └── alerts.ts
│ ├── shims-globals.d.ts
│ ├── Layouts
│ │ ├── AdminLayout.vue
│ │ └── ServerLayout.vue
│ ├── plugins
│ │ └── axios.ts
│ ├── bootstrap.js
│ └── main.ts
├── markdown
│ ├── policy.md
│ └── terms.md
├── lang
│ └── en
│ │ ├── pagination.php
│ │ ├── auth.php
│ │ └── passwords.php
└── views
│ └── app.blade.php
├── bootstrap
├── cache
│ └── .gitignore
└── app.php
├── storage
├── logs
│ └── .gitignore
├── app
│ ├── public
│ │ └── .gitignore
│ └── .gitignore
└── framework
│ ├── testing
│ └── .gitignore
│ ├── views
│ └── .gitignore
│ ├── cache
│ ├── data
│ │ └── .gitignore
│ └── .gitignore
│ ├── sessions
│ └── .gitignore
│ └── .gitignore
├── babel.config.json
├── .gitattributes
├── app
├── Enums
│ └── Servers
│ │ └── Cloudinit
│ │ ├── BiosType.php
│ │ └── AuthenticationType.php
├── Http
│ ├── Controllers
│ │ ├── Admin
│ │ │ ├── Servers
│ │ │ │ ├── SuspensionController.php
│ │ │ │ ├── DeletionController.php
│ │ │ │ ├── CreationController.php
│ │ │ │ ├── TemplateController.php
│ │ │ │ └── ServerController.php
│ │ │ ├── BaseController.php
│ │ │ └── Users
│ │ │ │ └── UserController.php
│ │ ├── Client
│ │ │ └── Servers
│ │ │ │ ├── SecurityController.php
│ │ │ │ ├── PerformanceController.php
│ │ │ │ ├── ServerController.php
│ │ │ │ ├── AgentController.php
│ │ │ │ ├── StatusController.php
│ │ │ │ ├── VNCController.php
│ │ │ │ ├── PowerController.php
│ │ │ │ ├── SettingsController.php
│ │ │ │ ├── BackupController.php
│ │ │ │ └── SnapshotController.php
│ │ ├── Controller.php
│ │ └── ApplicationApiController.php
│ ├── Middleware
│ │ ├── EncryptCookies.php
│ │ ├── VerifyCsrfToken.php
│ │ ├── TrustHosts.php
│ │ ├── PreventRequestsDuringMaintenance.php
│ │ ├── TrimStrings.php
│ │ ├── Authenticate.php
│ │ ├── TrustProxies.php
│ │ ├── AdminAuthenticate.php
│ │ ├── VerifyCloudinitEnabled.php
│ │ ├── RedirectIfAuthenticated.php
│ │ ├── AuthenticateServerAccess.php
│ │ └── HandleInertiaRequests.php
│ └── Requests
│ │ ├── Admin
│ │ ├── Servers
│ │ │ └── UpdateOwnerRequest.php
│ │ └── Nodes
│ │ │ ├── StoreNodeRequest.php
│ │ │ └── UpdateNodeRequest.php
│ │ └── Client
│ │ └── Servers
│ │ ├── Snapshots
│ │ └── SnapshotRequest.php
│ │ ├── Settings
│ │ ├── UpdateDisplayInfoRequest.php
│ │ ├── UpdateBiosTypeRequest.php
│ │ └── UpdateNetworkConfigRequest.php
│ │ └── Security
│ │ └── UpdatePasswordRequest.php
├── Models
│ ├── Plan.php
│ ├── Node.php
│ ├── Server.php
│ └── User.php
├── Services
│ ├── Servers
│ │ ├── StatusService.php
│ │ ├── TemplateService.php
│ │ ├── DeletionService.php
│ │ ├── VNCService.php
│ │ ├── AgentService.php
│ │ ├── CreationService.php
│ │ ├── PowerService.php
│ │ ├── SnapshotService.php
│ │ ├── BackupService.php
│ │ └── CloudinitService.php
│ ├── Nodes
│ │ ├── VersionService.php
│ │ ├── Health
│ │ │ ├── ResultsService.php
│ │ │ └── HealthService.php
│ │ └── SyncService.php
│ └── ProxmoxService.php
├── Actions
│ ├── Fortify
│ │ ├── PasswordValidationRules.php
│ │ ├── ResetUserPassword.php
│ │ ├── CreateNewUser.php
│ │ └── UpdateUserPassword.php
│ └── Jetstream
│ │ └── DeleteUser.php
├── Providers
│ ├── BroadcastServiceProvider.php
│ ├── AppServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── EventServiceProvider.php
│ ├── JetstreamServiceProvider.php
│ └── FortifyServiceProvider.php
├── Exceptions
│ └── Handler.php
├── Console
│ └── Kernel.php
└── Rules
│ └── Network
│ ├── Hostname.php
│ └── Domain.php
├── tests
├── TestCase.php
├── Unit
│ └── ExampleTest.php
├── Feature
│ ├── ExampleTest.php
│ ├── BrowserSessionsTest.php
│ ├── ProfileInformationTest.php
│ ├── RegistrationTest.php
│ ├── DeleteApiTokenTest.php
│ ├── AuthenticationTest.php
│ ├── DeleteAccountTest.php
│ ├── CreateApiTokenTest.php
│ ├── PasswordConfirmationTest.php
│ ├── ApiTokenPermissionsTest.php
│ └── UpdatePasswordTest.php
└── CreatesApplication.php
├── .styleci.yml
├── .gitignore
├── README.md
├── routes
├── channels.php
├── console.php
├── api.php
├── admin.php
└── web.php
├── server.php
├── webpack.config.js
├── .github
├── workflows
│ ├── frontend-tests.yml
│ └── laravel-tests.yml
└── FUNDING.yml
├── tailwind.config.js
├── vite.config.ts
├── config
├── cors.php
├── services.php
├── view.php
├── hashing.php
└── sanctum.php
├── .env.example
├── tsconfig.json
├── LICENSE
├── phpunit.xml
├── package.json
└── artisan
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 |
--------------------------------------------------------------------------------
/resources/js/components/SecondaryButton.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/resources/js/heroicons.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@heroicons/*';
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !public/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/framework/testing/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-flow"]
3 | }
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/resources/js/util/bytesToMegabytes.ts:
--------------------------------------------------------------------------------
1 | export default (bytes: number) => Math.floor(bytes / 1024 / 1024);
--------------------------------------------------------------------------------
/public/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/js/main.js": "/js/main.js",
3 | "/css/app.css": "/css/app.css"
4 | }
5 |
--------------------------------------------------------------------------------
/resources/markdown/policy.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | Edit this file to define the privacy policy for your application.
4 |
--------------------------------------------------------------------------------
/resources/markdown/terms.md:
--------------------------------------------------------------------------------
1 | # Terms of Service
2 |
3 | Edit this file to define the terms of service for your application.
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.css linguist-vendored
3 | *.scss linguist-vendored
4 | *.js linguist-vendored
5 | CHANGELOG.md export-ignore
6 |
--------------------------------------------------------------------------------
/resources/js/api/node/runHealthTests.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export default () => axios.post(route('admin.nodes.health.test'))
4 |
--------------------------------------------------------------------------------
/resources/js/api/node/deleteNode.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export default (id: number) => axios.delete(route('admin.nodes.destroy', id))
4 |
--------------------------------------------------------------------------------
/storage/framework/.gitignore:
--------------------------------------------------------------------------------
1 | compiled.php
2 | config.php
3 | down
4 | events.scanned.php
5 | maintenance.php
6 | routes.php
7 | routes.scanned.php
8 | schedule-*
9 | services.json
10 |
--------------------------------------------------------------------------------
/app/Enums/Servers/Cloudinit/BiosType.php:
--------------------------------------------------------------------------------
1 |
4 | export default component
5 | }
6 |
--------------------------------------------------------------------------------
/resources/js/api/server/editPowerState.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export default (id: number, state: string) => {
4 | return axios.post(
5 | route(`servers.show.power.${state}`, id)
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/app/Enums/Servers/Cloudinit/AuthenticationType.php:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/resources/js/api/user/searchUsers.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export default (search: string) => {
4 | return axios.get(route('admin.users.search'), {
5 | params: {
6 | search
7 | }
8 | })
9 | }
--------------------------------------------------------------------------------
/resources/js/Pages/Admin/Nodes/nestedLinks.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | name: 'Node List',
4 | route: 'admin.nodes.index',
5 | },
6 | {
7 | name: 'Health',
8 | route: 'admin.nodes.health.show',
9 | },
10 | ]
11 |
--------------------------------------------------------------------------------
/app/Models/Plan.php:
--------------------------------------------------------------------------------
1 | {
4 | return axios.post(route('servers.show.snapshots.create', id), {
5 | name: snapName,
6 | })
7 | }
8 |
--------------------------------------------------------------------------------
/resources/js/api/server/snapshots/deleteSnapshot.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export default (id: number, snapName: string) => {
4 | return axios.post(route('servers.show.snapshots.delete', id), {
5 | name: snapName,
6 | })
7 | }
8 |
--------------------------------------------------------------------------------
/resources/js/api/server/snapshots/rollbackSnapshot.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export default (id: number, snapName: string) => {
4 | return axios.post(route('servers.show.snapshots.rollback', id), {
5 | name: snapName,
6 | })
7 | }
8 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | php:
2 | preset: laravel
3 | disabled:
4 | - no_unused_imports
5 | finder:
6 | not-name:
7 | - index.php
8 | - server.php
9 | js:
10 | finder:
11 | not-name:
12 | - webpack.mix.js
13 | css: true
14 |
--------------------------------------------------------------------------------
/resources/js/api/user/getUser.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export default (id: number) => {
4 | return axios.get(route('admin.users.show', id), {
5 | headers: {
6 | 'Content-Type': 'application/json'
7 | }
8 | })
9 | }
--------------------------------------------------------------------------------
/resources/js/Pages/Servers/Backups/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/js/state/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from 'vuex'
2 | import alerts from '@/state/alerts'
3 | import serverStatus from '@/state/server/status'
4 |
5 | const store = createStore({
6 | modules: {
7 | alerts,
8 | serverStatus,
9 | }
10 | })
11 |
12 | export default store
--------------------------------------------------------------------------------
/resources/js/shims-globals.d.ts:
--------------------------------------------------------------------------------
1 | import {AxiosInstance} from "axios";
2 | import {Config, Router} from "ziggy-js";
3 |
4 | declare global {
5 | function route(): Router;
6 | function route(name: string, params?: any, absolute?: boolean, customZiggy?: Config): string;
7 | function usePage(): any;
8 | }
--------------------------------------------------------------------------------
/resources/js/Jetstream/InputError.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ message }}
5 |
6 |
7 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/resources/js/components/InputError.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ message }}
5 |
6 |
7 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/resources/js/components/NavLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
--------------------------------------------------------------------------------
/app/Services/Servers/StatusService.php:
--------------------------------------------------------------------------------
1 | instance()->status()->current()->get();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/resources/js/components/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /public/hot
3 | /public/storage
4 | /storage/*.key
5 | /storage/debugbar
6 | /public/css/*
7 | /public/js/*
8 | /vendor
9 | /laradock
10 | .env
11 | .env.backup
12 | .phpunit.result.cache
13 | docker-compose.override.yml
14 | Homestead.json
15 | Homestead.yaml
16 | npm-debug.log
17 | yarn-error.log
18 | /.idea
--------------------------------------------------------------------------------
/resources/js/Jetstream/Label.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ value }}
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/resources/js/components/Label.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ value }}
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/resources/js/components/SettingContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/app/Services/Nodes/VersionService.php:
--------------------------------------------------------------------------------
1 | mainInstance()->nodes()->node($this->node->cluster)->version()->get();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/Unit/ExampleTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Services/Nodes/Health/ResultsService.php:
--------------------------------------------------------------------------------
1 | get();
12 | }
13 | }
--------------------------------------------------------------------------------
/resources/js/util/serverInterface.ts:
--------------------------------------------------------------------------------
1 | export default interface ServerInterface {
2 | id: number
3 | name: string
4 | description: string | null
5 | user_id: number
6 | node_id: number
7 | vnc_username: string | null
8 | vnc_password: string | null
9 | cloud_init_enabled: boolean
10 | vmid: number
11 | created_at: string
12 | updated_at: string
13 | }
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | create();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Http/Middleware/EncryptCookies.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Client/Servers/SecurityController.php:
--------------------------------------------------------------------------------
1 | $server,
14 | ]);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :warning: Moved :warning:
2 |
3 | :warning::warning: This project has been moved to Convoy Panel at https://github.com/ConvoyPanel/panel :warning::warning:
4 |
5 | ## About Stratum
6 |
7 | Stratum is an administrative panel for clients and admins to manage their KVM instances via Proxmox's API.
8 |
9 | ## Requirements
10 |
11 | - PHP 8.0 (required extensions: Fileinfo, JSON, Mbstring, OpenSSL, PDO)
12 | - NGINX or Apache server
13 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 | instance()->postTemplate($params);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Actions/Fortify/PasswordValidationRules.php:
--------------------------------------------------------------------------------
1 | get('/');
18 |
19 | $response->assertStatus(200);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustHosts.php:
--------------------------------------------------------------------------------
1 | allSubdomainsOfApplicationUrl(),
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/ActionMessage.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
16 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Client/Servers/PerformanceController.php:
--------------------------------------------------------------------------------
1 | $server,
15 | ]);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/PreventRequestsDuringMaintenance.php:
--------------------------------------------------------------------------------
1 | instance($cluster)->nodes()->node()->storage()->get();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/public/images/running.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Admin/BaseController.php:
--------------------------------------------------------------------------------
1 | Server::count(),
15 | 'version' => config()->get('app.version'),
16 | ]);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Models/Node.php:
--------------------------------------------------------------------------------
1 | hasMany(Server::class);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/ApplicationMark.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/Actions/Jetstream/DeleteUser.php:
--------------------------------------------------------------------------------
1 | deleteProfilePhoto();
18 | $user->tokens->each->delete();
19 | $user->delete();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
19 |
20 | return $app;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Providers/BroadcastServiceProvider.php:
--------------------------------------------------------------------------------
1 | deletionService->destroy($server);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/Providers/AppServiceProvider.php:
--------------------------------------------------------------------------------
1 | count(10)->create(['user_id' => User::factory()->create(), 'node_id' => Node::factory()->create(), 'cloud_init_enabled' => false]);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/Input.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
--------------------------------------------------------------------------------
/app/Services/Servers/DeletionService.php:
--------------------------------------------------------------------------------
1 | proxmox()->delete($params); // WIP
23 | }
24 | }
--------------------------------------------------------------------------------
/resources/js/components/Overlay.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Authenticate.php:
--------------------------------------------------------------------------------
1 | expectsJson()) {
18 | return route('login');
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/AuthenticationCardLogo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/Models/Server.php:
--------------------------------------------------------------------------------
1 | belongsTo(Node::class);
22 | }
23 |
24 | public function owner()
25 | {
26 | return $this->belongsTo(User::class, 'user_id');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Client/Servers/ServerController.php:
--------------------------------------------------------------------------------
1 | $request->user()->servers,
14 | ]);
15 | }
16 |
17 | public function show(Server $server) {
18 | return inertia('Servers/General', [
19 | 'server' => $server,
20 | ]);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/resources/lang/en/pagination.php:
--------------------------------------------------------------------------------
1 | '« Previous',
17 | 'next' => 'Next »',
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/tests/Feature/BrowserSessionsTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->create());
16 |
17 | $response = $this->delete('/user/other-browser-sessions', [
18 | 'password' => 'password',
19 | ]);
20 |
21 | $response->assertSessionHasNoErrors();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/routes/channels.php:
--------------------------------------------------------------------------------
1 | id === (int) $id;
18 | });
19 |
--------------------------------------------------------------------------------
/resources/js/util/sendAlert.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from 'vuex'
2 | import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'
3 |
4 | const store = useStore()
5 |
6 | export const sendPending = (msg: string) => {
7 | store.dispatch('alerts/createAlert', {
8 | message: msg,
9 | timeout: false,
10 | })
11 | }
12 |
13 | export const sendSuccess = (msg: string) => {
14 | store.dispatch('alerts/createAlert', {
15 | message: msg,
16 | icon: faCheck,
17 | })
18 | }
19 |
20 | export const sendError = (msg: string) => {
21 | store.dispatch('alerts/createAlert', {
22 | message: msg,
23 | icon: faTimes,
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/DangerButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/server.php:
--------------------------------------------------------------------------------
1 |
8 | */
9 |
10 | $uri = urldecode(
11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
12 | );
13 |
14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the
15 | // built-in PHP web server. This provides a convenient way to test a Laravel
16 | // application without having installed a "real" web server software here.
17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) {
18 | return false;
19 | }
20 |
21 | require_once __DIR__.'/public/index.php';
22 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 |
5 | output: {
6 | publicPath: 'http://localhost:4206/',
7 | },
8 | resolve: {
9 | extensions: ['.js', '.vue', '.ts', '.tsx'],
10 | alias: {
11 | '@components': path.resolve('resources/js/components'),
12 | '@assets': path.resolve('resources/assets'),
13 | '@api': path.resolve('resources/js/api'),
14 | '@layouts': path.resolve('resources/js/Layouts'),
15 | '@pages': path.resolve('resources/js/Pages'),
16 | '@': path.resolve('resources/js'),
17 | },
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/SectionTitle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/resources/js/util/dateTimeCalculator.ts:
--------------------------------------------------------------------------------
1 | // Argument must be unix time stamp
2 |
3 | export default (timestamp: number) => {
4 | let months = [
5 | 'Jan',
6 | 'Feb',
7 | 'Mar',
8 | 'Apr',
9 | 'May',
10 | 'Jun',
11 | 'Jul',
12 | 'Aug',
13 | 'Sep',
14 | 'Oct',
15 | 'Nov',
16 | 'Dec',
17 | ]
18 |
19 | let date = new Date(timestamp * 1000)
20 |
21 | let year = date.getFullYear()
22 | let month = months[date.getMonth()]
23 | let day = date.getDate()
24 | let hours = date.getHours()
25 | let minutes = date.getMinutes()
26 | let seconds = date.getSeconds()
27 |
28 | return {year, month, day, hours, minutes, seconds}
29 | }
30 |
--------------------------------------------------------------------------------
/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
19 | })->purpose('Display an inspiring quote');
20 |
--------------------------------------------------------------------------------
/resources/js/Pages/Servers/Security/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Security
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/frontend-tests.yml:
--------------------------------------------------------------------------------
1 | name: Frontend Tests
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | frontend-tests:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 |
16 | - name: Checkout
17 |
18 | uses: actions/checkout@v2
19 |
20 | - name: Setup Node.js
21 |
22 | uses: actions/setup-node@v2-beta
23 |
24 | with:
25 |
26 | node-version: '12'
27 |
28 | check-latest: true
29 |
30 | - name: Install NPM dependencies
31 |
32 | run: npm install
33 |
34 | - name: Compile assets for production
35 |
36 | run: npm run production
37 |
38 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews -Indexes
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Handle Authorization Header
9 | RewriteCond %{HTTP:Authorization} .
10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
11 |
12 | # Redirect Trailing Slashes If Not A Folder...
13 | RewriteCond %{REQUEST_FILENAME} !-d
14 | RewriteCond %{REQUEST_URI} (.+)/$
15 | RewriteRule ^ %1 [L,R=301]
16 |
17 | # Send Requests To Front Controller...
18 | RewriteCond %{REQUEST_FILENAME} !-d
19 | RewriteCond %{REQUEST_FILENAME} !-f
20 | RewriteRule ^ index.php [L]
21 |
22 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/ValidationErrors.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Whoops! Something went wrong.
4 |
5 |
8 |
9 |
10 |
11 |
24 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustProxies.php:
--------------------------------------------------------------------------------
1 | agentService->getOsInfo();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Http/Middleware/AdminAuthenticate.php:
--------------------------------------------------------------------------------
1 | user() || !$request->user()->root_admin) {
21 | throw new AccessDeniedHttpException();
22 | }
23 |
24 | return $next($request);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/resources/js/Layouts/AdminLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
31 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/Servers/UpdateOwnerRequest.php:
--------------------------------------------------------------------------------
1 | 'required|numeric|exists:users,id'
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Requests/Client/Servers/Snapshots/SnapshotRequest.php:
--------------------------------------------------------------------------------
1 | 'required|alpha_dash|max:50'
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Providers/AuthServiceProvider.php:
--------------------------------------------------------------------------------
1 | 'App\Policies\ModelPolicy',
17 | ];
18 |
19 | /**
20 | * Register any authentication / authorization services.
21 | *
22 | * @return void
23 | */
24 | public function boot()
25 | {
26 | $this->registerPolicies();
27 |
28 | //
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | group(function () {
19 | Route::get('/user', function (Request $request) {
20 | return $request->user();
21 | });
22 | });
--------------------------------------------------------------------------------
/tests/Feature/ProfileInformationTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->create());
16 |
17 | $response = $this->put('/user/profile-information', [
18 | 'name' => 'Test Name',
19 | 'email' => 'test@example.com',
20 | ]);
21 |
22 | $this->assertEquals('Test Name', $user->fresh()->name);
23 | $this->assertEquals('test@example.com', $user->fresh()->email);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/database/migrations/2021_08_23_043010_create_backups_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | *
25 | * @return void
26 | */
27 | public function down()
28 | {
29 | Schema::dropIfExists('backups');
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/resources/js/plugins/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import NProgress from 'nprogress'
3 |
4 | export interface AxiosConfig {
5 | hideProgress?: boolean;
6 | [key: string]: any;
7 | }
8 |
9 | //axios.defaults.timeout = 2000
10 |
11 | axios.interceptors.request.use(
12 | (config: AxiosConfig) => {
13 | if (!config.hideProgress) {
14 | NProgress.start()
15 | }
16 | return config
17 | },
18 | (error) => {
19 | NProgress.done()
20 | NProgress.remove()
21 | return Promise.reject(error)
22 | }
23 | )
24 |
25 | axios.interceptors.response.use(
26 | (response) => {
27 | NProgress.done();
28 | return response
29 | },
30 | (error) => {
31 | NProgress.done()
32 | return Promise.reject(error)
33 | }
34 | )
35 |
--------------------------------------------------------------------------------
/app/Services/Servers/VNCService.php:
--------------------------------------------------------------------------------
1 | "VNC",
13 | "privs" => [
14 | "VM.Console"
15 | ]
16 | ];
17 | return $this->proxmox()->access()->roles()->post($vnc_role);
18 | }
19 |
20 | public function createUser()
21 | {
22 | $user = [
23 | "enable" => "1",
24 | "userid" => "test",
25 | "email" => "",
26 | "password" => ""
27 | ];
28 | return $this->proxmox()->access()->users()->post($user);
29 | }
30 | }
--------------------------------------------------------------------------------
/resources/lang/en/auth.php:
--------------------------------------------------------------------------------
1 | 'These credentials do not match our records.',
17 | 'password' => 'The provided password is incorrect.',
18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
19 |
20 | ];
21 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/ActionSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
25 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/Button.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
26 |
27 |
37 |
--------------------------------------------------------------------------------
/app/Services/Servers/AgentService.php:
--------------------------------------------------------------------------------
1 | instance()->agent()->exec($command);
22 | }
23 |
24 | /**
25 | * @param string $command
26 | * @param array $params
27 | * @return mixed
28 | */
29 | public function getOSInfo()
30 | {
31 | return $this->instance()->agent()->getOsinfo();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/Client/Servers/Settings/UpdateDisplayInfoRequest.php:
--------------------------------------------------------------------------------
1 | 'required|string|min:1|max:50',
28 | 'description' => 'min:0|max:200',
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Middleware/VerifyCloudinitEnabled.php:
--------------------------------------------------------------------------------
1 | route()->parameter('server');
21 |
22 | if (!$server->cloud_init_enabled)
23 | {
24 | throw new BadRequestHttpException('Cloud init is not enabled');
25 | }
26 |
27 | return $next($request);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Services/Servers/CreationService.php:
--------------------------------------------------------------------------------
1 | instance()->clone()->post(['newid' => $id, 'name' => $name]);
20 | }
21 |
22 | /**
23 | * @param array $params
24 | * @return mixed
25 | */
26 | public function changeConfiguration(int $memory, int $storage, int $cores)
27 | {
28 | // Will be used to change VM info after clone
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/resources/js/components/UserHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
{{ $page.props.user.name }}
14 | Manage Account
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: performave
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/resources/js/Pages/PrivacyPolicy.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
27 |
--------------------------------------------------------------------------------
/resources/js/Pages/TermsOfService.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
27 |
--------------------------------------------------------------------------------
/database/factories/ServerFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->firstName(),
28 | 'user_id' => User::factory(),
29 | 'node_id' => Node::factory(),
30 | 'vmid' => rand(1,5000)
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/SecondaryButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
27 |
28 |
38 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Client/Servers/StatusController.php:
--------------------------------------------------------------------------------
1 | returnContent($this->proxmoxService->setServer($server)->fetchStatus());
27 | }
28 | }
--------------------------------------------------------------------------------
/resources/js/util/paginationTypes.ts:
--------------------------------------------------------------------------------
1 | export interface Server {
2 | cloud_init_enabled: number
3 | created_at: string
4 | description: string | null
5 | id: number
6 | name: string
7 | node_id: number
8 | updated_at: string
9 | user_id: number
10 | vmid: number
11 | vnc_password: string | null
12 | vnc_username: string | null
13 | }
14 |
15 | export interface Link {
16 | url: string | null
17 | label: string
18 | active: boolean
19 | }
20 |
21 | export interface PaginationReturns- {
22 | current_page: number
23 | data: Item[]
24 | first_page_url: string | null
25 | from: number
26 | last_page: number
27 | last_page_url: string
28 | links: Link[]
29 | next_page_url: string | null
30 | path: string
31 | per_page: number
32 | prev_page_url: string | null
33 | to: number
34 | total: number
35 | }
36 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/NavLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
--------------------------------------------------------------------------------
/app/Actions/Fortify/ResetUserPassword.php:
--------------------------------------------------------------------------------
1 | $this->passwordRules(),
24 | ])->validate();
25 |
26 | $user->forceFill([
27 | 'password' => Hash::make($input['password']),
28 | ])->save();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Requests/Client/Servers/Settings/UpdateBiosTypeRequest.php:
--------------------------------------------------------------------------------
1 | [new Enum(BiosType::class), 'alpha_dash', 'required']
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Providers/EventServiceProvider.php:
--------------------------------------------------------------------------------
1 | [
19 | SendEmailVerificationNotification::class,
20 | ],
21 | ];
22 |
23 | /**
24 | * Register any events for your application.
25 | *
26 | * @return void
27 | */
28 | public function boot()
29 | {
30 | //
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/resources/lang/en/passwords.php:
--------------------------------------------------------------------------------
1 | 'Your password has been reset!',
17 | 'sent' => 'We have emailed your password reset link!',
18 | 'throttled' => 'Please wait before retrying.',
19 | 'token' => 'This password reset token is invalid.',
20 | 'user' => "We can't find a user with that email address.",
21 |
22 | ];
23 |
--------------------------------------------------------------------------------
/database/migrations/2021_06_25_011315_create_iso_images_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | $table->string("name");
20 | $table->string("link");
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::dropIfExists('iso_images');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
33 |
--------------------------------------------------------------------------------
/resources/js/components/Button.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
26 |
27 |
41 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_100000_create_password_resets_table.php:
--------------------------------------------------------------------------------
1 | string('email')->index();
18 | $table->string('token');
19 | $table->timestamp('created_at')->nullable();
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | *
26 | * @return void
27 | */
28 | public function down()
29 | {
30 | Schema::dropIfExists('password_resets');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Client/Servers/VNCController.php:
--------------------------------------------------------------------------------
1 | VNCService->setServer($server)->createUser();
29 | $this->VNCService->setServer($server)->createRole();
30 | }
31 | }
--------------------------------------------------------------------------------
/resources/js/components/IconButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
23 |
--------------------------------------------------------------------------------
/resources/js/components/Input.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
42 |
--------------------------------------------------------------------------------
/database/migrations/2021_06_25_011311_create_templates_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | $table->string("name");
20 | $table->string("proxmox_id");
21 | $table->string("link");
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | *
28 | * @return void
29 | */
30 | public function down()
31 | {
32 | Schema::dropIfExists('templates');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/DropdownLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
--------------------------------------------------------------------------------
/app/Http/Middleware/RedirectIfAuthenticated.php:
--------------------------------------------------------------------------------
1 | check()) {
26 | return redirect(RouteServiceProvider::HOME);
27 | }
28 | }
29 |
30 | return $next($request);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/database/factories/NodeFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->firstName(),
26 | 'hostname' => $this->faker->lastName(),
27 | 'username' => $this->faker->name,
28 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
29 | 'port' => 8006,
30 | 'auth_type' => 'pam'
31 | ];
32 | }
33 | }
--------------------------------------------------------------------------------
/resources/js/components/NestedTabs.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ link.name }}
16 |
17 |
18 |
19 |
20 |
31 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme')
2 |
3 | module.exports = {
4 | content: [
5 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
6 | './vendor/laravel/jetstream/**/*.blade.php',
7 | './storage/framework/views/*.php',
8 | './resources/views/**/*.blade.php',
9 | './resources/js/**/*.vue',
10 | ],
11 | safelist: [
12 | 'bg-red-200',
13 | 'text-red-700',
14 | 'bg-green-200',
15 | 'text-green-700',
16 | 'text-gray-700',
17 | 'bg-gray-200',
18 | ],
19 |
20 | theme: {
21 | extend: {
22 | fontFamily: {
23 | sans: ['Inter var', ...defaultTheme.fontFamily.sans],
24 | },
25 | },
26 | },
27 |
28 | variants: {
29 | extend: {
30 | opacity: ['disabled'],
31 | },
32 | },
33 |
34 | plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
35 | }
36 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import laravel from 'vite-plugin-laravel'
4 | import tailwindcss from 'tailwindcss'
5 | import autoprefixer from 'autoprefixer'
6 | import { resolve } from 'path'
7 |
8 | export default defineConfig({
9 | server: {
10 | host: '0.0.0.0',
11 | cors: true,
12 | },
13 | resolve: {
14 | alias: {
15 | '@': resolve(__dirname, './resources/js'),
16 | '@components': resolve(__dirname, './resources/js/components'),
17 | '@assets': resolve(__dirname, './resources/assets'),
18 | '@api': resolve(__dirname, './resources/js/api'),
19 | '@layouts': resolve(__dirname, './resources/js/Layouts'),
20 | '@pages': resolve(__dirname, './resources/js/Pages'),
21 | },
22 | },
23 | plugins: [
24 | vue(),
25 | laravel({
26 | postcss: [tailwindcss(), autoprefixer()],
27 | }),
28 | ],
29 | })
30 |
--------------------------------------------------------------------------------
/app/Http/Requests/Client/Servers/Security/UpdatePasswordRequest.php:
--------------------------------------------------------------------------------
1 | [new Enum(AuthenticationType::class), 'required'],
30 | 'password' => ['required', 'confirmed', 'max:255', 'min:10'],
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Admin/Servers/CreationController.php:
--------------------------------------------------------------------------------
1 | creationService->setServer($server)->createInstance($id, $name);
25 |
26 | $newServer = Server::create([
27 | 'name' => $name,
28 | 'node_id' => $server->node->id,
29 | 'vmid' => $id
30 | ]);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/resources/js/components/DialogModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
39 |
--------------------------------------------------------------------------------
/app/Http/Requests/Client/Servers/Settings/UpdateNetworkConfigRequest.php:
--------------------------------------------------------------------------------
1 | ['required', new Hostname],
30 | 'nameservers' => ['array', 'nullable'],
31 | 'nameservers.*' => ['string', new Domain, 'nullable'],
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/database/migrations/2021_08_23_053450_create_ip_addresses_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->timestamps();
19 | });
20 |
21 | Schema::create('ipv6_addresses', function (Blueprint $table) {
22 | $table->id();
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('ip_addresses');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/resources/js/Pages/Servers/Settings/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Server Information
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
33 |
--------------------------------------------------------------------------------
/app/Exceptions/Handler.php:
--------------------------------------------------------------------------------
1 | reportable(function (Throwable $e) {
38 | //
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/js/components/settings/BackupSetting.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | {{ statusActions[powerState] }}
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/Http/Controllers/ApplicationApiController.php:
--------------------------------------------------------------------------------
1 | header('X-Inertia', true);
14 | }
15 |
16 | protected function returnContent($payload, $code = Response::HTTP_OK): Response
17 | {
18 | return (new Response($payload, $code))->header('X-Inertia', true);
19 | }
20 |
21 | // This can only be used if you are returning a success message after completing an action
22 | protected function returnInertiaResponse(Request $request, String $message)
23 | {
24 | return $request->wantsJson()
25 | ? $this->returnNoContent()
26 | : back()->with('status', $message);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/resources/js/Pages/Servers/General.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | General
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
36 |
--------------------------------------------------------------------------------
/resources/js/components/GoBackButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
21 |
22 |
23 |
24 |
44 |
--------------------------------------------------------------------------------
/config/cors.php:
--------------------------------------------------------------------------------
1 | ['api/*', 'sanctum/csrf-cookie'],
19 |
20 | 'allowed_methods' => ['*'],
21 |
22 | 'allowed_origins' => ['*'],
23 |
24 | 'allowed_origins_patterns' => [],
25 |
26 | 'allowed_headers' => ['*'],
27 |
28 | 'exposed_headers' => [],
29 |
30 | 'max_age' => 0,
31 |
32 | 'supports_credentials' => false,
33 |
34 | ];
35 |
--------------------------------------------------------------------------------
/resources/js/api/user/getAllUsers.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | /*
3 | created_at: "2022-04-24T22:46:01.000000Z"
4 | current_team_id: null
5 | email: "fahey.eino@example.net"
6 | email_verified_at: "2022-04-24T22:46:01.000000Z"
7 | id: 1
8 | name: "Miss Kirsten Carroll II"
9 | profile_photo_path: null
10 | profile_photo_url: "https://ui-avatars.com/api/?name=M+K+C+I&color=7F9CF5&background=EBF4FF"
11 | root_admin: 1
12 | updated_at: "2022-04-29T00:30:07.000000Z
13 | */
14 |
15 | export interface User {
16 | created_at: string,
17 | current_team_id: null,
18 | email: string,
19 | email_verified_at: string,
20 | id: number,
21 | name: string,
22 | profile_photo_path: null | string,
23 | profile_photo_url: null | string,
24 | root_admin: number,
25 | updated_at: string,
26 | }
27 |
28 | export default () => {
29 | return axios.get(route('admin.users.index'), {
30 | headers: {
31 | 'Content-Type': 'application/json'
32 | }
33 | })
34 | }
--------------------------------------------------------------------------------
/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | window._ = require('lodash');
2 |
3 | /**
4 | * We'll load the axios HTTP library which allows us to easily issue requests
5 | * to our Laravel back-end. This library automatically handles sending the
6 | * CSRF token as a header based on the value of the "XSRF" token cookie.
7 | */
8 |
9 | window.axios = require('axios');
10 |
11 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
12 |
13 | /**
14 | * Echo exposes an expressive API for subscribing to channels and listening
15 | * for events that are broadcast by Laravel. Echo and event broadcasting
16 | * allows your team to easily build robust real-time web applications.
17 | */
18 |
19 | // import Echo from 'laravel-echo';
20 |
21 | // window.Pusher = require('pusher-js');
22 |
23 | // window.Echo = new Echo({
24 | // broadcaster: 'pusher',
25 | // key: process.env.MIX_PUSHER_APP_KEY,
26 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER,
27 | // forceTLS: true
28 | // });
29 |
--------------------------------------------------------------------------------
/database/migrations/2021_08_23_043232_create_disks_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->foreignId('node_id')->constrained()->onDelete('cascade');
19 | $table->string('name');
20 | $table->string('type');
21 | $table->bigInteger('total');
22 | $table->bigInteger('used');
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('disks');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/js/main.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
2 | * @license MIT */
3 |
4 | /*!
5 | * Chart.js v2.9.4
6 | * https://www.chartjs.org
7 | * (c) 2020 Chart.js Contributors
8 | * Released under the MIT License
9 | */
10 |
11 | /*!
12 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com
13 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
14 | */
15 |
16 | /*!
17 | * vuex v4.0.2
18 | * (c) 2021 Evan You
19 | * @license MIT
20 | */
21 |
22 | /**
23 | * @license
24 | * Lodash
25 | * Copyright OpenJS Foundation and other contributors
26 | * Released under MIT license
27 | * Based on Underscore.js 1.8.3
28 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
29 | */
30 |
31 | //! moment.js
32 |
33 | //! moment.js locale configuration
34 |
--------------------------------------------------------------------------------
/resources/js/components/settings/SnapshotSetting.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | View Snapshots
11 |
12 |
13 |
14 |
15 |
38 |
--------------------------------------------------------------------------------
/app/Console/Kernel.php:
--------------------------------------------------------------------------------
1 | command('inspire')->hourly();
28 | }
29 |
30 | /**
31 | * Register the commands for the application.
32 | *
33 | * @return void
34 | */
35 | protected function commands()
36 | {
37 | $this->load(__DIR__.'/Commands');
38 |
39 | require base_path('routes/console.php');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/js/Pages/Admin/Servers/Edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ server.name }}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
28 |
--------------------------------------------------------------------------------
/resources/js/components/settings/SecuritySetting.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | Open Security
11 |
12 |
13 |
14 |
15 |
38 |
--------------------------------------------------------------------------------
/database/migrations/2019_08_19_000000_create_failed_jobs_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('uuid')->unique();
19 | $table->text('connection');
20 | $table->text('queue');
21 | $table->longText('payload');
22 | $table->longText('exception');
23 | $table->timestamp('failed_at')->useCurrent();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('failed_jobs');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/Http/Middleware/AuthenticateServerAccess.php:
--------------------------------------------------------------------------------
1 | user();
22 | $server = $request->route()->parameter('server');
23 |
24 | if (!$server instanceof Server)
25 | {
26 | throw new NotFoundHttpException('Server not found');
27 | }
28 |
29 | if ($user->id !== $server->user_id && !$user->root_admin)
30 | {
31 | throw new NotFoundHttpException('Server not found');
32 | }
33 |
34 | return $next($request);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/migrations/2021_05_24_042531_create_sessions_table.php:
--------------------------------------------------------------------------------
1 | string('id')->primary();
18 | $table->foreignId('user_id')->nullable()->index();
19 | $table->string('ip_address', 45)->nullable();
20 | $table->text('user_agent')->nullable();
21 | $table->text('payload');
22 | $table->integer('last_activity')->index();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | *
29 | * @return void
30 | */
31 | public function down()
32 | {
33 | Schema::dropIfExists('sessions');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/resources/views/app.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ config('app.name', 'Laravel') }}
10 |
11 |
12 |
13 |
14 |
15 | @routes
16 | @production
17 | @vite
18 | @endproduction
19 | @env('local')
20 | @verbatim
21 |
22 | @endverbatim
23 | @endenv
24 |
25 |
26 |
27 | @inertia
28 |
29 |
30 | @env('local')
31 |
32 | @endenv
33 |
34 |
--------------------------------------------------------------------------------
/tests/Feature/RegistrationTest.php:
--------------------------------------------------------------------------------
1 | get('/register');
17 |
18 | $response->assertStatus(200);
19 | }
20 |
21 | public function test_new_users_can_register()
22 | {
23 | $response = $this->post('/register', [
24 | 'name' => 'Test User',
25 | 'email' => 'test@example.com',
26 | 'password' => 'password',
27 | 'password_confirmation' => 'password',
28 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
29 | ]);
30 |
31 | $this->assertAuthenticated();
32 | $response->assertRedirect(RouteServiceProvider::HOME);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/Nodes/StoreNodeRequest.php:
--------------------------------------------------------------------------------
1 | ['required', 'string', 'min:1', 'max:50'],
29 | 'cluster' => ['required', 'string', 'min:1', 'max:50'],
30 | 'hostname' => ['required', new Domain],
31 | 'port' => ['required', 'numeric'],
32 | 'username' => ['required', 'string'],
33 | 'password' => ['required', 'string'],
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/Nodes/UpdateNodeRequest.php:
--------------------------------------------------------------------------------
1 | ['required', 'string', 'min:1', 'max:50'],
29 | 'cluster' => ['required', 'string', 'min:1', 'max:50'],
30 | 'hostname' => ['required', new Domain],
31 | 'port' => ['required', 'numeric'],
32 | 'username' => ['nullable', 'string'],
33 | 'password' => ['nullable', 'string'],
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/migrations/2021_06_09_203702_create_plans_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->unsignedBigInteger('memory');
20 | $table->unsignedBigInteger('storage');
21 | $table->unsignedBigInteger('vcores');
22 | $table->foreignId('node_id')->constrained()->onDelete('cascade');
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('plans');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/resources/js/components/Radio.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
45 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=Laravel
2 | APP_ENV=local
3 | APP_KEY=
4 | APP_DEBUG=true
5 | APP_URL=http://localhost
6 |
7 | LOG_CHANNEL=stack
8 | LOG_LEVEL=debug
9 |
10 | DB_CONNECTION=mysql
11 | DB_HOST=127.0.0.1
12 | DB_PORT=3306
13 | DB_DATABASE=stratum_panel
14 | DB_USERNAME=root
15 | DB_PASSWORD=
16 |
17 | BROADCAST_DRIVER=log
18 | CACHE_DRIVER=file
19 | QUEUE_CONNECTION=sync
20 | SESSION_DRIVER=database
21 | SESSION_LIFETIME=120
22 |
23 | MEMCACHED_HOST=127.0.0.1
24 |
25 | REDIS_HOST=127.0.0.1
26 | REDIS_PASSWORD=null
27 | REDIS_PORT=6379
28 |
29 | MAIL_MAILER=smtp
30 | MAIL_HOST=mailhog
31 | MAIL_PORT=1025
32 | MAIL_USERNAME=null
33 | MAIL_PASSWORD=null
34 | MAIL_ENCRYPTION=null
35 | MAIL_FROM_ADDRESS=null
36 | MAIL_FROM_NAME="${APP_NAME}"
37 |
38 | AWS_ACCESS_KEY_ID=
39 | AWS_SECRET_ACCESS_KEY=
40 | AWS_DEFAULT_REGION=us-east-1
41 | AWS_BUCKET=
42 |
43 | PUSHER_APP_ID=
44 | PUSHER_APP_KEY=
45 | PUSHER_APP_SECRET=
46 | PUSHER_APP_CLUSTER=mt1
47 |
48 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
49 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
50 |
51 | SCOUT_DRIVER=database
--------------------------------------------------------------------------------
/resources/js/Pages/API/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API Tokens
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
36 |
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | [
18 | 'domain' => env('MAILGUN_DOMAIN'),
19 | 'secret' => env('MAILGUN_SECRET'),
20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
21 | ],
22 |
23 | 'postmark' => [
24 | 'token' => env('POSTMARK_TOKEN'),
25 | ],
26 |
27 | 'ses' => [
28 | 'key' => env('AWS_ACCESS_KEY_ID'),
29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
31 | ],
32 |
33 | ];
34 |
--------------------------------------------------------------------------------
/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->morphs('tokenable');
19 | $table->string('name');
20 | $table->string('token', 64)->unique();
21 | $table->text('abilities')->nullable();
22 | $table->timestamp('last_used_at')->nullable();
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('personal_access_tokens');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/ResponsiveNavLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Client/Servers/PowerController.php:
--------------------------------------------------------------------------------
1 | powerService->setServer($server)->shutdown();
18 |
19 | return $this->returnNoContent();
20 | }
21 |
22 | public function start(Server $server) {
23 | return $this->returnContent($this->powerService->setServer($server)->start());
24 | }
25 |
26 | public function kill(Server $server) {
27 | $this->powerService->setServer($server)->kill();
28 |
29 | return $this->returnNoContent();
30 | }
31 |
32 | public function restart(Server $server) {
33 | $this->powerService->setServer($server)->reboot();
34 |
35 | return $this->returnNoContent();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "esModuleInterop": true,
13 | "lib": [
14 | "ESNext",
15 | "DOM"
16 | ],
17 | "skipLibCheck": true,
18 | "paths": {
19 | "@/*": [
20 | "./resources/js/*"
21 | ],
22 | "@components/*": [
23 | "./resources/js/components/*"
24 | ],
25 | "@assets/*": [
26 | "./resources/assets/*"
27 | ],
28 | "@api/*": [
29 | "./resources/js/api/*"
30 | ],
31 | "@layouts/*": [
32 | "./resources/js/Layouts/*"
33 | ],
34 | "@pages/*": [
35 | "./resources/js/Pages/*"
36 | ]
37 | },
38 | "baseUrl": "."
39 | },
40 | "include": [
41 | "resources/**/*.ts",
42 | "resources/**/*.d.ts",
43 | "resources/**/*.tsx",
44 | "resources/**/*.vue"
45 | ]
46 | }
--------------------------------------------------------------------------------
/app/Rules/Network/Hostname.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->foreignId('user_id')->constrained()->onDelete('cascade');
20 | $table->foreignId('server_id')->constrained()->onDelete('cascade');
21 | $table->foreignId('node_id')->constrained()->onDelete('cascade');
22 | $table->unsignedBigInteger('snapshot_name');
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('snapshots');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php:
--------------------------------------------------------------------------------
1 | text('two_factor_secret')
18 | ->after('password')
19 | ->nullable();
20 |
21 | $table->text('two_factor_recovery_codes')
22 | ->after('two_factor_secret')
23 | ->nullable();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::table('users', function (Blueprint $table) {
35 | $table->dropColumn('two_factor_secret', 'two_factor_recovery_codes');
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 StratumPanel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/Feature/DeleteApiTokenTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('API support is not enabled.');
19 | }
20 |
21 | if (Features::hasTeamFeatures()) {
22 | $this->actingAs($user = User::factory()->withPersonalTeam()->create());
23 | } else {
24 | $this->actingAs($user = User::factory()->create());
25 | }
26 |
27 | $token = $user->tokens()->create([
28 | 'name' => 'Test Token',
29 | 'token' => Str::random(40),
30 | 'abilities' => ['create', 'read'],
31 | ]);
32 |
33 | $response = $this->delete('/user/api-tokens/'.$token->id);
34 |
35 | $this->assertCount(0, $user->fresh()->tokens);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/laravel-tests.yml:
--------------------------------------------------------------------------------
1 | name: Laravel
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | laravel-tests:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
16 | with:
17 | php-version: '8.1'
18 | - uses: actions/checkout@v2
19 | - name: Copy .env
20 | run: php -r "file_exists('.env') || copy('.env.example', '.env');"
21 | - name: Install Dependencies
22 | run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
23 | - name: Generate key
24 | run: php artisan key:generate
25 | - name: Directory Permissions
26 | run: chmod -R 777 storage bootstrap/cache
27 | # screw tests for now
28 | # - name: Create Database
29 | # run: |
30 | # mkdir -p database
31 | # touch database/database.sqlite
32 | # - name: Execute tests (Unit and Feature tests) via PHPUnit
33 | # env:
34 | # DB_CONNECTION: sqlite
35 | # DB_DATABASE: database/database.sqlite
36 | # run: vendor/bin/phpunit
37 |
--------------------------------------------------------------------------------
/resources/js/components/Setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{ title }}
8 |
{{ description }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
41 |
--------------------------------------------------------------------------------
/app/Providers/JetstreamServiceProvider.php:
--------------------------------------------------------------------------------
1 | configurePermissions();
29 |
30 | Jetstream::deleteUsersUsing(DeleteUser::class);
31 | }
32 |
33 | /**
34 | * Configure the permissions that are available within the application.
35 | *
36 | * @return void
37 | */
38 | protected function configurePermissions()
39 | {
40 | Jetstream::defaultApiTokenPermissions(['read']);
41 |
42 | Jetstream::permissions([
43 | 'create',
44 | 'read',
45 | 'update',
46 | 'delete',
47 | ]);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/Rules/Network/Domain.php:
--------------------------------------------------------------------------------
1 | $server,
21 | 'config' => $this->cloudinitService->setServer($server)->fetchConfig()['data'],
22 | ]);
23 | }
24 |
25 | public function update(Server $server, UpdateDisplayInfoRequest $request)
26 | {
27 | $server->name = $request->name;
28 | $server->description = $request->description;
29 | $server->save();
30 |
31 | return $request->wantsJson()
32 | ? $this->returnNoContent()
33 | : back()->with('status', 'display-information-updated');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/Services/Servers/PowerService.php:
--------------------------------------------------------------------------------
1 | instance()->status()->start()->post();
21 | }
22 |
23 | /**
24 | * @param array $params
25 | * @return mixed
26 | */
27 | public function shutdown()
28 | {
29 | return $this->instance()->status()->shutdown()->post();
30 | }
31 |
32 | /**
33 | * @param array $params
34 | * @return mixed
35 | */
36 | public function kill()
37 | {
38 | return $this->instance()->status()->stop()->post();
39 | }
40 |
41 | /**
42 | * @param string $cluster
43 | * @param string $vmid
44 | * @param array $params
45 | * @return mixed
46 | */
47 | public function reboot()
48 | {
49 | return $this->instance()->status()->reboot()->post();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/Http/Middleware/HandleInertiaRequests.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->string('email')->unique();
20 | $table->timestamp('email_verified_at')->nullable();
21 | $table->string('password');
22 | $table->boolean('root_admin')->default(false);
23 | $table->rememberToken();
24 | $table->foreignId('current_team_id')->nullable();
25 | $table->text('profile_photo_path')->nullable();
26 | $table->timestamps();
27 | });
28 | }
29 |
30 | /**
31 | * Reverse the migrations.
32 | *
33 | * @return void
34 | */
35 | public function down()
36 | {
37 | Schema::dropIfExists('users');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Actions/Fortify/CreateNewUser.php:
--------------------------------------------------------------------------------
1 | ['required', 'string', 'max:255'],
25 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
26 | 'password' => $this->passwordRules(),
27 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['required', 'accepted'] : '',
28 | ])->validate();
29 |
30 | return User::create([
31 | 'name' => $input['name'],
32 | 'email' => $input['email'],
33 | 'password' => Hash::make($input['password']),
34 | ]);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/Services/Nodes/Health/HealthService.php:
--------------------------------------------------------------------------------
1 | versionService->setNode($node)->getDetails();
28 |
29 | $latency = round((microtime(true) - $start) * 1000);
30 |
31 | Node::find($node->id)->update(['latency' => $latency, 'last_pinged' => Carbon::now()]);
32 |
33 | array_push($results, ['name' => $node->name, 'latency_in_ms' => $latency, 'success' => true]);
34 | } catch (Exception $e) {
35 | array_push($results, ['name' => $node->name, 'latency_in_ms' => 0, 'success' => false]);
36 | }
37 | }
38 |
39 | return $results;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/js/components/CardStatistic.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
11 |
{{ title }}
12 |
{{ body }}
13 |
14 |
15 |
16 |
17 |
45 |
--------------------------------------------------------------------------------
/config/view.php:
--------------------------------------------------------------------------------
1 | [
17 | resource_path('views'),
18 | ],
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Compiled View Path
23 | |--------------------------------------------------------------------------
24 | |
25 | | This option determines where all the compiled Blade templates will be
26 | | stored for your application. Typically, this is within the storage
27 | | directory. However, as usual, you are free to change this value.
28 | |
29 | */
30 |
31 | 'compiled' => env(
32 | 'VIEW_COMPILED_PATH',
33 | realpath(storage_path('framework/views'))
34 | ),
35 |
36 | ];
37 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Admin/Users/UserController.php:
--------------------------------------------------------------------------------
1 | wantsJson())
14 | {
15 | return $this->returnContent(User::all());
16 | } else {
17 | return inertia('Admin/Users/Index', [
18 | 'users' => User::select('id', 'name', 'email', 'root_admin')->paginate(20)
19 | ]);
20 | }
21 | }
22 |
23 | public function show(User $user, Request $request)
24 | {
25 | if ($request->wantsJson())
26 | {
27 | return $this->returnContent($user);
28 | } else {
29 | return inertia('Admin/Users/Show', [
30 | 'user' => [
31 | 'id' => $user->id,
32 | 'name' => $user->name,
33 | 'email' => $user->email,
34 | 'root_admin' => $user->root_admin,
35 | ]
36 | ]);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Feature/AuthenticationTest.php:
--------------------------------------------------------------------------------
1 | get('/login');
17 |
18 | $response->assertStatus(200);
19 | }
20 |
21 | public function test_users_can_authenticate_using_the_login_screen()
22 | {
23 | $user = User::factory()->create();
24 |
25 | $response = $this->post('/login', [
26 | 'email' => $user->email,
27 | 'password' => 'password',
28 | ]);
29 |
30 | $this->assertAuthenticated();
31 | $response->assertRedirect(RouteServiceProvider::HOME);
32 | }
33 |
34 | public function test_users_can_not_authenticate_with_invalid_password()
35 | {
36 | $user = User::factory()->create();
37 |
38 | $this->post('/login', [
39 | 'email' => $user->email,
40 | 'password' => 'wrong-password',
41 | ]);
42 |
43 | $this->assertGuest();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/DialogModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
51 |
--------------------------------------------------------------------------------
/app/Actions/Fortify/UpdateUserPassword.php:
--------------------------------------------------------------------------------
1 | ['required', 'string'],
24 | 'password' => $this->passwordRules(),
25 | ])->after(function ($validator) use ($user, $input) {
26 | if (! isset($input['current_password']) || ! Hash::check($input['current_password'], $user->password)) {
27 | $validator->errors()->add('current_password', __('The provided password does not match your current password.'));
28 | }
29 | })->validateWithBag('updatePassword');
30 |
31 | $user->forceFill([
32 | 'password' => Hash::make($input['password']),
33 | ])->save();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/database/migrations/2021_06_09_202301_create_nodes_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->string('cluster')->default("proxmox");
20 | $table->string('hostname');
21 | $table->string('username');
22 | $table->string('password');
23 | $table->integer('port');
24 | $table->string('auth_type');
25 | //$table->foreignId('group_id')->constrained()->onDelete('cascade');
26 | $table->integer('latency')->nullable();
27 | $table->timestamp('last_pinged')->nullable();
28 | $table->timestamps();
29 | });
30 | }
31 |
32 | /**
33 | * Reverse the migrations.
34 | *
35 | * @return void
36 | */
37 | public function down()
38 | {
39 | Schema::dropIfExists('nodes');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/public/web.config:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Admin/Servers/TemplateController.php:
--------------------------------------------------------------------------------
1 | templateService->createTemplate($server, $server->node(), ["vmid" => 101]);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Feature/DeleteAccountTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('Account deletion is not enabled.');
18 | }
19 |
20 | $this->actingAs($user = User::factory()->create());
21 |
22 | $response = $this->delete('/user', [
23 | 'password' => 'password',
24 | ]);
25 |
26 | $this->assertNull($user->fresh());
27 | }
28 |
29 | public function test_correct_password_must_be_provided_before_account_can_be_deleted()
30 | {
31 | if (! Features::hasAccountDeletionFeatures()) {
32 | return $this->markTestSkipped('Account deletion is not enabled.');
33 | }
34 |
35 | $this->actingAs($user = User::factory()->create());
36 |
37 | $response = $this->delete('/user', [
38 | 'password' => 'wrong-password',
39 | ]);
40 |
41 | $this->assertNotNull($user->fresh());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/routes/admin.php:
--------------------------------------------------------------------------------
1 | group(function () {
12 | Route::get('/', [BaseController::class, 'index'])->name('index');
13 |
14 | Route::name('nodes.')->prefix('nodes/health')->group(function () {
15 | Route::get('/', [NodeController::class, 'showHealth'])->name('health.show');
16 |
17 | Route::post('/tests', [NodeController::class, 'runTests'])->name('health.test');
18 | });
19 |
20 | Route::resource('nodes', NodeController::class);
21 |
22 |
23 | Route::prefix('servers')->put('/{server}/update-owner', [ServerController::class, 'updateOwner'])->name('servers.update-owner');
24 | Route::resource('servers', ServerController::class);
25 |
26 | Route::prefix('users')->group(function() {
27 | Route::get('/search', function (Request $request) {
28 | return User::search($request->search)->get();
29 | })->name('users.search');
30 | });
31 |
32 | Route::resource('users', UserController::class);
33 | });
--------------------------------------------------------------------------------
/tests/Feature/CreateApiTokenTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('API support is not enabled.');
18 | }
19 |
20 | if (Features::hasTeamFeatures()) {
21 | $this->actingAs($user = User::factory()->withPersonalTeam()->create());
22 | } else {
23 | $this->actingAs($user = User::factory()->create());
24 | }
25 |
26 | $response = $this->post('/user/api-tokens', [
27 | 'name' => 'Test Token',
28 | 'permissions' => [
29 | 'read',
30 | 'update',
31 | ],
32 | ]);
33 |
34 | $this->assertCount(1, $user->fresh()->tokens);
35 | $this->assertEquals('Test Token', $user->fresh()->tokens->first()->name);
36 | $this->assertTrue($user->fresh()->tokens->first()->can('read'));
37 | $this->assertFalse($user->fresh()->tokens->first()->can('delete'));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Client/Servers/BackupController.php:
--------------------------------------------------------------------------------
1 | $server,
20 | 'snapshots' => array_reverse($this->snapshotService->fetchSnapshots($server)['data']),
21 | ]);
22 | }
23 |
24 | public function createSnapshot(Server $server, Request $request)
25 | {
26 | $this->snapshotService->doSnapshot($server, $server->node(), ['snapname' => $request->name]);
27 |
28 | return $this->returnNoContent();
29 | }
30 |
31 | public function rollbackSnapshot(Server $server, Request $request)
32 | {
33 | $request->validate([
34 | 'snapname' => 'required|string'
35 | ]);
36 |
37 | $this->snapshotService->rollbackSnapshot($request->snapname, $server, $server->node());
38 |
39 | return $this->returnNoContent();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | ./tests/Unit
10 |
11 |
12 | ./tests/Feature
13 |
14 |
15 |
16 |
17 | ./app
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/Services/Servers/SnapshotService.php:
--------------------------------------------------------------------------------
1 | instance()->snapshot()->post(['snapname' => $name]);
21 | }
22 |
23 | /**
24 | * @param array $params
25 | * @return mixed
26 | */
27 | public function fetchSnapshots()
28 | {
29 | return $this->instance()->snapshot()->get();
30 | }
31 |
32 | /**
33 | * @param array $params
34 | * @return mixed
35 | */
36 | public function rollbackSnapshot(string $snapname)
37 | {
38 | // return $this->instance($server, $cluster)->snapname($snapname)->postRollback();
39 | return $this->instance()->snapshot()->snapname($snapname)->rollback()->post();
40 | }
41 |
42 | /**
43 | * @param array $params
44 | * @return mixed
45 | */
46 | public function deleteSnapshot(string $snapname)
47 | {
48 | return $this->instance()->snapshot()->snapname($snapname)->delete();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/database/migrations/2021_06_09_202437_create_servers_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->text('description')->nullable();
20 | $table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade');
21 | $table->foreignId('node_id')->constrained()->onDelete('cascade');
22 | $table->string('vnc_username')->nullable();
23 | $table->string('vnc_password')->nullable();
24 | $table->boolean('cloud_init_enabled')->default(true);
25 | $table->boolean('is_os_template')->default(false);
26 | $table->unsignedBigInteger('vmid');
27 | $table->timestamps();
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | *
34 | * @return void
35 | */
36 | public function down()
37 | {
38 | Schema::dropIfExists('servers');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/resources/js/Layouts/ServerLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
35 |
36 |
43 |
--------------------------------------------------------------------------------
/resources/js/components/Table.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 | {{ header.text }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{ item[header.value] }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
52 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | name('status'); // For some reason this wasn't working in api.php, will look into it later
22 | Route::post('/api/snapshot/{server}/take', [SnapshotController::class, 'index'])->name('snapshot');
23 | Route::get('/api/snapshot/{server}/fetch', [SnapshotController::class, 'fetch'])->name('fetchsnapshots');
24 | Route::get('/api/templates/{server}/create', [TemplateController::class, 'createTemplateFromVM'])->name('createtemplates');
25 | Route::get('/api/create/{server}', [\App\Http\Controllers\Admin\Servers\CreationController::class, 'create'])->name('createtempla2asdtes');
26 |
--------------------------------------------------------------------------------
/app/Services/Servers/BackupService.php:
--------------------------------------------------------------------------------
1 | proxmox($server, $cluster)->qemu()->vmid($server->vmid)->snapshot();
17 | }
18 |
19 | /**
20 | * @param string $name
21 | * @param array $params
22 | * @return mixed
23 | */
24 | public function doSnapshot(string $name, $server, $cluster = [])
25 | {
26 | return $this->instance($server, $cluster)->post(['snapname' => $name]);
27 | }
28 |
29 | /**
30 | * @param array $params
31 | * @return mixed
32 | */
33 | public function fetchSnapshots($server, $cluster = [])
34 | {
35 | return $this->instance($server, $cluster)->get();
36 | }
37 |
38 | /**
39 | * @param array $params
40 | * @return mixed
41 | */
42 | public function rollbackSnapshot(string $name, $server, $cluster = [])
43 | {
44 | // return $this->proxmox($server, $cluster)->qemu()->vmid($server->vmid)->snapshot()->snapname($name)->delete();
45 | return $this->instance($server, $cluster)->snapname($name)->postRollback();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/resources/js/components/FormSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
40 |
41 |
42 |
59 |
--------------------------------------------------------------------------------
/tests/Feature/PasswordConfirmationTest.php:
--------------------------------------------------------------------------------
1 | withPersonalTeam()->create()
18 | : User::factory()->create();
19 |
20 | $response = $this->actingAs($user)->get('/user/confirm-password');
21 |
22 | $response->assertStatus(200);
23 | }
24 |
25 | public function test_password_can_be_confirmed()
26 | {
27 | $user = User::factory()->create();
28 |
29 | $response = $this->actingAs($user)->post('/user/confirm-password', [
30 | 'password' => 'password',
31 | ]);
32 |
33 | $response->assertRedirect();
34 | $response->assertSessionHasNoErrors();
35 | }
36 |
37 | public function test_password_is_not_confirmed_with_invalid_password()
38 | {
39 | $user = User::factory()->create();
40 |
41 | $response = $this->actingAs($user)->post('/user/confirm-password', [
42 | 'password' => 'wrong-password',
43 | ]);
44 |
45 | $response->assertSessionHasErrors();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/resources/js/main.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | //require('./bootstrap')
3 |
4 | import {
5 | App as InertiaApp,
6 | createInertiaApp,
7 | plugin as InertiaPlugin,
8 | } from '@inertiajs/inertia-vue3'
9 | import { createApp, h } from 'vue'
10 | import { Inertia } from '@inertiajs/inertia'
11 | import '@/plugins/axios'
12 | import store from '@/state'
13 | import NProgress from 'nprogress'
14 | import '../css/app.css'
15 |
16 | function resolvePageComponent(name: string, pages: Record) {
17 | for (const path in pages) {
18 | if (path.endsWith(`${name.replace('.', '/')}.vue`)) {
19 | return typeof pages[path] === 'function'
20 | ? pages[path]()
21 | : pages[path]
22 | }
23 | }
24 |
25 | throw new Error(`Page not found: ${name}`)
26 | }
27 |
28 | createInertiaApp({
29 | resolve: (name) => resolvePageComponent(name, import.meta.glob('./Pages/**/*.vue')),
30 | setup({ el, app, props, plugin }) {
31 | createApp({ render: () => h(app, props) })
32 | .mixin({ methods: { route } })
33 | .use(plugin)
34 | .use(store)
35 | .mount(el)
36 | },
37 | })
38 |
39 | Inertia.on('start', () => NProgress.start())
40 | Inertia.on('finish', (event) => {
41 | if (event.detail.visit.completed) {
42 | NProgress.done()
43 | } else if (event.detail.visit.interrupted) {
44 | NProgress.set(0)
45 | } else if (event.detail.visit.cancelled) {
46 | NProgress.done()
47 | NProgress.remove()
48 | }
49 | })
50 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/FormSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
22 |
23 |
24 |
25 |
42 |
--------------------------------------------------------------------------------
/resources/js/state/server/status.ts:
--------------------------------------------------------------------------------
1 | import { ServerState, FormattedBytes } from '@api/server/getStatus'
2 |
3 | export interface ServerStatus {
4 | id: number;
5 | state: ServerState;
6 | cpu: number;
7 | mem: FormattedBytes;
8 | maxmem: FormattedBytes;
9 | memUnparsed: MemUnparsed;
10 | }
11 |
12 | export interface MemUnparsed {
13 | mem: number;
14 | maxmem: number;
15 | }
16 |
17 | const status = {
18 | namespaced: true,
19 |
20 | state: () => ({
21 | id: NaN,
22 | state: 'querying',
23 | cpu: 0,
24 | mem: { size: 0, unit: 'B' },
25 | maxmem: { size: 0, unit: 'B' },
26 | memUnparsed: {
27 | mem: 0,
28 | maxmem: 0,
29 | },
30 | }),
31 |
32 | mutations: {
33 | setStatus(state: ServerStatus, payload: ServerStatus) {
34 | Object.assign(state, payload)
35 | },
36 | setState(state: ServerStatus, payload: ServerState) {
37 | state.state = payload
38 | }
39 | },
40 |
41 | actions: {
42 | setStatus(context: any, payload: ServerStatus) {
43 | context.commit('setStatus', payload)
44 | },
45 |
46 | clearStatus(context: any) {
47 | context.commit('setStatus', {
48 | id: NaN,
49 | state: 'querying',
50 | cpu: 0,
51 | mem: { size: 0, unit: 'B' },
52 | maxmem: { size: 0, unit: 'B' },
53 | memUnparsed: {
54 | mem: 0,
55 | maxmem: 0,
56 | },
57 | })
58 | },
59 | },
60 | }
61 |
62 | export default status
63 |
--------------------------------------------------------------------------------
/resources/js/Pages/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Looks like there are no servers
13 |
14 |
15 |
16 |
23 |
24 |
25 |
26 |
27 |
57 |
--------------------------------------------------------------------------------
/app/Providers/FortifyServiceProvider.php:
--------------------------------------------------------------------------------
1 | by($request->email.$request->ip());
41 | });
42 |
43 | RateLimiter::for('two-factor', function (Request $request) {
44 | return Limit::perMinute(5)->by($request->session()->get('login.id'));
45 | });
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Feature/ApiTokenPermissionsTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('API support is not enabled.');
19 | }
20 |
21 | if (Features::hasTeamFeatures()) {
22 | $this->actingAs($user = User::factory()->withPersonalTeam()->create());
23 | } else {
24 | $this->actingAs($user = User::factory()->create());
25 | }
26 |
27 | $token = $user->tokens()->create([
28 | 'name' => 'Test Token',
29 | 'token' => Str::random(40),
30 | 'abilities' => ['create', 'read'],
31 | ]);
32 |
33 | $response = $this->put('/user/api-tokens/'.$token->id, [
34 | 'name' => $token->name,
35 | 'permissions' => [
36 | 'delete',
37 | 'missing-permission',
38 | ],
39 | ]);
40 |
41 | $this->assertTrue($user->fresh()->tokens->first()->can('delete'));
42 | $this->assertFalse($user->fresh()->tokens->first()->can('read'));
43 | $this->assertFalse($user->fresh()->tokens->first()->can('missing-permission'));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/resources/js/Pages/Admin/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
16 |
22 |
28 |
29 |
30 |
31 |
32 |
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "vite",
5 | "build": "vite build"
6 | },
7 | "devDependencies": {
8 | "@babel/preset-flow": "^7.14.5",
9 | "@inertiajs/inertia": "^0.11.0",
10 | "@inertiajs/inertia-vue3": "^0.6.0",
11 | "@tailwindcss/forms": "^0.5.0",
12 | "@tailwindcss/typography": "^0.5.2",
13 | "@types/chart.js": "^2.9.35",
14 | "@types/nprogress": "^0.2.0",
15 | "@types/ziggy-js": "^1.3.1",
16 | "@typescript-eslint/eslint-plugin": "^4.4.0",
17 | "@typescript-eslint/parser": "^4.4.0",
18 | "@vue/compiler-sfc": "^3.2.31",
19 | "autoprefixer": "^10.3.1",
20 | "axios": "^0.21",
21 | "lodash": "^4.17.21",
22 | "postcss": "^8.3.6",
23 | "postcss-import": "^14.0.2",
24 | "tailwindcss": "^3.0.0",
25 | "ts-loader": "^9.2.9",
26 | "typescript": "^4.6.3",
27 | "vite": "^2.9.9",
28 | "vite-plugin-laravel": "^0.2.0-beta.12",
29 | "vue": "^3.2.31",
30 | "vue-loader": "^17.0.0"
31 | },
32 | "dependencies": {
33 | "@fortawesome/fontawesome-svg-core": "^1.2.35",
34 | "@fortawesome/free-solid-svg-icons": "^5.15.3",
35 | "@fortawesome/vue-fontawesome": "^3.0.0-4",
36 | "@headlessui/vue": "^1.6.0",
37 | "@heroicons/vue": "^1.0.1",
38 | "@vitejs/plugin-vue": "^2.3.3",
39 | "chart.js": "^2.9.4",
40 | "deepmerge": "^4.2.2",
41 | "nprogress": "^0.2.0",
42 | "vuex": "^4.0.2",
43 | "ziggy-js": "^1.4.6"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Client/Servers/SnapshotController.php:
--------------------------------------------------------------------------------
1 | $server,
21 | 'snapshots' => array_reverse($this->snapshotService->setServer($server)->fetchSnapshots()['data']),
22 | ]);
23 | }
24 |
25 | public function create(Server $server, SnapshotRequest $request)
26 | {
27 | $this->snapshotService->setServer($server)->doSnapshot($request->name);
28 |
29 | return $this->returnInertiaResponse($request, 'snapshot-created');
30 | }
31 |
32 | public function delete(Server $server, SnapshotRequest $request)
33 | {
34 |
35 | $this->snapshotService->setServer($server)->deleteSnapshot($request->name);
36 |
37 | return $this->returnNoContent();
38 | }
39 |
40 | public function rollback(Server $server, SnapshotRequest $request)
41 | {
42 | $this->snapshotService->setServer($server)->rollbackSnapshot($request->name);
43 |
44 | return $this->returnNoContent();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/Services/ProxmoxService.php:
--------------------------------------------------------------------------------
1 | server = $server;
28 |
29 | $this->setNode($this->server->node);
30 |
31 | return $this;
32 | }
33 |
34 | /**
35 | * Set the node model this request is stemming from.
36 | *
37 | * @return $this
38 | */
39 | public function setNode(Node $node)
40 | {
41 | $this->node = $node;
42 |
43 | return $this;
44 | }
45 |
46 | public function instance()
47 | {
48 | return $this->proxmox()->nodes()->node($this->node->cluster)->qemu()->vmid($this->server->vmid);
49 | }
50 |
51 | public function mainInstance()
52 | {
53 | return $this->proxmox();
54 | }
55 |
56 | public function proxmox()
57 | {
58 | Assert::isInstanceOf($this->node, Node::class);
59 |
60 | $node = [
61 | $this->node->hostname,
62 | $this->node->username,
63 | $this->node->password,
64 | intval($this->node->port),
65 | $this->node->auth_type
66 | ];
67 |
68 | $proxmox = new PVE(...$node);
69 |
70 | return $proxmox;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/resources/js/components/UsageBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{ name }}
7 |
{{ data ? data : 'N/A' }}
8 |
{{ unit }}
9 |
10 |
11 |
12 |
56 |
--------------------------------------------------------------------------------
/resources/js/Pages/Admin/Servers/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
63 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Admin/Servers/ServerController.php:
--------------------------------------------------------------------------------
1 | Server::with(['node', 'owner'])->paginate(20)->through(function ($item) {
20 | return [
21 | 'id' => $item->id,
22 | 'name' => $item->name,
23 | 'vmid' => $item->vmid,
24 | 'node_id' => $item->node_id,
25 | 'user_id' => $item->user_id,
26 | 'node_name' => $item->node->name,
27 | 'user_name' => $item->owner->name,
28 | ];
29 | })
30 | ]);
31 | }
32 |
33 | public function edit(Server $server)
34 | {
35 | return inertia('Admin/Servers/Edit', [
36 | 'server' => $server,
37 | 'config' => $this->cloudinitService->setServer($server)->fetchConfig()['data'],
38 | ]);
39 | }
40 |
41 | public function updateOwner(UpdateOwnerRequest $request, Server $server)
42 | {
43 | $server->update($request->validated());
44 |
45 | return $this->returnInertiaResponse($request, 'owner-updated');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/resources/js/Pages/Admin/Users/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
66 |
--------------------------------------------------------------------------------
/resources/js/state/alerts.ts:
--------------------------------------------------------------------------------
1 | import { FontawesomeObject } from '@fortawesome/fontawesome-svg-core'
2 |
3 | export interface AlertMessage {
4 | message: string;
5 | icon: null | FontawesomeObject;
6 | }
7 |
8 | export interface CreateAlertPayload extends AlertMessage {
9 | timeout?: boolean;
10 | timeoutDuration?: number;
11 | }
12 |
13 | const alerts = {
14 | namespaced: true,
15 |
16 | state: (): AlertMessage => ({
17 | message: '',
18 | icon: null,
19 | }),
20 | mutations: {
21 | setMessage (state: AlertMessage, message: string) {
22 | state.message = message
23 | },
24 |
25 | setIcon (state: AlertMessage, icon: null | FontawesomeObject) {
26 | state.icon = icon
27 | }
28 | },
29 | actions: {
30 | clearAlerts (context: any) { // I hate using 'any'
31 | context.commit('setMessage', '')
32 | context.commit('setIcon', null)
33 | },
34 |
35 | clearSpecificAlert (context: any, message: string) {
36 | if (context.state.message === message) {
37 | context.dispatch('clearAlerts')
38 | }
39 | },
40 |
41 | createAlert (context: any, payload: CreateAlertPayload) {
42 | context.commit('setMessage', payload.message)
43 | context.commit('setIcon', payload.icon)
44 |
45 | if (payload.timeout === undefined || payload.timeout) {
46 | setTimeout(() => {
47 | context.dispatch('clearSpecificAlert', payload.message)
48 | }, (payload.timeoutDuration) ? payload.timeoutDuration : 3000);
49 | }
50 | }
51 | }
52 | }
53 |
54 | export default alerts
--------------------------------------------------------------------------------
/resources/js/api/server/getStatus.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | faPlay,
4 | faClock,
5 | faStop,
6 | faExclamationCircle,
7 | } from '@fortawesome/free-solid-svg-icons'
8 |
9 | export const refreshTime: number = 2000 // milliseconds
10 |
11 | export interface FormattedBytes {
12 | size: number;
13 | unit: string;
14 | }
15 |
16 | export type ServerState = 'querying' | 'stopped' | 'running' | 'error'
17 |
18 | export interface iconStateInterface {
19 | [key: string]: {
20 | backgroundColor: string;
21 | textColor: string;
22 | icon: any;
23 | }
24 | }
25 |
26 | export const iconState: iconStateInterface = {
27 | querying: {
28 | backgroundColor: 'bg-gray-200',
29 | textColor: 'text-gray-700',
30 | icon: faClock,
31 | },
32 | stopped: {
33 | backgroundColor: 'bg-red-200',
34 | textColor: 'text-red-700',
35 | icon: faStop,
36 | },
37 | running: {
38 | backgroundColor: 'bg-green-200',
39 | textColor: 'text-green-700',
40 | icon: faPlay,
41 | },
42 | error: {
43 | backgroundColor: 'bg-gray-200',
44 | textColor: 'text-gray-700',
45 | icon: faExclamationCircle,
46 | },
47 | }
48 |
49 | export function formatBytes(bytes: number, decimals = 2): FormattedBytes {
50 | if (bytes === 0) return { size: 0, unit: 'B' }
51 |
52 | const k = 1024
53 | const dm = decimals < 0 ? 0 : decimals
54 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
55 |
56 | const i = Math.floor(Math.log(bytes) / Math.log(k))
57 |
58 | return {
59 | size: parseFloat((bytes / Math.pow(k, i)).toFixed(dm)),
60 | unit: sizes[i],
61 | }
62 | }
63 |
64 | export default (id: number) => {
65 | // @ts-ignore
66 | return axios.get(route('servers.show.status', id), {hideProgress: true})
67 | }
68 |
--------------------------------------------------------------------------------
/config/hashing.php:
--------------------------------------------------------------------------------
1 | 'bcrypt',
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Bcrypt Options
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may specify the configuration options that should be used when
26 | | passwords are hashed using the Bcrypt algorithm. This will allow you
27 | | to control the amount of time it takes to hash the given password.
28 | |
29 | */
30 |
31 | 'bcrypt' => [
32 | 'rounds' => env('BCRYPT_ROUNDS', 10),
33 | ],
34 |
35 | /*
36 | |--------------------------------------------------------------------------
37 | | Argon Options
38 | |--------------------------------------------------------------------------
39 | |
40 | | Here you may specify the configuration options that should be used when
41 | | passwords are hashed using the Argon algorithm. These will allow you
42 | | to control the amount of time it takes to hash the given password.
43 | |
44 | */
45 |
46 | 'argon' => [
47 | 'memory' => 1024,
48 | 'threads' => 2,
49 | 'time' => 2,
50 | ],
51 |
52 | ];
53 |
--------------------------------------------------------------------------------
/app/Services/Servers/CloudinitService.php:
--------------------------------------------------------------------------------
1 | instance()->config()->get();
20 | }
21 |
22 | /**
23 | * @param string $password
24 | * @param array $params
25 | * @return mixed
26 | */
27 | public function changePassword(string $password, AuthenticationType $type)
28 | {
29 | return $this->instance()->config()->post([$type->value => $password]);
30 | }
31 |
32 | /**
33 | * @param BiosType $type
34 | * @param array $params
35 | * @return mixed
36 | */
37 | // Generally needed for Windows VM's with over 2TB disk, still WIP since I still need to add EFI disk
38 | public function changeBIOS(BiosType $type)
39 | {
40 | return $this->instance()->config()->post(['bios' => $type->value]);
41 | }
42 |
43 | /**
44 | * @param string $hostname
45 | * @param array $params
46 | * @return mixed
47 | */
48 | public function changeHostname(string $hostname)
49 | {
50 | return $this->instance()->config()->post(['seachdomain' => $hostname]);
51 | }
52 |
53 | /**
54 | * @param string $dns
55 | * @param array $params
56 | * @return mixed
57 | */
58 | public function changeNameserver(string $nameserver)
59 | {
60 | return $this->instance()->config()->post(['nameserver' => $nameserver]);
61 | }
62 |
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/resources/js/Pages/Admin/Users/Show.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Update Profile
5 |
6 |
7 | Ensure your account is using a long, random password to stay secure.
8 |
9 |
10 |
11 |
12 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Saved.
27 |
28 |
29 |
33 | Save
34 |
35 |
36 |
37 |
38 |
39 |
40 |
58 |
--------------------------------------------------------------------------------
/tests/Feature/UpdatePasswordTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->create());
17 |
18 | $response = $this->put('/user/password', [
19 | 'current_password' => 'password',
20 | 'password' => 'new-password',
21 | 'password_confirmation' => 'new-password',
22 | ]);
23 |
24 | $this->assertTrue(Hash::check('new-password', $user->fresh()->password));
25 | }
26 |
27 | public function test_current_password_must_be_correct()
28 | {
29 | $this->actingAs($user = User::factory()->create());
30 |
31 | $response = $this->put('/user/password', [
32 | 'current_password' => 'wrong-password',
33 | 'password' => 'new-password',
34 | 'password_confirmation' => 'new-password',
35 | ]);
36 |
37 | $response->assertSessionHasErrors();
38 |
39 | $this->assertTrue(Hash::check('password', $user->fresh()->password));
40 | }
41 |
42 | public function test_new_passwords_must_match()
43 | {
44 | $this->actingAs($user = User::factory()->create());
45 |
46 | $response = $this->put('/user/password', [
47 | 'current_password' => 'password',
48 | 'password' => 'new-password',
49 | 'password_confirmation' => 'wrong-password',
50 | ]);
51 |
52 | $response->assertSessionHasErrors();
53 |
54 | $this->assertTrue(Hash::check('password', $user->fresh()->password));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | singleton(
30 | Illuminate\Contracts\Http\Kernel::class,
31 | App\Http\Kernel::class
32 | );
33 |
34 | $app->singleton(
35 | Illuminate\Contracts\Console\Kernel::class,
36 | App\Console\Kernel::class
37 | );
38 |
39 | $app->singleton(
40 | Illuminate\Contracts\Debug\ExceptionHandler::class,
41 | App\Exceptions\Handler::class
42 | );
43 |
44 | /*
45 | |--------------------------------------------------------------------------
46 | | Return The Application
47 | |--------------------------------------------------------------------------
48 | |
49 | | This script returns the application instance. The instance is given to
50 | | the calling script so we can separate the building of the instances
51 | | from the actual running of the application and sending responses.
52 | |
53 | */
54 |
55 | return $app;
56 |
--------------------------------------------------------------------------------
/app/Models/User.php:
--------------------------------------------------------------------------------
1 | 'datetime',
53 | ];
54 |
55 | /**
56 | * The accessors to append to the model's array form.
57 | *
58 | * @var array
59 | */
60 | protected $appends = [
61 | 'profile_photo_url',
62 | ];
63 |
64 | public function toSearchableArray()
65 | {
66 | $modified = [];
67 | $modified['name'] = $this->name;
68 |
69 | return $modified;
70 | }
71 |
72 | public function servers()
73 | {
74 | return $this->hasMany(Server::class);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/artisan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | make(Illuminate\Contracts\Console\Kernel::class);
34 |
35 | $status = $kernel->handle(
36 | $input = new Symfony\Component\Console\Input\ArgvInput,
37 | new Symfony\Component\Console\Output\ConsoleOutput
38 | );
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Shutdown The Application
43 | |--------------------------------------------------------------------------
44 | |
45 | | Once Artisan has finished running, we will fire off the shutdown events
46 | | so that any final work may be done by the application before we shut
47 | | down the process. This is the last thing to happen to the request.
48 | |
49 | */
50 |
51 | $kernel->terminate($input, $status);
52 |
53 | exit($status);
54 |
--------------------------------------------------------------------------------
/resources/js/components/Pagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Previous
10 |
11 |
12 |
13 | {{ link.label }}
18 |
19 |
20 | {{ link.label }}
26 |
27 |
28 |
29 | Next
34 |
35 |
36 |
37 |
38 |
39 |
46 |
--------------------------------------------------------------------------------
/config/sanctum.php:
--------------------------------------------------------------------------------
1 | explode(',', env(
17 | 'SANCTUM_STATEFUL_DOMAINS',
18 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1,'.parse_url(env('APP_URL'), PHP_URL_HOST)
19 | )),
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | Expiration Minutes
24 | |--------------------------------------------------------------------------
25 | |
26 | | This value controls the number of minutes until an issued token will be
27 | | considered expired. If this value is null, personal access tokens do
28 | | not expire. This won't tweak the lifetime of first-party sessions.
29 | |
30 | */
31 |
32 | 'expiration' => null,
33 |
34 | /*
35 | |--------------------------------------------------------------------------
36 | | Sanctum Middleware
37 | |--------------------------------------------------------------------------
38 | |
39 | | When authenticating your first-party SPA with Sanctum you may need to
40 | | customize some of the middleware Sanctum uses while processing the
41 | | request. You may change the middleware listed below as required.
42 | |
43 | */
44 |
45 | 'middleware' => [
46 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
47 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
48 | ],
49 |
50 | ];
51 |
--------------------------------------------------------------------------------