Initial commit

This commit is contained in:
dyzcdn
2025-12-22 12:03:01 +07:00
commit 10dc345147
367 changed files with 31188 additions and 0 deletions

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

108
.env.example Normal file
View File

@@ -0,0 +1,108 @@
APP_NAME="DyDev APP"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
# Database Configuration
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dy_app_db
DB_USERNAME=root
DB_PASSWORD=
# Session Configuration
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
# Broadcasting & Filesystem
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
# Cache Configuration
CACHE_STORE=database
# CACHE_PREFIX=
# Memcached Configuration
MEMCACHED_HOST=127.0.0.1
# Redis Configuration
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
# Mail Configuration
MAIL_MAILER=smtp
# MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=587
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
# AWS Configuration
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
# VITE Configuration
VITE_APP_NAME="${APP_NAME}"
# Social Authentication (OAuth) Configuration
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URI=${APP_URL}/auth/github/callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=${APP_URL}/auth/google/callback
# CA ROOT
CA_ROOT_COUNTRY_NAME="ID"
CA_ROOT_ORGANIZATION_NAME="DyDev TrustLab OpenSource"
CA_ROOT_ORGANIZATIONAL_UNIT_NAME="www.dyzulk.com"
CA_ROOT_COMMON_NAME="DyzulkDev"
# CA INTERMEDIATE 4096
CA_4096_COUNTRY_NAME="ID"
CA_4096_ORGANIZATION_NAME="DyDev TrustLab"
CA_4096_ORGANIZATIONAL_UNIT_NAME="www.dyzulk.com"
CA_4096_COMMON_NAME="DyDev Infinity CA1"
# CA INTERMEDIATE 2048
CA_2048_COUNTRY_NAME="ID"
CA_2048_ORGANIZATION_NAME="Twinpath TrustLab"
CA_2048_ORGANIZATIONAL_UNIT_NAME="www.dyzulk.com"
CA_2048_COMMON_NAME="Twinpath Infinity CA2"
# CA LEAF DEFAULT
CA_LEAF_DEFAULT_COUNTRY_NAME="ID"
CA_LEAF_DEFAULT_LOCALITY="Jakarta"
CA_LEAF_DEFAULT_STATE="DKI Jakarta"
CA_LEAF_DEFAULT_ORGANIZATION_NAME="MyLab Secured"

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
.DS_Store
Thumbs.db
*.log
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
Homestead.json
Homestead.yaml
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.phpunit.result.cache
# SQLite
/database/*.sqlite
/database/*.sqlite-journal
# Compiled assets
mix-manifest.json

124
README.md Normal file
View File

@@ -0,0 +1,124 @@
# Certificate Authority & API Management System
A robust, modern platform for managing Root CAs, Intermediate CAs, and Leaf Certificates with an integrated API management system. Built on **Laravel 12**, **Tailwind CSS v4**, and **Alpine.js**.
## 🚀 Key Features
* **CA Management**: Securely manage Root and Intermediate CAs.
* **Certificate Issuance**: Issue and manage Leaf certificates for users.
* **API Key System**: Advanced API key management with:
* **Regeneration**: Securely rotate keys with a single click.
* **Activity Tracking**: Real-time "Last Used" monitoring.
* **Public/Private Endpoints**: Documentation with interactive tabs and code snippets.
* **AJAX-Powered UI**: Zero-refresh search, pagination, and status toggles.
* **Dynamic Dashboard**: Real-time metrics, certificate issuance trends, and server latency monitoring.
* **Modern Interactive UI**: High-performance dashboard with vibrant metrics and dark mode support.
## 🛠️ Built With
* **Laravel 12**: Secure and scalable backend framework.
* **Tailwind CSS v4**: Modern, utility-first styling.
* **Alpine.js**: Lightweight reactivity.
* **Chart.js**: Visual trend analysis.
## 🚦 Quick Start
### 1. Requirements
* **PHP 8.2+** with the following extensions:
* `openssl` (Required for SSL/TLS operations)
* `zip` (Required for certificate bundle downloads)
* `bcmath` (Required for large serial number handling)
* `mbstring`, `xml`, `curl`, `ctype`, `filter` (Standard Laravel requirements)
* **Node.js 18+** & NPM
* **OpenSSL CLI** (Ensure it is accessible in your system PATH)
> [!NOTE]
> Default PHP installations on Windows (XAMPP/WAMP), Mac (Homebrew), and Linux (apt/yum) often vary. Please ensure the extensions above are enabled in your `php.ini`.
### 2. Setup
#### Option A: Terminal Access
```bash
# Clone and enter
git clone <your-repo-url>
cd app-tail
# Install dependencies
composer install
npm install
# Setup environment
cp .env.example .env
php artisan key:generate
```
#### Option B: Manual (No Terminal/Shared Hosting)
1. **Download**: Click the "Code" button on GitHub and select **Download ZIP**, then extract it to your local computer.
2. **Dependencies**:
* Run `composer install` and `npm run build` on your **local computer**.
* Upload the entire project folder to your server via FTP/File Manager, **including** the `vendor` and `public/build` directories.
3. **Environment**:
* Rename `.env.example` to `.env` using your hosting File Manager.
* **APP_KEY**: Since you cannot run `key:generate`, visit `yourdomain.com/key-gen.html` to generate a secure key, then paste it into the `APP_KEY=` field in your `.env`.
### 3. Database & Migrations
#### Option A: Terminal Access (Recommended)
```bash
php artisan migrate --seed
```
#### Option B: Manual Import (Shared Hosting)
If your hosting does not provide terminal access:
1. Create a new database via your hosting panel (e.g., cPanel MySQL Wizard).
2. Open **phpMyAdmin**.
3. Select your database and go to the **Import** tab.
4. Choose the file `database/install.sql` from this project and click **Go**.
* **Default Admin**: `admin@dyzulk.com`
* **Default Password**: `password`
## 🚀 Production Deployment
### 1. Optimize Environment
Update your `.env` for production:
```env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
```
### 2. Assets & Storage Link
#### Terminal Method
```bash
npm run build
php artisan storage:link
php artisan optimize
```
#### Manual Method (No Terminal)
1. **Assets**: Ensure you have uploaded the `public/build` folder from your local machine after running `npm run build`.
2. **Storage Link**: Create a file named `link.php` in your `public/` directory with this content:
```php
<?php
symlink(__DIR__.'/../storage/app/public', __DIR__.'/storage');
echo "Storage link created!";
```
Visit `yourdomain.com/link.php` in your browser, then delete the file.
3. **Optimization**: To clear cache manually, delete all files inside `storage/framework/views/` and `bootstrap/cache/` (except `.gitignore`).
> [!IMPORTANT]
> **Web Server Root**: Ensure your domain/subdomain points to the `/public` directory of this project, not the root folder.
## 📡 API Endpoints
### Public CA Certificates
`GET /api/public/ca-certificates`
Returns Root and Intermediate CA certificates in JSON format.
### Authenticated Certificates
`GET /api/v1/certificates`
Retrieves user-specific leaf certificates. Requires `X-API-KEY` header.
## 📦 License
Refer to the [LICENSE](LICENSE) file for details.

223
app/Helpers/MenuHelper.php Normal file
View File

@@ -0,0 +1,223 @@
<?php
namespace App\Helpers;
class MenuHelper
{
public static function getMainNavItems()
{
return [
[
'name' => 'Dashboard',
'icon' => 'dashboard',
'route_name' => 'dashboard',
],
[
'name' => 'Certificate',
'icon' => 'certificate',
'subItems' => [
['name' => 'Certificate Management', 'route_name' => 'certificate.index', 'pro' => false],
['name' => 'Create Certificate', 'route_name' => 'certificate.create', 'pro' => false],
],
],
[
'name' => 'Calendar',
'icon' => 'calendar',
'route_name' => 'calendar',
],
[
'name' => 'Forms',
'icon' => 'forms',
'subItems' => [
['name' => 'Form Elements', 'route_name' => 'form-elements', 'pro' => false],
],
],
[
'name' => 'Tables',
'icon' => 'tables',
'subItems' => [
['name' => 'Basic Tables', 'route_name' => 'basic-tables', 'pro' => false]
],
],
[
'name' => 'API Keys',
'icon' => 'api-key',
'route_name' => 'api-keys.index',
],
[
'name' => 'Pages',
'icon' => 'pages',
'subItems' => [
['name' => 'Blank Page', 'route_name' => 'blank', 'pro' => false],
['name' => '404 Error', 'route_name' => 'error-404', 'pro' => false],
// ['name' => 'API Keys', 'route_name' => 'api-keys.index', 'pro' => false, 'new' => true]
],
],
];
}
public static function getMyAccountItems()
{
return [
[
'name' => 'User Profile',
'icon' => 'user-profile',
'route_name' => 'profile',
],
[
'name' => 'Account Settings',
'icon' => 'settings',
'route_name' => 'settings',
],
];
}
public static function getOthersItems()
{
return [
[
'name' => 'Charts',
'icon' => 'charts',
'subItems' => [
['name' => 'Line Chart', 'route_name' => 'line-chart', 'pro' => false],
['name' => 'Bar Chart', 'route_name' => 'bar-chart', 'pro' => false]
],
],
[
'name' => 'UI Elements',
'icon' => 'ui-elements',
'subItems' => [
['name' => 'Alerts', 'route_name' => 'alerts', 'pro' => false],
['name' => 'Avatar', 'route_name' => 'avatars', 'pro' => false],
['name' => 'Badge', 'route_name' => 'badges', 'pro' => false],
['name' => 'Buttons', 'route_name' => 'buttons', 'pro' => false],
['name' => 'Images', 'route_name' => 'images', 'pro' => false],
['name' => 'Videos', 'route_name' => 'videos', 'pro' => false],
],
],
[
'name' => 'Authentication',
'icon' => 'authentication',
'subItems' => [
['name' => 'Sign In', 'route_name' => 'signin', 'pro' => false],
['name' => 'Sign Up', 'route_name' => 'signup', 'pro' => false],
],
],
];
}
public static function getMenuGroups()
{
$groups = [];
// Admin Menu
if (auth()->check() && auth()->user()->isAdmin()) {
$groups[] = [
'title' => 'Admin Management',
'items' => [
[
'name' => 'User Management',
'icon' => 'users',
'route_name' => 'admin.users.index',
],
[
'name' => 'Root CA Management',
'icon' => 'certificate',
'route_name' => 'admin.root-ca.index',
],
]
];
}
// Filter Main Items based on role
$mainItems = self::getMainNavItems();
if (!auth()->check() || !auth()->user()->isAdmin()) {
$mainItems = array_values(array_filter($mainItems, function ($item) {
return in_array($item['name'], ['Dashboard', 'Certificate', 'API Keys']);
}));
}
// Standard Menus
$groups[] = [
'title' => 'Menu',
'items' => $mainItems
];
$groups[] = [
'title' => 'My Account',
'items' => self::getMyAccountItems()
];
// Others - Admin Only
if (auth()->check() && auth()->user()->isAdmin()) {
$groups[] = [
'title' => 'Others',
'items' => self::getOthersItems()
];
}
return $groups;
}
public static function isActive($item)
{
if (isset($item['route_name'])) {
return request()->routeIs($item['route_name']);
}
if (isset($item['subItems'])) {
foreach ($item['subItems'] as $subItem) {
if (isset($subItem['route_name']) && request()->routeIs($subItem['route_name'])) {
return true;
}
}
}
return false;
}
public static function getIconSvg($iconName)
{
$icons = [
'certificate' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 5.75C2 4.23122 3.23122 3 4.75 3H19.25C20.7688 3 22 4.23122 22 5.75V15.25C22 16.7688 20.7688 18 19.25 18H10V17.0005C10.12 16.8408 10.2306 16.6737 10.3311 16.5H19.25C19.9404 16.5 20.5 15.9404 20.5 15.25V5.75C20.5 5.05964 19.9404 4.5 19.25 4.5H4.75C4.05964 4.5 3.5 5.05964 3.5 5.75V9.66891C2.91464 10.0075 2.40429 10.4614 2 10.9995V5.75ZM6.75 7C6.33579 7 6 7.33579 6 7.75C6 8.16421 6.33579 8.5 6.75 8.5H17.25C17.6642 8.5 18 8.16421 18 7.75C18 7.33579 17.6642 7 17.25 7H6.75ZM12.75 12C12.3358 12 12 12.3358 12 12.75C12 13.1642 12.3358 13.5 12.75 13.5H17.25C17.6642 13.5 18 13.1642 18 12.75C18 12.3358 17.6642 12 17.25 12H12.75ZM5.99967 10C3.79017 10 1.99902 11.7911 1.99902 14.0006C1.99902 16.2101 3.79017 18.0013 5.99967 18.0013C8.20916 18.0013 10.0003 16.2101 10.0003 14.0006C10.0003 11.7911 8.20916 10 5.99967 10ZM9.00076 18.001C8.16487 18.6291 7.12573 19.0013 5.99967 19.0013C4.8745 19.0013 3.83612 18.6297 3.00058 18.0025L3.0001 21.2487C3.0001 21.8195 3.6046 22.1681 4.09019 21.9176L4.17966 21.8635L6.00002 20.5912L7.81967 21.8635C8.28757 22.1904 8.91959 21.8946 8.99232 21.353L8.99923 21.2487L9.00076 18.001Z" fill="currentColor"/></svg>',
'dashboard' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 3.25C4.25736 3.25 3.25 4.25736 3.25 5.5V8.99998C3.25 10.2426 4.25736 11.25 5.5 11.25H9C10.2426 11.25 11.25 10.2426 11.25 8.99998V5.5C11.25 4.25736 10.2426 3.25 9 3.25H5.5ZM4.75 5.5C4.75 5.08579 5.08579 4.75 5.5 4.75H9C9.41421 4.75 9.75 5.08579 9.75 5.5V8.99998C9.75 9.41419 9.41421 9.74998 9 9.74998H5.5C5.08579 9.74998 4.75 9.41419 4.75 8.99998V5.5ZM5.5 12.75C4.25736 12.75 3.25 13.7574 3.25 15V18.5C3.25 19.7426 4.25736 20.75 5.5 20.75H9C10.2426 20.75 11.25 19.7427 11.25 18.5V15C11.25 13.7574 10.2426 12.75 9 12.75H5.5ZM4.75 15C4.75 14.5858 5.08579 14.25 5.5 14.25H9C9.41421 14.25 9.75 14.5858 9.75 15V18.5C9.75 18.9142 9.41421 19.25 9 19.25H5.5C5.08579 19.25 4.75 18.9142 4.75 18.5V15ZM12.75 5.5C12.75 4.25736 13.7574 3.25 15 3.25H18.5C19.7426 3.25 20.75 4.25736 20.75 5.5V8.99998C20.75 10.2426 19.7426 11.25 18.5 11.25H15C13.7574 11.25 12.75 10.2426 12.75 8.99998V5.5ZM15 4.75C14.5858 4.75 14.25 5.08579 14.25 5.5V8.99998C14.25 9.41419 14.5858 9.74998 15 9.74998H18.5C18.9142 9.74998 19.25 9.41419 19.25 8.99998V5.5C19.25 5.08579 18.9142 4.75 18.5 4.75H15ZM15 12.75C13.7574 12.75 12.75 13.7574 12.75 15V18.5C12.75 19.7426 13.7574 20.75 15 20.75H18.5C19.7426 20.75 20.75 19.7427 20.75 18.5V15C20.75 13.7574 19.7426 12.75 18.5 12.75H15ZM14.25 15C14.25 14.5858 14.5858 14.25 15 14.25H18.5C18.9142 14.25 19.25 14.5858 19.25 15V18.5C19.25 18.9142 18.9142 19.25 18.5 19.25H15C14.5858 19.25 14.25 18.9142 14.25 18.5V15Z" fill="currentColor"></path></svg>',
'settings' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.4858 3.5L13.5182 3.5C13.9233 3.5 14.2518 3.82851 14.2518 4.23377C14.2518 5.9529 16.1129 7.02795 17.602 6.1682C17.9528 5.96567 18.4014 6.08586 18.6039 6.43667L20.1203 9.0631C20.3229 9.41407 20.2027 9.86286 19.8517 10.0655C18.3625 10.9253 18.3625 13.0747 19.8517 13.9345C20.2026 14.1372 20.3229 14.5859 20.1203 14.9369L18.6039 17.5634C18.4013 17.9142 17.9528 18.0344 17.602 17.8318C16.1129 16.9721 14.2518 18.0471 14.2518 19.7663C14.2518 20.1715 13.9233 20.5 13.5182 20.5H10.4858C10.0804 20.5 9.75182 20.1714 9.75182 19.766C9.75182 18.0461 7.88983 16.9717 6.40067 17.8314C6.04945 18.0342 5.60037 17.9139 5.39767 17.5628L3.88167 14.937C3.67903 14.586 3.79928 14.1372 4.15026 13.9346C5.63949 13.0748 5.63946 10.9253 4.15025 10.0655C3.79926 9.86282 3.67901 9.41401 3.88165 9.06303L5.39764 6.43725C5.60034 6.08617 6.04943 5.96581 6.40065 6.16858C7.88982 7.02836 9.75182 5.9539 9.75182 4.23399C9.75182 3.82862 10.0804 3.5 10.4858 3.5ZM13.5182 2L10.4858 2C9.25201 2 8.25182 3.00019 8.25182 4.23399C8.25182 4.79884 7.64013 5.15215 7.15065 4.86955C6.08213 4.25263 4.71559 4.61859 4.0986 5.68725L2.58261 8.31303C1.96575 9.38146 2.33183 10.7477 3.40025 11.3645C3.88948 11.647 3.88947 12.3531 3.40026 12.6355C2.33184 13.2524 1.96578 14.6186 2.58263 15.687L4.09863 18.3128C4.71562 19.3814 6.08215 19.7474 7.15067 19.1305C7.64015 18.8479 8.25182 19.2012 8.25182 19.766C8.25182 20.9998 9.25201 22 10.4858 22H13.5182C14.7519 22 15.7518 20.9998 15.7518 19.7663C15.7518 19.2015 16.3632 18.8487 16.852 19.1309C17.9202 19.7476 19.2862 19.3816 19.9029 18.3134L21.4193 15.6869C22.0361 14.6185 21.6701 13.2523 20.6017 12.6355C20.1125 12.3531 20.1125 11.647 20.6017 11.3645C21.6701 10.7477 22.0362 9.38152 21.4193 8.3131L19.903 5.68667C19.2862 4.61842 17.9202 4.25241 16.852 4.86917C16.3632 5.15138 15.7518 4.79856 15.7518 4.23377C15.7518 3.00024 14.7519 2 13.5182 2ZM9.6659 11.9999C9.6659 10.7103 10.7113 9.66493 12.0009 9.66493C13.2905 9.66493 14.3359 10.7103 14.3359 11.9999C14.3359 13.2895 13.2905 14.3349 12.0009 14.3349C10.7113 14.3349 9.6659 13.2895 9.6659 11.9999ZM12.0009 8.16493C9.88289 8.16493 8.1659 9.88191 8.1659 11.9999C8.1659 14.1179 9.88289 15.8349 12.0009 15.8349C14.1189 15.8349 15.8359 14.1179 15.8359 11.9999C15.8359 9.88191 14.1189 8.16493 12.0009 8.16493Z" fill="currentColor"></path></svg>',
'ecommerce' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.31641 4H3.49696C4.24468 4 4.87822 4.55068 4.98234 5.29112L5.13429 6.37161M5.13429 6.37161L6.23641 14.2089C6.34053 14.9493 6.97407 15.5 7.72179 15.5L17.0833 15.5C17.6803 15.5 18.2205 15.146 18.4587 14.5986L21.126 8.47023C21.5572 7.4795 20.8312 6.37161 19.7507 6.37161H5.13429Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7.7832 19.5H7.7932M16.3203 19.5H16.3303" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>',
'calendar' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 2C8.41421 2 8.75 2.33579 8.75 2.75V3.75H15.25V2.75C15.25 2.33579 15.5858 2 16 2C16.4142 2 16.75 2.33579 16.75 2.75V3.75H18.5C19.7426 3.75 20.75 4.75736 20.75 6V9V19C20.75 20.2426 19.7426 21.25 18.5 21.25H5.5C4.25736 21.25 3.25 20.2426 3.25 19V9V6C3.25 4.75736 4.25736 3.75 5.5 3.75H7.25V2.75C7.25 2.33579 7.58579 2 8 2ZM8 5.25H5.5C5.08579 5.25 4.75 5.58579 4.75 6V8.25H19.25V6C19.25 5.58579 18.9142 5.25 18.5 5.25H16H8ZM19.25 9.75H4.75V19C4.75 19.4142 5.08579 19.75 5.5 19.75H18.5C18.9142 19.75 19.25 19.4142 19.25 19V9.75Z" fill="currentColor"></path></svg>',
'user-profile' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 3.5C7.30558 3.5 3.5 7.30558 3.5 12C3.5 14.1526 4.3002 16.1184 5.61936 17.616C6.17279 15.3096 8.24852 13.5955 10.7246 13.5955H13.2746C15.7509 13.5955 17.8268 15.31 18.38 17.6167C19.6996 16.119 20.5 14.153 20.5 12C20.5 7.30558 16.6944 3.5 12 3.5ZM17.0246 18.8566V18.8455C17.0246 16.7744 15.3457 15.0955 13.2746 15.0955H10.7246C8.65354 15.0955 6.97461 16.7744 6.97461 18.8455V18.856C8.38223 19.8895 10.1198 20.5 12 20.5C13.8798 20.5 15.6171 19.8898 17.0246 18.8566ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9991 7.25C10.8847 7.25 9.98126 8.15342 9.98126 9.26784C9.98126 10.3823 10.8847 11.2857 11.9991 11.2857C13.1135 11.2857 14.0169 10.3823 14.0169 9.26784C14.0169 8.15342 13.1135 7.25 11.9991 7.25ZM8.48126 9.26784C8.48126 7.32499 10.0563 5.75 11.9991 5.75C13.9419 5.75 15.5169 7.32499 15.5169 9.26784C15.5169 11.2107 13.9419 12.7857 11.9991 12.7857C10.0563 12.7857 8.48126 11.2107 8.48126 9.26784Z" fill="currentColor"></path></svg>',
'task' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.75586 5.50098C7.75586 5.08676 8.09165 4.75098 8.50586 4.75098H18.4985C18.9127 4.75098 19.2485 5.08676 19.2485 5.50098L19.2485 15.4956C19.2485 15.9098 18.9127 16.2456 18.4985 16.2456H8.50586C8.09165 16.2456 7.75586 15.9098 7.75586 15.4956V5.50098ZM8.50586 3.25098C7.26322 3.25098 6.25586 4.25834 6.25586 5.50098V6.26318H5.50195C4.25931 6.26318 3.25195 7.27054 3.25195 8.51318V18.4995C3.25195 19.7422 4.25931 20.7495 5.50195 20.7495H15.4883C16.7309 20.7495 17.7383 19.7421 17.7383 18.4995L17.7383 17.7456H18.4985C19.7411 17.7456 20.7485 16.7382 20.7485 15.4956L20.7485 5.50097C20.7485 4.25833 19.7411 3.25098 18.4985 3.25098H8.50586ZM16.2383 17.7456H8.50586C7.26322 17.7456 6.25586 16.7382 6.25586 15.4956V7.76318H5.50195C5.08774 7.76318 4.75195 8.09897 4.75195 8.51318V18.4995C4.75195 18.9137 5.08774 19.2495 5.50195 19.2495H15.4883C15.9025 19.2495 16.2383 18.9137 16.2383 18.4995L16.2383 17.7456Z" fill="currentColor"></path></svg>',
'forms' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 3.25C4.25736 3.25 3.25 4.25736 3.25 5.5V18.5C3.25 19.7426 4.25736 20.75 5.5 20.75H18.5001C19.7427 20.75 20.7501 19.7426 20.7501 18.5V5.5C20.7501 4.25736 19.7427 3.25 18.5001 3.25H5.5ZM4.75 5.5C4.75 5.08579 5.08579 4.75 5.5 4.75H18.5001C18.9143 4.75 19.2501 5.08579 19.2501 5.5V18.5C19.2501 18.9142 18.9143 19.25 18.5001 19.25H5.5C5.08579 19.25 4.75 18.9142 4.75 18.5V5.5ZM6.25005 9.7143C6.25005 9.30008 6.58583 8.9643 7.00005 8.9643L17 8.96429C17.4143 8.96429 17.75 9.30008 17.75 9.71429C17.75 10.1285 17.4143 10.4643 17 10.4643L7.00005 10.4643C6.58583 10.4643 6.25005 10.1285 6.25005 9.7143ZM6.25005 14.2857C6.25005 13.8715 6.58583 13.5357 7.00005 13.5357H17C17.4143 13.5357 17.75 13.8715 17.75 14.2857C17.75 14.6999 17.4143 15.0357 17 15.0357H7.00005C6.58583 15.0357 6.25005 14.6999 6.25005 14.2857Z" fill="currentColor"></path></svg>',
'tables' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 5.5C3.25 4.25736 4.25736 3.25 5.5 3.25H18.5C19.7426 3.25 20.75 4.25736 20.75 5.5V18.5C20.75 19.7426 19.7426 20.75 18.5 20.75H5.5C4.25736 20.75 3.25 19.7426 3.25 18.5V5.5ZM5.5 4.75C5.08579 4.75 4.75 5.08579 4.75 5.5V8.58325L19.25 8.58325V5.5C19.25 5.08579 18.9142 4.75 18.5 4.75H5.5ZM19.25 10.0833H15.416V13.9165H19.25V10.0833ZM13.916 10.0833L10.083 10.0833V13.9165L13.916 13.9165V10.0833ZM8.58301 10.0833H4.75V13.9165H8.58301V10.0833ZM4.75 18.5V15.4165H8.58301V19.25H5.5C5.08579 19.25 4.75 18.9142 4.75 18.5ZM10.083 19.25V15.4165L13.916 15.4165V19.25H10.083ZM15.416 19.25V15.4165H19.25V18.5C19.25 18.9142 18.9142 19.25 18.5 19.25H15.416Z" fill="currentColor"></path></svg>',
'pages' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.50391 4.25C8.50391 3.83579 8.83969 3.5 9.25391 3.5H15.2777C15.4766 3.5 15.6674 3.57902 15.8081 3.71967L18.2807 6.19234C18.4214 6.333 18.5004 6.52376 18.5004 6.72268V16.75C18.5004 17.1642 18.1646 17.5 17.7504 17.5H16.248V17.4993H14.748V17.5H9.25391C8.83969 17.5 8.50391 17.1642 8.50391 16.75V4.25ZM14.748 19H9.25391C8.01126 19 7.00391 17.9926 7.00391 16.75V6.49854H6.24805C5.83383 6.49854 5.49805 6.83432 5.49805 7.24854V19.75C5.49805 20.1642 5.83383 20.5 6.24805 20.5H13.998C14.4123 20.5 14.748 20.1642 14.748 19.75L14.748 19ZM7.00391 4.99854V4.25C7.00391 3.00736 8.01127 2 9.25391 2H15.2777C15.8745 2 16.4468 2.23705 16.8687 2.659L19.3414 5.13168C19.7634 5.55364 20.0004 6.12594 20.0004 6.72268V16.75C20.0004 17.9926 18.9931 19 17.7504 19H16.248L16.248 19.75C16.248 20.9926 15.2407 22 13.998 22H6.24805C5.00541 22 3.99805 20.9926 3.99805 19.75V7.24854C3.99805 6.00589 5.00541 4.99854 6.24805 4.99854H7.00391Z" fill="currentColor"></path></svg>',
'charts' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.00002 12.0957C4.00002 7.67742 7.58174 4.0957 12 4.0957C16.4183 4.0957 20 7.67742 20 12.0957C20 16.514 16.4183 20.0957 12 20.0957H5.06068L6.34317 18.8132C6.48382 18.6726 6.56284 18.4818 6.56284 18.2829C6.56284 18.084 6.48382 17.8932 6.34317 17.7526C4.89463 16.304 4.00002 14.305 4.00002 12.0957ZM12 2.5957C6.75332 2.5957 2.50002 6.849 2.50002 12.0957C2.50002 14.4488 3.35633 16.603 4.77303 18.262L2.71969 20.3154C2.50519 20.5299 2.44103 20.8525 2.55711 21.1327C2.6732 21.413 2.94668 21.5957 3.25002 21.5957H12C17.2467 21.5957 21.5 17.3424 21.5 12.0957C21.5 6.849 17.2467 2.5957 12 2.5957ZM7.62502 10.8467C6.93467 10.8467 6.37502 11.4063 6.37502 12.0967C6.37502 12.787 6.93467 13.3467 7.62502 13.3467H7.62512C8.31548 13.3467 8.87512 12.787 8.87512 12.0967C8.87512 11.4063 8.31548 10.8467 7.62512 10.8467H7.62502ZM10.75 12.0967C10.75 11.4063 11.3097 10.8467 12 10.8467H12.0001C12.6905 10.8467 13.2501 11.4063 13.2501 12.0967C13.2501 12.787 12.6905 13.3467 12.0001 13.3467H12C11.3097 13.3467 10.75 12.787 10.75 12.0967ZM16.375 10.8467C15.6847 10.8467 15.125 11.4063 15.125 12.0967C15.125 12.787 15.6847 13.3467 16.375 13.3467H16.3751C17.0655 13.3467 17.6251 12.787 17.6251 12.0967C17.6251 11.4063 17.0655 10.8467 16.3751 10.8467H16.375Z" fill="currentColor"></path></svg>',
'ui-elements' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.665 3.75618C11.8762 3.65061 12.1247 3.65061 12.3358 3.75618L18.7807 6.97853L12.3358 10.2009C12.1247 10.3064 11.8762 10.3064 11.665 10.2009L5.22014 6.97853L11.665 3.75618ZM4.29297 8.19199V16.0946C4.29297 16.3787 4.45347 16.6384 4.70757 16.7654L11.25 20.0365V11.6512C11.1631 11.6205 11.0777 11.5843 10.9942 11.5425L4.29297 8.19199ZM12.75 20.037L19.2933 16.7654C19.5474 16.6384 19.7079 16.3787 19.7079 16.0946V8.19199L13.0066 11.5425C12.9229 11.5844 12.8372 11.6207 12.75 11.6515V20.037ZM13.0066 2.41453C12.3732 2.09783 11.6277 2.09783 10.9942 2.41453L4.03676 5.89316C3.27449 6.27429 2.79297 7.05339 2.79297 7.90563V16.0946C2.79297 16.9468 3.27448 17.7259 4.03676 18.1071L10.9942 21.5857L11.3296 20.9149L10.9942 21.5857C11.6277 21.9024 12.3732 21.9024 13.0066 21.5857L19.9641 18.1071C20.7264 17.7259 21.2079 16.9468 21.2079 16.0946V7.90563C21.2079 7.05339 20.7264 6.27429 19.9641 5.89316L13.0066 2.41453Z" fill="currentColor"></path></svg>',
'authentication' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 2.75C14 2.33579 14.3358 2 14.75 2C15.1642 2 15.5 2.33579 15.5 2.75V5.73291L17.75 5.73291H19C19.4142 5.73291 19.75 6.0687 19.75 6.48291C19.75 6.89712 19.4142 7.23291 19 7.23291H18.5L18.5 12.2329C18.5 15.5691 15.9866 18.3183 12.75 18.6901V21.25C12.75 21.6642 12.4142 22 12 22C11.5858 22 11.25 21.6642 11.25 21.25V18.6901C8.01342 18.3183 5.5 15.5691 5.5 12.2329L5.5 7.23291H5C4.58579 7.23291 4.25 6.89712 4.25 6.48291C4.25 6.0687 4.58579 5.73291 5 5.73291L6.25 5.73291L8.5 5.73291L8.5 2.75C8.5 2.33579 8.83579 2 9.25 2C9.66421 2 10 2.33579 10 2.75L10 5.73291L14 5.73291V2.75ZM7 7.23291L7 12.2329C7 14.9943 9.23858 17.2329 12 17.2329C14.7614 17.2329 17 14.9943 17 12.2329L17 7.23291L7 7.23291Z" fill="currentColor"></path></svg>',
'chat' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.00002 12.0957C4.00002 7.67742 7.58174 4.0957 12 4.0957C16.4183 4.0957 20 7.67742 20 12.0957C20 16.514 16.4183 20.0957 12 20.0957H5.06068L6.34317 18.8132C6.48382 18.6726 6.56284 18.4818 6.56284 18.2829C6.56284 18.084 6.48382 17.8932 6.34317 17.7526C4.89463 16.304 4.00002 14.305 4.00002 12.0957ZM12 2.5957C6.75332 2.5957 2.50002 6.849 2.50002 12.0957C2.50002 14.4488 3.35633 16.603 4.77303 18.262L2.71969 20.3154C2.50519 20.5299 2.44103 20.8525 2.55711 21.1327C2.6732 21.413 2.94668 21.5957 3.25002 21.5957H12C17.2467 21.5957 21.5 17.3424 21.5 12.0957C21.5 6.849 17.2467 2.5957 12 2.5957ZM7.62502 10.8467C6.93467 10.8467 6.37502 11.4063 6.37502 12.0967C6.37502 12.787 6.93467 13.3467 7.62502 13.3467H7.62512C8.31548 13.3467 8.87512 12.787 8.87512 12.0967C8.87512 11.4063 8.31548 10.8467 7.62512 10.8467H7.62502ZM10.75 12.0967C10.75 11.4063 11.3097 10.8467 12 10.8467H12.0001C12.6905 10.8467 13.2501 11.4063 13.2501 12.0967C13.2501 12.787 12.6905 13.3467 12.0001 13.3467H12C11.3097 13.3467 10.75 12.787 10.75 12.0967ZM16.375 10.8467C15.6847 10.8467 15.125 11.4063 15.125 12.0967C15.125 12.787 15.6847 13.3467 16.375 13.3467H16.3751C17.0655 13.3467 17.6251 12.787 17.6251 12.0967C17.6251 11.4063 17.0655 10.8467 16.3751 10.8467H16.375Z" fill="currentColor"></path></svg>',
'support-ticket' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 17.0518V12C20 7.58174 16.4183 4 12 4C7.58168 4 3.99994 7.58174 3.99994 12V17.0518M19.9998 14.041V19.75C19.9998 20.5784 19.3282 21.25 18.4998 21.25H13.9998M6.5 18.75H5.5C4.67157 18.75 4 18.0784 4 17.25V13.75C4 12.9216 4.67157 12.25 5.5 12.25H6.5C7.32843 12.25 8 12.9216 8 13.75V17.25C8 18.0784 7.32843 18.75 6.5 18.75ZM17.4999 18.75H18.4999C19.3284 18.75 19.9999 18.0784 19.9999 17.25V13.75C19.9999 12.9216 19.3284 12.25 18.4999 12.25H17.4999C16.6715 12.25 15.9999 12.9216 15.9999 13.75V17.25C15.9999 18.0784 16.6715 18.75 17.4999 18.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>',
'users' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 11C18.2091 11 20 9.20914 20 7C20 4.79086 18.2091 3 16 3C13.7909 3 12 4.79086 12 7C12 9.20914 13.7909 11 16 11ZM16 13C13.3333 13 8 14.3333 8 17V20H24V17C24 14.3333 18.6667 13 16 13ZM8 11C10.2091 11 12 9.20914 12 7C12 4.79086 10.2091 3 8 3C5.79086 3 4 4.79086 4 7C4 9.20914 5.79086 11 8 11ZM8 13C5.67 13 1 14.17 1 16.5V19H6.35C6.13 18.37 6 17.7 6 17C6 15.61 6.81 14.28 8 13Z" fill="currentColor"/></svg>',
'email' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 8.187V17.25C3.5 17.6642 3.83579 18 4.25 18H19.75C20.1642 18 20.5 17.6642 20.5 17.25V8.18747L13.2873 13.2171C12.5141 13.7563 11.4866 13.7563 10.7134 13.2171L3.5 8.187ZM20.5 6.2286C20.5 6.23039 20.5 6.23218 20.5 6.23398V6.24336C20.4976 6.31753 20.4604 6.38643 20.3992 6.42905L12.4293 11.9867C12.1716 12.1664 11.8291 12.1664 11.5713 11.9867L3.60116 6.42885C3.538 6.38481 3.50035 6.31268 3.50032 6.23568C3.50028 6.10553 3.60577 6 3.73592 6H20.2644C20.3922 6 20.4963 6.10171 20.5 6.2286ZM22 6.25648V17.25C22 18.4926 20.9926 19.5 19.75 19.5H4.25C3.00736 19.5 2 18.4926 2 17.25V6.23398C2 6.22371 2.00021 6.2135 2.00061 6.20333C2.01781 5.25971 2.78812 4.5 3.73592 4.5H20.2644C21.2229 4.5 22 5.27697 22.0001 6.23549C22.0001 6.24249 22.0001 6.24949 22 6.25648Z" fill="currentColor"></path></svg>',
'api-key' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M10.5 10.5L21 21L19.5 22.5L16.5 19.5L15 21L13.5 19.5L10.5 16.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
];
return $icons[$iconName] ?? '<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/></svg>';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\CaCertificate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Str;
class RootCaController extends Controller
{
public function index()
{
$certificates = CaCertificate::all()->map(function($cert) {
$cert->status = ($cert->valid_to > now()) ? 'valid' : 'expired';
return $cert;
});
return view('pages.admin.root-ca.index', [
'certificates' => $certificates,
'title' => 'Root CA Management'
]);
}
public function renew(Request $request, CaCertificate $certificate)
{
$days = (int) $request->input('days', 3650);
try {
$newData = app(\App\Services\OpenSslService::class)->renewCaCertificate($certificate, $days);
$certificate->update([
'cert_content' => $newData['cert_content'],
'serial_number' => $newData['serial_number'],
'valid_from' => $newData['valid_from'],
'valid_to' => $newData['valid_to'],
]);
return redirect()->back()->with('success', 'Certificate renewed successfully (Re-signed).');
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Renewal failed: ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class CertificateApiController extends Controller
{
/**
* Display a listing of the user's certificates.
*/
public function index(Request $request)
{
$user = $request->get('authenticated_user');
$certificates = $user->certificates()
->latest()
->get([
'uuid',
'common_name',
'organization',
'san',
'valid_from',
'valid_to',
'cert_content',
'key_content'
])
->map(function ($cert) {
return [
'id' => $cert->uuid,
'common_name' => $cert->common_name,
'organization' => $cert->organization,
'san' => $cert->san,
'issued_at' => $cert->valid_from->toIso8601String(),
'expires_at' => $cert->valid_to->toIso8601String(),
'certificate' => $cert->cert_content,
'private_key' => $cert->key_content,
];
});
return response()->json([
'success' => true,
'data' => $certificates
]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\CaCertificate;
use Illuminate\Http\Request;
class PublicCaController extends Controller
{
/**
* Display a listing of public CA certificates.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$caTypes = ['root', 'intermediate_2048', 'intermediate_4096'];
$certificates = CaCertificate::whereIn('ca_type', $caTypes)
->get(['common_name', 'ca_type', 'serial_number', 'valid_to', 'cert_content'])
->map(function ($cert) {
return [
'name' => $cert->common_name,
'type' => $cert->ca_type,
'serial' => $cert->serial_number,
'expires_at' => $cert->valid_to->toIso8601String(),
'certificate' => $cert->cert_content,
];
});
return response()->json([
'success' => true,
'data' => $certificates
]);
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers;
use App\Models\ApiKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ApiKeyController extends Controller
{
public function index(Request $request)
{
$search = $request->query('search');
$perPage = $request->query('per_page', 10);
$query = Auth::user()->apiKeys()->latest();
if ($search) {
$query->where('name', 'like', '%' . $search . '%');
}
$apiKeys = $query->paginate($perPage)->withQueryString();
if ($request->ajax()) {
return view('api-keys.partials.table', compact('apiKeys'))->render();
}
return view('api-keys.index', compact('apiKeys'));
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
]);
$key = ApiKey::generate();
Auth::user()->apiKeys()->create([
'name' => $request->name,
'key' => $key,
]);
return back()->with('success', 'API Key generated successfully.')
->with('generated_key', $key);
}
public function destroy(ApiKey $apiKey)
{
if ($apiKey->user_id !== Auth::id()) {
abort(403);
}
$apiKey->delete();
return back()->with('success', 'API Key deleted successfully.');
}
public function toggle(ApiKey $apiKey)
{
if ($apiKey->user_id !== Auth::id()) {
abort(403);
}
$apiKey->update([
'is_active' => !$apiKey->is_active
]);
if (request()->wantsJson()) {
return response()->json(['success' => true, 'message' => 'API Key status updated successfully.']);
}
return back()->with('success', 'API Key status updated successfully.');
}
public function regenerate(ApiKey $apiKey)
{
if ($apiKey->user_id !== Auth::id()) {
abort(403);
}
$newKey = ApiKey::generate();
$apiKey->update([
'key' => $newKey,
'last_used_at' => null, // Reset usage
]);
if (request()->wantsJson()) {
return response()->json([
'success' => true,
'message' => 'API Key regenerated successfully.',
'new_key' => $newKey
]);
}
return back()->with('success', 'API Key regenerated successfully.')
->with('generated_key', $newKey);
}
public function update(Request $request, ApiKey $apiKey)
{
if ($apiKey->user_id !== Auth::id()) {
abort(403);
}
$request->validate([
'name' => 'required|string|max:255',
]);
$apiKey->update([
'name' => $request->name,
]);
if (request()->wantsJson()) {
return response()->json(['success' => true, 'message' => 'API Key renamed successfully.']);
}
return back()->with('success', 'API Key renamed successfully.');
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
class ForgotPasswordController extends Controller
{
/**
* Display the form to request a password reset link.
*/
public function showLinkRequestForm()
{
return view('pages.auth.forgot-password', ['title' => 'Forgot Password']);
}
/**
* Send a reset link to the given user.
*/
public function sendResetLinkEmail(Request $request)
{
$request->validate(['email' => 'required|email']);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status === Password::RESET_LINK_SENT) {
return back()->with(['status' => __($status)]);
}
throw ValidationException::withMessages([
'email' => [__($status)],
]);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class ResetPasswordController extends Controller
{
/**
* Display the password reset view for the given token.
*
* @param \Illuminate\Http\Request $request
* @param string $token
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showResetForm(Request $request, $token = null)
{
return view('pages.auth.reset-password')->with(
['token' => $token, 'email' => $request->email, 'title' => 'Reset Password']
);
}
/**
* Reset the given user's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
public function reset(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed|min:8',
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => Hash::make($password)
])->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status === Password::PASSWORD_RESET) {
return redirect()->route('signin')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [__($status)],
]);
}
}

View File

@@ -0,0 +1,412 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Http;
use App\Models\LoginHistory;
use App\Traits\RevokesSocialTokens;
use Illuminate\Auth\Events\Registered;
class AuthController extends Controller
{
use RevokesSocialTokens;
public function signin()
{
return view('pages.auth.signin', ['title' => 'Sign In']);
}
public function authenticate(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
// Find user by email
$user = User::where('email', $credentials['email'])->first();
// Check if user exists and is social-only (no password set)
if ($user && !$user->password) {
return back()->withErrors([
'email' => 'This account was created using social login. Please sign in with ' .
($user->google_id ? 'Google' : 'GitHub') . ' instead.',
])->onlyInput('email');
}
$remember = $request->boolean('remember');
if (Auth::attempt($credentials, $remember)) {
$user = Auth::user();
$this->recordLogin($user, 'credentials');
$request->session()->regenerate();
return redirect()->intended('dashboard');
}
return back()->withErrors([
'email' => 'The provided credentials do not match our records.',
])->onlyInput('email');
}
public function signup()
{
return view('pages.auth.signup', ['title' => 'Sign Up']);
}
public function store(Request $request)
{
$validated = $request->validate([
'fname' => 'required|string|max:255',
'lname' => 'nullable|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
'terms' => 'required|accepted',
]);
$roleInfo = \App\Models\Role::where('name', 'customer')->first();
$user = User::create([
'first_name' => $validated['fname'],
'last_name' => $validated['lname'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'role_id' => $roleInfo ? $roleInfo->id : null,
]);
Auth::login($user);
event(new Registered($user));
return redirect()->route('verification.notice');
}
/**
* Unified social redirect method
*/
public function socialRedirect($provider, $context)
{
// Store context in session for callback
session(['social_auth_context' => $context]);
return Socialite::driver($provider)->redirect();
}
/**
* Unified social callback method
*/
public function socialCallback($provider)
{
if (request()->has('error')) {
\Log::info('Social auth error: ' . request()->get('error'));
return redirect()->route('signin')->with('error', 'Authentication failed or was cancelled.');
}
try {
$socialUser = Socialite::driver($provider)->user();
\Log::info('Social user retrieved', ['provider' => $provider, 'email' => $socialUser->email]);
} catch (\Exception $e) {
\Log::error('Socialite error: ' . $e->getMessage());
return redirect()->route('signin')->with('error', 'Failed to authenticate with ' . ucfirst($provider) . '.');
}
$context = session('social_auth_context', 'signin');
\Log::info('Social auth context', ['context' => $context, 'is_authenticated' => Auth::check()]);
session()->forget('social_auth_context');
// Handle 'connect' context - user wants to link social account
if ($context === 'connect') {
if (!Auth::check()) {
\Log::warning('Connect attempt without authentication');
return redirect()->route('signin')->with('error', 'Please sign in first to connect your social account.');
}
\Log::info('Handling social connect from settings');
return $this->connectSocialAccount($provider, $socialUser);
}
// If user is already authenticated (shouldn't happen for signin/signup)
if (Auth::check()) {
\Log::info('User already authenticated, treating as connect');
return $this->connectSocialAccount($provider, $socialUser);
}
// Handle based on context
if ($context === 'signin') {
\Log::info('Handling social signin');
return $this->handleSocialSignin($provider, $socialUser);
} else {
\Log::info('Handling social signup');
return $this->handleSocialSignup($provider, $socialUser);
}
}
/**
* Connect social account to existing authenticated user
*/
protected function connectSocialAccount($provider, $socialUser)
{
$user = Auth::user();
$updateData = [
$provider . '_id' => $socialUser->id,
$provider . '_token' => $socialUser->token,
$provider . '_refresh_token' => $socialUser->refreshToken,
];
// Ensure update persists
$user->forceFill($updateData)->save();
$this->recordLogin($user, $provider);
\Log::info('Social account connected for user', ['user_id' => $user->id, 'provider' => $provider]);
return redirect()->route('settings')->with('success', ucfirst($provider) . ' account connected successfully.');
}
/**
* Handle social signin (only for existing users)
*/
protected function handleSocialSignin($provider, $socialUser)
{
// Try to find user by provider ID first
$user = User::where($provider . '_id', $socialUser->id)->first();
// If user not found by ID
if (!$user) {
// Check if user exists by email just to provide a helpful error message
if ($socialUser->email && User::where('email', $socialUser->email)->exists()) {
$this->revokeSocialToken($provider, $socialUser->token);
return redirect()->route('signin')->with('error', "An account with this email exists but is not connected to " . ucfirst($provider) . ". Please log in with your password and connect your social account from Settings.");
}
// Genuinely no account found
$this->revokeSocialToken($provider, $socialUser->token);
return redirect()->route('signin')->with('error', 'No account found with this ' . ucfirst($provider) . ' account. Please sign up first.');
}
// Proceed with login for connected user
// Update login tracking
$this->recordLogin($user, $provider);
Auth::login($user);
return redirect()->route('dashboard');
}
/**
* Handle social signup (only for new users)
*/
protected function handleSocialSignup($provider, $socialUser)
{
// Check if user already exists by email
$existingUser = User::where('email', $socialUser->email)->first();
if ($existingUser) {
return redirect()->route('signup')->withErrors([
'email' => 'An account with this email already exists. Please sign in instead.',
]);
}
// Download and save avatar
$avatarPath = null;
if ($socialUser->avatar) {
$avatarPath = $this->downloadSocialAvatar($socialUser->avatar, $socialUser->email);
}
// Parse name
$nameParts = explode(' ', $socialUser->name ?? $socialUser->email, 2);
$firstName = $nameParts[0];
$lastName = $nameParts[1] ?? '';
// Store user data in session for password setup
session([
'needs_password_setup' => true,
'social_signup_provider' => $provider,
'social_signup_name' => $socialUser->name ?? $socialUser->email,
'social_signup_email' => $socialUser->email,
'social_signup_first_name' => $firstName,
'social_signup_last_name' => $lastName,
'social_signup_avatar' => $avatarPath ? asset('storage/' . $avatarPath) : null,
'social_signup_avatar_path' => $avatarPath,
'social_signup_provider_id' => $socialUser->id,
'social_signup_provider_token' => $socialUser->token,
'social_signup_provider_refresh_token' => $socialUser->refreshToken,
]);
\Log::info('Social signup - redirecting to password setup', ['email' => $socialUser->email]);
// Redirect to password setup page
return redirect()->route('setup-password');
}
/**
* Download social avatar and save to storage
*/
protected function downloadSocialAvatar($avatarUrl, $userEmail)
{
try {
// Generate unique filename
$filename = 'avatars/' . Str::slug($userEmail) . '_' . time() . '.jpg';
// Download image content
$imageContent = file_get_contents($avatarUrl);
if ($imageContent === false) {
return null;
}
// Save to storage
Storage::disk('public')->put($filename, $imageContent);
return $filename;
} catch (\Exception $e) {
// If download fails, return null (user will have default avatar)
\Log::error('Failed to download social avatar: ' . $e->getMessage());
return null;
}
}
/**
* Show password setup page for social signup users
*/
public function showPasswordSetup()
{
// Check if user has the session flag
if (!session('needs_password_setup')) {
return redirect()->route('dashboard');
}
return view('pages.auth.setup-password', ['title' => 'Setup Password']);
}
/**
* Complete password setup for social signup users
*/
public function completePasswordSetup(Request $request)
{
// Validate session
if (!session('needs_password_setup')) {
return redirect()->route('dashboard');
}
// Validate input
$validated = $request->validate([
'password' => 'required|string|min:8|confirmed',
]);
// Get data from session
$provider = session('social_signup_provider');
$email = session('social_signup_email');
$firstName = session('social_signup_first_name');
$lastName = session('social_signup_last_name');
$avatarPath = session('social_signup_avatar_path');
$providerId = session('social_signup_provider_id');
$providerToken = session('social_signup_provider_token');
$providerRefreshToken = session('social_signup_provider_refresh_token');
// Create user
$roleInfo = \App\Models\Role::where('name', 'customer')->first();
$user = User::create([
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'avatar' => $avatarPath,
'password' => Hash::make($validated['password']),
'role_id' => $roleInfo ? $roleInfo->id : null,
$provider . '_id' => $providerId,
$provider . '_token' => $providerToken,
$provider . '_refresh_token' => $providerRefreshToken,
]);
$this->recordLogin($user, $provider);
// Clear session data
session()->forget([
'needs_password_setup',
'social_signup_provider',
'social_signup_name',
'social_signup_email',
'social_signup_first_name',
'social_signup_last_name',
'social_signup_avatar',
'social_signup_avatar_path',
'social_signup_provider_id',
'social_signup_provider_token',
'social_signup_provider_refresh_token',
]);
// Login user
Auth::login($user);
\Log::info('Social signup completed with password setup', ['user_id' => $user->id]);
return redirect()->route('dashboard')->with('success', 'Welcome! Your account has been created successfully.');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('signin');
}
/**
* Handle manual GET access to logout (e.g. typing in URL)
*/
public function logoutGet()
{
if (Auth::check()) {
return redirect()->route('dashboard')->with('error', 'For security reasons, you cannot logout by typing the URL. Please use the Logout button.');
}
return redirect()->route('signin');
}
/**
* Record login history and enforce limits
*/
protected function recordLogin(User $user, $provider)
{
$loginAt = now();
LoginHistory::create([
'user_id' => $user->id,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'provider' => $provider,
'login_at' => $loginAt,
]);
// Cleanup old records (older than 1 month)
LoginHistory::where('user_id', $user->id)
->where('login_at', '<', $loginAt->subMonth())
->delete();
// Cleanup excess records (keep only latest 10)
$idsToKeep = LoginHistory::where('user_id', $user->id)
->latest('login_at')
->take(10)
->pluck('id');
LoginHistory::where('user_id', $user->id)
->whereNotIn('id', $idsToKeep)
->delete();
}
}

View File

@@ -0,0 +1,360 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Certificate;
use App\Models\CaCertificate;
use App\Services\OpenSslService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Config;
use ZipArchive;
use Illuminate\Support\Str;
class CertificateController extends Controller
{
protected $sslService;
public function __construct(OpenSslService $sslService)
{
$this->sslService = $sslService;
}
public function index(Request $request)
{
$caReady = CaCertificate::where('ca_type', 'root')->exists() &&
CaCertificate::where('ca_type', 'intermediate_2048')->exists() &&
CaCertificate::where('ca_type', 'intermediate_4096')->exists();
$perPage = $request->input('per_page', 10);
$search = $request->input('search');
$query = Certificate::where('user_id', Auth::id());
if ($search) {
$query->where(function($q) use ($search) {
$q->where('common_name', 'like', "%{$search}%")
->orWhere('serial_number', 'like', "%{$search}%")
->orWhere('san', 'like', "%{$search}%");
});
}
$certificates = $query->latest()->paginate($perPage)->withQueryString();
if ($request->ajax()) {
return view('pages.certificate.partials.table', [
'certificates' => $certificates,
])->render();
}
return view('pages.certificate.index', [
'title' => 'Certificate Management',
'caReady' => $caReady,
'certificates' => $certificates,
'perPage' => $perPage,
'search' => $search,
'defaults' => Config::get('openssl.ca_leaf_default')
]);
}
public function downloadCa($type)
{
// map legacy or simple type to specific ca_type
$caType = match($type) {
'root' => 'root',
'intermediate', 'int_4096' => 'intermediate_4096',
'int_2048' => 'intermediate_2048',
default => $type
};
$ca = CaCertificate::where('ca_type', $caType)->firstOrFail();
$configKey = match($caType) {
'root' => 'openssl.ca_root.organizationName',
'intermediate_4096' => 'openssl.ca_4096.organizationName',
'intermediate_2048' => 'openssl.ca_2048.organizationName',
default => 'app.name'
};
$orgName = config($configKey);
$brand = Str::slug($orgName, '_');
$filename = "{$brand}_ca_{$type}.crt";
return response($ca->cert_content)
->header('Content-Type', 'application/x-x509-ca-cert')
->header('Content-Disposition', "attachment; filename={$filename}");
}
public function downloadCaBundle()
{
$root = CaCertificate::where('ca_type', 'root')->firstOrFail();
$int2048 = CaCertificate::where('ca_type', 'intermediate_2048')->firstOrFail();
$int4096 = CaCertificate::where('ca_type', 'intermediate_4096')->firstOrFail();
// Bundle includes all for convenience
$bundle = $int4096->cert_content . "\n" . $int2048->cert_content . "\n" . $root->cert_content;
$brand = Str::slug(config('openssl.ca_root.organizationName'), '_');
$filename = "{$brand}_ca-bundle.crt";
return response($bundle)
->header('Content-Type', 'application/x-x509-ca-cert')
->header('Content-Disposition', "attachment; filename={$filename}");
}
public function downloadCaAndroid()
{
$root = CaCertificate::where('ca_type', 'root')->firstOrFail();
// Convert PEM to DER
$certPem = $root->cert_content;
$begin = "-----BEGIN CERTIFICATE-----";
$end = "-----END CERTIFICATE-----";
$der = base64_decode($certPem);
$brand = Str::slug(config('openssl.ca_root.organizationName'), '_');
$filename = "{$brand}_root-ca.der";
return response($der)
->header('Content-Type', 'application/pkix-cert')
->header('Content-Disposition', "attachment; filename={$filename}");
}
public function downloadInstaller()
{
$appUrl = config('app.url');
$brand = Str::slug(config('openssl.ca_root.organizationName'), '_');
$script = <<<BATCH
@echo off
setlocal
:: One-Click Certificate Installer for {$brand}
:: Generated by {$appUrl}
set "SERVER_URL={$appUrl}"
set "TEMP_DIR=%TEMP%\\{$brand}_installer"
if not exist "%TEMP_DIR%" mkdir "%TEMP_DIR%"
cd /d "%TEMP_DIR%"
echo [1/5] Downloading Root CA...
curl -s -f -o "root.crt" "%SERVER_URL%/certificate/download-ca/root"
if %errorlevel% neq 0 (
echo Failed to download Root CA.
pause
exit /b %errorlevel%
)
echo [2/5] Downloading Intermediate CA 2048...
curl -s -f -o "int_2048.crt" "%SERVER_URL%/certificate/download-ca/int_2048"
if %errorlevel% neq 0 (
echo Failed to download Intermediate CA 2048.
pause
exit /b %errorlevel%
)
echo [3/5] Downloading Intermediate CA 4096...
curl -s -f -o "int_4096.crt" "%SERVER_URL%/certificate/download-ca/int_4096"
if %errorlevel% neq 0 (
echo Failed to download Intermediate CA 4096.
pause
exit /b %errorlevel%
)
echo [4/5] Installing Root CA to Trusted Root Certification Authorities...
certutil -user -addstore "Root" "root.crt"
echo [5/5] Installing Intermediate CAs to Intermediate Certification Authorities...
certutil -user -addstore "CA" "int_2048.crt"
certutil -user -addstore "CA" "int_4096.crt"
echo.
echo Cleanup...
cd ..
rmdir /s /q "%TEMP_DIR%"
echo.
echo ========================================================
echo Installation Complete!
echo You may need to restart your browser dynamically.
echo ========================================================
pause
BATCH;
return response($script)
->header('Content-Type', 'application/x-msdos-program')
->header('Content-Disposition', "attachment; filename={$brand}_install_certs.bat");
}
public function create()
{
return view('pages.certificate.create', [
'title' => 'Generate Certificate',
'defaults' => Config::get('openssl.ca_leaf_default')
]);
}
public function setupCa()
{
if (CaCertificate::count() > 0) {
return redirect()->route('certificate.index')->with('error', 'CA already initialized.');
}
if ($this->sslService->setupCa()) {
return redirect()->route('certificate.index')->with('success', 'Root and Intermediate CA successfully initialized.');
}
return redirect()->route('certificate.index')->with('error', 'Failed to initialize CA.');
}
public function generate(Request $request)
{
$validated = $request->validate([
'common_name' => 'required|string|max:255',
'config_mode' => 'required|in:default,manual',
'organization' => 'nullable|required_if:config_mode,manual|string|max:255',
'locality' => 'nullable|required_if:config_mode,manual|string|max:255',
'state' => 'nullable|required_if:config_mode,manual|string|max:255',
'country' => 'nullable|required_if:config_mode,manual|string|size:2',
'san' => 'nullable|string',
'key_bits' => 'required|in:2048,4096',
]);
try {
// Apply defaults if mode is 'default'
if ($validated['config_mode'] === 'default') {
$defaults = Config::get('openssl.ca_leaf_default');
$validated['organization'] = $defaults['organizationName'];
$validated['locality'] = $defaults['localityName'];
$validated['state'] = $defaults['stateOrProvinceName'];
$validated['country'] = $defaults['countryName'];
}
$result = $this->sslService->generateLeaf($validated);
Certificate::create([
'user_id' => Auth::id(),
'common_name' => $validated['common_name'],
'organization' => $validated['organization'],
'locality' => $validated['locality'],
'state' => $validated['state'],
'country' => $validated['country'],
'san' => $validated['san'],
'key_bits' => $validated['key_bits'],
'serial_number' => $this->sslService->formatSerialToHex($result['serial']),
'cert_content' => $result['cert'],
'key_content' => $result['key'],
'csr_content' => $result['csr'],
'valid_from' => $result['valid_from'] ?? null,
'valid_to' => $result['valid_to'] ?? null,
]);
return redirect()->route('certificate.index')->with('success', 'Certificate generated successfully.');
} catch (\Exception $e) {
return redirect()->back()->withInput()->with('error', $e->getMessage());
}
}
public function regenerate(Certificate $certificate)
{
$this->authorizeOwner($certificate);
try {
$data = [
'common_name' => $certificate->common_name,
'organization' => $certificate->organization,
'locality' => $certificate->locality,
'state' => $certificate->state,
'country' => $certificate->country,
'san' => $certificate->san,
'key_bits' => $certificate->key_bits,
];
$result = $this->sslService->generateLeaf($data);
// Update existing record with new content
$certificate->update([
'serial_number' => $this->sslService->formatSerialToHex($result['serial']),
'cert_content' => $result['cert'],
'key_content' => $result['key'],
'csr_content' => $result['csr'],
'valid_from' => $result['valid_from'] ?? null,
'valid_to' => $result['valid_to'] ?? null,
'created_at' => now(), // Refresh timestamp
]);
return redirect()->route('certificate.index')->with('success', 'Certificate regenerated successfully.');
} catch (\Exception $e) {
return redirect()->route('certificate.index')->with('error', 'Failed to regenerate: ' . $e->getMessage());
}
}
public function downloadZip(Certificate $certificate)
{
$this->authorizeOwner($certificate);
$tempFile = tempnam(sys_get_temp_dir(), 'cert_zip');
$zip = new ZipArchive;
if ($zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
return redirect()->back()->with('error', 'Failed to create ZIP archive.');
}
$filenameBase = preg_replace('/[^a-zA-Z0-9-_\.]/', '_', $certificate->common_name);
$zip->addFromString("{$filenameBase}.pem", $certificate->cert_content);
$zip->addFromString("{$filenameBase}.key", $certificate->key_content);
if ($certificate->csr_content) {
$zip->addFromString("{$filenameBase}.csr", $certificate->csr_content);
}
$zip->close();
return response()->download($tempFile, "cert_{$filenameBase}.zip")->deleteFileAfterSend(true);
}
public function downloadP12(Certificate $certificate)
{
$this->authorizeOwner($certificate);
$password = '112133'; // Default password from user request
if (openssl_pkcs12_export($certificate->cert_content, $p12, $certificate->key_content, $password)) {
$filenameBase = preg_replace('/[^a-zA-Z0-9-_\.]/', '_', $certificate->common_name);
return response($p12)
->header('Content-Type', 'application/x-pkcs12')
->header('Content-Disposition', "attachment; filename={$filenameBase}.p12");
}
return redirect()->back()->with('error', 'Failed to generate P12 file.');
}
public function viewFile(Certificate $certificate, $type)
{
$this->authorizeOwner($certificate);
$content = match($type) {
'cert' => $certificate->cert_content,
'key' => $certificate->key_content,
'csr' => $certificate->csr_content,
default => abort(404)
};
return response($content)->header('Content-Type', 'text/plain');
}
public function delete(Certificate $certificate)
{
$this->authorizeOwner($certificate);
$certificate->delete();
return redirect()->route('certificate.index')->with('success', 'Certificate deleted successfully.');
}
protected function authorizeOwner(Certificate $certificate)
{
if ($certificate->user_id !== Auth::id()) {
abort(403);
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ChartController extends Controller
{
public function lineChart()
{
return view('pages.chart.line-chart', ['title' => 'Line Chart']);
}
public function barChart()
{
return view('pages.chart.bar-chart', ['title' => 'Bar Chart']);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers;
use App\Models\ApiKey;
use App\Models\Certificate;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class DashboardController extends Controller
{
public function index()
{
$user = Auth::user();
// Basic Counts
$totalApiKeys = $user->apiKeys()->count();
$totalCertificates = $user->certificates()->count();
$activeCertificates = $user->certificates()
->where('valid_to', '>', now())
->count();
$expiringSoonCount = $user->certificates()
->where('valid_to', '>', now())
->where('valid_to', '<=', now()->addDays(14))
->count();
// Recent Activity
$recentCertificates = $user->certificates()
->latest()
->limit(5)
->get();
$recentApiActivity = $user->apiKeys()
->whereNotNull('last_used_at')
->orderBy('last_used_at', 'desc')
->limit(5)
->get();
// Chart Data: Certificates issued per month (last 6 months)
$months = [];
$issuanceData = [];
for ($i = 5; $i >= 0; $i--) {
$date = now()->subMonths($i);
$months[] = $date->format('M');
$issuanceData[] = $user->certificates()
->whereYear('created_at', $date->year)
->whereMonth('created_at', $date->month)
->count();
}
return view('pages.dashboard', compact(
'totalApiKeys',
'totalCertificates',
'activeCertificates',
'expiringSoonCount',
'recentCertificates',
'recentApiActivity',
'months',
'issuanceData'
));
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PageController extends Controller
{
public function calendar()
{
return view('pages.calender', ['title' => 'Calendar']);
}
public function blank()
{
return view('pages.blank', ['title' => 'Blank']);
}
public function error404()
{
return view('pages.errors.error-404', ['title' => 'Error 404']);
}
public function php()
{
phpinfo();
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class ProfileController extends Controller
{
/**
* Display the user's profile.
*/
public function index()
{
return view('pages.profile', [
'title' => 'User Profile',
'user' => Auth::user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(Request $request)
{
$user = Auth::user();
$validated = $request->validate([
'name' => 'sometimes|required|string|max:255',
'first_name' => 'nullable|string|max:255',
'last_name' => 'nullable|string|max:255',
'email' => 'sometimes|required|email|max:255|unique:users,email,' . $user->id,
'phone' => 'nullable|string|max:20',
'bio' => 'nullable|string',
'country' => 'nullable|string|max:255',
'city_state' => 'nullable|string|max:255',
'postal_code' => 'nullable|string|max:20',
'tax_id' => 'nullable|string|max:50',
'facebook' => 'nullable|url|max:255',
'x_link' => 'nullable|url|max:255',
'linkedin' => 'nullable|url|max:255',
'instagram' => 'nullable|url|max:255',
'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
if ($request->hasFile('avatar')) {
// Delete old avatar if exists and it's not a default one
if ($user->avatar && Storage::disk('public')->exists($user->avatar)) {
Storage::disk('public')->delete($user->avatar);
}
$path = $request->file('avatar')->store('avatars', 'public');
$validated['avatar'] = $path;
}
$user->update($validated);
return back()->with('success', 'Profile updated successfully.');
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rules\Password;
use App\Traits\RevokesSocialTokens;
class SettingsController extends Controller
{
use RevokesSocialTokens;
/**
* Display account settings.
*/
public function index()
{
return view('pages.settings', [
'title' => 'Account Settings',
'user' => Auth::user()->load(['loginHistories' => fn($q) => $q->latest('login_at')]),
]);
}
/**
* Update user password.
*/
public function updatePassword(Request $request)
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('success', 'Password updated successfully.');
}
/**
* Disconnect a social account.
*/
public function disconnectSocial(string $provider)
{
$user = Auth::user();
if (!in_array($provider, ['google', 'github'])) {
return back()->with('error', 'Invalid provider.');
}
// Revoke token on provider side if exists
$this->revokeSocialToken($provider, $user->{$provider . '_token'});
$user->update([
"{$provider}_id" => null,
"{$provider}_token" => null,
"{$provider}_refresh_token" => null,
]);
return back()->with('success', ucfirst($provider) . ' account disconnected successfully.');
}
/**
* Delete user account.
*/
public function destroy(Request $request)
{
$request->validate([
'confirmation' => ['required', 'string', 'in:Yes I will delete my account'],
]);
$user = $request->user();
if ($user->isAdmin()) {
return back()->with('error', 'Administrator accounts cannot be deleted.');
}
// Revoke connected social tokens before deletion
foreach (['google', 'github'] as $provider) {
if ($user->{$provider . '_id'}) {
$this->revokeSocialToken($provider, $user->{$provider . '_token'});
}
}
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('signin')->with('success', 'Your account has been deleted.');
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class SidebarController extends Controller
{
public function getMenuData()
{
$menuGroups = [
[
'title' => 'Menu',
'items' => [
[
'icon' => 'grid-icon',
'name' => 'Dashboard',
'path' => '/dashboard',
],
[
'icon' => 'bot-icon',
'name' => 'AI Assistant',
'new' => true,
'subItems' => [
['name' => 'Text Generator', 'path' => '/text-generator'],
['name' => 'Image Generator', 'path' => '/image-generator'],
['name' => 'Code Generator', 'path' => '/code-generator'],
['name' => 'Video Generator', 'path' => '/video-generator'],
],
],
[
'icon' => 'cart-icon',
'name' => 'E-commerce',
'new' => true,
'subItems' => [
['name' => 'Products', 'path' => '/products-list'],
['name' => 'Add Product', 'path' => '/add-product'],
['name' => 'Billing', 'path' => '/billing'],
['name' => 'Invoices', 'path' => '/invoices'],
['name' => 'Single Invoice', 'path' => '/single-invoice'],
['name' => 'Create Invoice', 'path' => '/create-invoice'],
['name' => 'Transactions', 'path' => '/transactions'],
['name' => 'Single Transaction', 'path' => '/single-transaction'],
],
],
[
'icon' => 'calendar-icon',
'name' => 'Calendar',
'path' => '/calendar',
],
[
'icon' => 'user-circle-icon',
'name' => 'User Profile',
'path' => '/profile',
],
[
'icon' => 'task-icon',
'name' => 'Task',
'subItems' => [
['name' => 'List', 'path' => '/task-list', 'pro' => false],
['name' => 'Kanban', 'path' => '/task-kanban', 'pro' => false],
],
],
[
'icon' => 'list-icon',
'name' => 'Forms',
'subItems' => [
['name' => 'Form Elements', 'path' => '/form-elements', 'pro' => false],
['name' => 'Form Layout', 'path' => '/form-layout', 'pro' => false],
],
],
[
'icon' => 'table-icon',
'name' => 'Tables',
'subItems' => [
['name' => 'Basic Tables', 'path' => '/basic-tables', 'pro' => false],
['name' => 'Data Tables', 'path' => '/data-tables', 'pro' => false],
],
],
[
'icon' => 'page-icon',
'name' => 'Pages',
'subItems' => [
['name' => 'File Manager', 'path' => '/file-manager', 'pro' => false],
['name' => 'Pricing Tables', 'path' => '/pricing-tables', 'pro' => false],
['name' => 'Faqs', 'path' => '/faq', 'pro' => false],
['name' => 'API Keys', 'path' => '/api-keys', 'new' => true],
['name' => 'Integrations', 'path' => '/integrations', 'new' => true],
['name' => 'Blank Page', 'path' => '/blank', 'pro' => false],
['name' => '404 Error', 'path' => '/error-404', 'pro' => false],
['name' => '500 Error', 'path' => '/error-500', 'pro' => false],
['name' => '503 Error', 'path' => '/error-503', 'pro' => false],
['name' => 'Coming Soon', 'path' => '/coming-soon', 'pro' => false],
['name' => 'Maintenance', 'path' => '/maintenance', 'pro' => false],
['name' => 'Success', 'path' => '/success', 'pro' => false],
],
],
],
],
[
'title' => 'Support',
'items' => [
[
'icon' => 'chat-icon',
'name' => 'Chat',
'path' => '/chat',
],
[
'icon' => 'call-icon',
'name' => 'Support Ticket',
'new' => true,
'subItems' => [
['name' => 'Ticket List', 'path' => '/support-tickets'],
['name' => 'Ticket Reply', 'path' => '/support-ticket-reply'],
],
],
[
'icon' => 'mail-icon',
'name' => 'Email',
'subItems' => [
['name' => 'Inbox', 'path' => '/inbox', 'pro' => false],
['name' => 'Details', 'path' => '/inbox-details', 'pro' => false],
],
],
],
],
[
'title' => 'Others',
'items' => [
[
'icon' => 'pie-chart-icon',
'name' => 'Charts',
'subItems' => [
['name' => 'Line Chart', 'path' => '/line-chart', 'pro' => false],
['name' => 'Bar Chart', 'path' => '/bar-chart', 'pro' => false],
['name' => 'Pie Chart', 'path' => '/pie-chart', 'pro' => false],
],
],
[
'icon' => 'box-cube-icon',
'name' => 'UI Elements',
'subItems' => [
['name' => 'Alerts', 'path' => '/alerts', 'pro' => false],
['name' => 'Avatar', 'path' => '/avatars', 'pro' => false],
['name' => 'Badge', 'path' => '/badge', 'pro' => false],
['name' => 'Breadcrumb', 'path' => '/breadcrumb', 'pro' => false],
['name' => 'Buttons', 'path' => '/buttons', 'pro' => false],
['name' => 'Buttons Group', 'path' => '/buttons-group', 'pro' => false],
['name' => 'Cards', 'path' => '/cards', 'pro' => false],
['name' => 'Carousel', 'path' => '/carousel', 'pro' => false],
['name' => 'Dropdowns', 'path' => '/dropdowns', 'pro' => false],
['name' => 'Images', 'path' => '/image', 'pro' => false],
['name' => 'Links', 'path' => '/links', 'pro' => false],
['name' => 'List', 'path' => '/list', 'pro' => false],
['name' => 'Modals', 'path' => '/modals', 'pro' => false],
['name' => 'Notification', 'path' => '/notifications', 'pro' => false],
['name' => 'Pagination', 'path' => '/pagination', 'pro' => false],
['name' => 'Popovers', 'path' => '/popovers', 'pro' => false],
['name' => 'Progressbar', 'path' => '/progress-bar', 'pro' => false],
['name' => 'Ribbons', 'path' => '/ribbons', 'pro' => false],
['name' => 'Spinners', 'path' => '/spinners', 'pro' => false],
['name' => 'Tabs', 'path' => '/tabs', 'pro' => false],
['name' => 'Tooltips', 'path' => '/tooltips', 'pro' => false],
['name' => 'Videos', 'path' => '/videos', 'pro' => false],
],
],
[
'icon' => 'plug-in-icon',
'name' => 'Authentication',
'subItems' => [
['name' => 'Sign In', 'path' => '/signin', 'pro' => false],
['name' => 'Sign Up', 'path' => '/signup', 'pro' => false],
['name' => 'Reset Password', 'path' => '/reset-password', 'pro' => false],
['name' => 'Two Step Verification', 'path' => '/two-step-verification', 'pro' => false],
],
],
],
],
];
return view('components.sidebar', compact('menuGroups'));
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class SuspendedController extends Controller
{
public function index()
{
// Prevent access if not strictly suspended
if (auth()->check() && !auth()->user()->isSuspended()) {
return redirect()->route('dashboard');
}
return view('pages.suspended', [
'title' => 'Account Suspended'
]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UiController extends Controller
{
public function alerts()
{
return view('pages.ui-elements.alerts', ['title' => 'Alerts']);
}
public function avatars()
{
return view('pages.ui-elements.avatars', ['title' => 'Avatars']);
}
public function badges()
{
return view('pages.ui-elements.badges', ['title' => 'Badges']);
}
public function buttons()
{
return view('pages.ui-elements.buttons', ['title' => 'Buttons']);
}
public function images()
{
return view('pages.ui-elements.images', ['title' => 'Images']);
}
public function videos()
{
return view('pages.ui-elements.videos', ['title' => 'Videos']);
}
public function formElements()
{
return view('pages.form.form-elements', ['title' => 'Form Elements']);
}
public function basicTables()
{
return view('pages.tables.basic-tables', ['title' => 'Basic Tables']);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use App\Traits\RevokesSocialTokens;
class UserManagementController extends Controller
{
use RevokesSocialTokens;
/**
* Display a listing of the users.
*/
public function index()
{
$users = User::latest()->paginate(10);
return view('pages.admin.users.index', [
'users' => $users
]);
}
public function destroy(User $user)
{
if ($user->id === auth()->id()) {
return back()->with('error', 'You cannot delete your own account.');
}
// Revoke Google Token
if ($user->google_token) {
$this->revokeSocialToken('google', $user->google_token);
}
// Revoke GitHub Token
if ($user->github_token) {
$this->revokeSocialToken('github', $user->github_token);
}
$user->delete();
return back()->with('success', 'User deleted successfully.');
}
public function sendResetLink(User $user)
{
$token = \Illuminate\Support\Facades\Password::createToken($user);
$user->sendPasswordResetNotification($token);
return back()->with('success', 'Password reset link sent to ' . $user->email);
}
public function toggleStatus(User $user)
{
if ($user->id === auth()->id()) {
return back()->with('error', 'You cannot change your own status.');
}
$newStatus = $user->status === 'suspended' ? 'active' : 'suspended';
$user->update(['status' => $newStatus]);
$message = $newStatus === 'suspended'
? 'User account has been suspended.'
: 'User account has been activated.';
return back()->with('success', $message);
}
public function sendVerification(User $user)
{
if ($user->hasVerifiedEmail()) {
return back()->with('message', 'User is already verified.');
}
$user->sendEmailVerificationNotification();
return back()->with('success', 'Verification link sent to ' . $user->email);
}
public function updateEmail(Request $request, User $user)
{
if ($user->id === auth()->id()) {
return back()->with('error', 'You cannot update your own email from here.');
}
$validated = $request->validate([
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id],
]);
$user->forceFill([
'email' => $validated['email'],
'email_verified_at' => null, // Reset verification status
])->save();
return back()->with('success', 'User email updated. Verification status has been reset.');
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\Request;
class VerificationController extends Controller
{
/**
* Show the email verification notice.
*/
public function show()
{
if (auth()->user()->hasVerifiedEmail()) {
return redirect()->route('dashboard');
}
return view('pages.auth.verify-email', ['title' => 'Verify Email']);
}
/**
* Mark the authenticated user's email address as verified.
*/
public function verify(Request $request, $id, $hash)
{
$user = \App\Models\User::findOrFail($id);
if (! hash_equals((string) $hash, sha1($user->getEmailForVerification()))) {
abort(403);
}
if ($user->hasVerifiedEmail()) {
return $this->redirectAfterVerification('Email already verified.');
}
if ($user->markEmailAsVerified()) {
event(new \Illuminate\Auth\Events\Verified($user));
}
return $this->redirectAfterVerification('Email verified successfully!');
}
private function redirectAfterVerification($message)
{
if (auth()->check()) {
return redirect()->route('dashboard')->with('success', $message);
}
return redirect()->route('signin')->with('success', $message . ' You can now login.');
}
/**
* Resend the email verification notification.
*/
public function resend(Request $request)
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->route('dashboard');
}
$request->user()->sendEmailVerificationNotification();
return back()->with('success', 'Verification link sent!');
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Middleware;
use Closure;
use App\Models\ApiKey;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckApiKey
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$keyString = $request->header('X-API-KEY');
if (!$keyString) {
return response()->json([
'success' => false,
'message' => 'API Key is missing. Please provide it in the X-API-KEY header.'
], 401);
}
$apiKey = ApiKey::where('key', $keyString)->first();
if (!$apiKey || !$apiKey->is_active) {
return response()->json([
'success' => false,
'message' => 'Invalid or inactive API Key.'
], 401);
}
// Update last used timestamp
$apiKey->update(['last_used_at' => now()]);
// Put the user in the request context
$request->merge(['authenticated_user' => $apiKey->user]);
// Alternatively, if we want to use Auth facade, we can manually log in the user for this request
// \Auth::login($apiKey->user);
return $next($request);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;
class EnsureUserIsActive
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
if (Auth::check() && Auth::user()->isSuspended()) {
// Allow access to suspended page and logout
if ($request->routeIs('suspended') || $request->routeIs('logout')) {
return $next($request);
}
// Redirect everything else to suspended page
return redirect()->route('suspended');
}
return $next($request);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsAdmin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user() || ! $request->user()->isAdmin()) {
return redirect()->route('dashboard')->with('error', 'Administrator privileges required.');
}
return $next($request);
}
}

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

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class ApiKey extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
'key',
'last_used_at',
'is_active',
];
protected $casts = [
'last_used_at' => 'datetime',
'is_active' => 'boolean',
];
public function user()
{
return $this->belongsTo(User::class);
}
public static function generate()
{
do {
$key = 'dvp_' . Str::random(60);
} while (static::where('key', $key)->exists());
return $key;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class CaCertificate extends Model
{
protected $fillable = [
'uuid',
'ca_type',
'cert_content',
'key_content',
'serial_number',
'common_name',
'organization',
'valid_from',
'valid_to'
];
protected $casts = [
'valid_from' => 'datetime',
'valid_to' => 'datetime',
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->uuid = (string) Str::uuid();
});
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Certificate extends Model
{
protected $fillable = [
'uuid', 'user_id', 'common_name', 'organization', 'locality',
'state', 'country', 'san', 'key_bits', 'serial_number',
'cert_content', 'key_content', 'csr_content',
'valid_from', 'valid_to'
];
protected $casts = [
'valid_from' => 'datetime',
'valid_to' => 'datetime',
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->uuid = (string) Str::uuid();
});
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class LoginHistory extends Model
{
protected $fillable = [
'user_id',
'ip_address',
'user_agent',
'provider',
'login_at',
];
protected $casts = [
'login_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
}

21
app/Models/Role.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
use HasFactory;
protected $fillable = [
'name',
'label',
];
public function users()
{
return $this->hasMany(User::class);
}
}

155
app/Models/User.php Normal file
View File

@@ -0,0 +1,155 @@
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable implements MustVerifyEmail
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/**
* The primary key type.
*
* @var string
*/
protected $keyType = 'string';
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = false;
/**
* Boot function from Laravel.
*/
protected static function booted()
{
static::creating(function ($user) {
if (empty($user->id)) {
do {
$id = \Illuminate\Support\Str::random(32);
} while (self::where('id', $id)->exists());
$user->id = $id;
}
});
}
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'role_id',
'email',
'password',
'avatar',
'first_name',
'last_name',
'phone',
'bio',
'status',
'country',
'city_state',
'postal_code',
'tax_id',
'facebook',
'x_link',
'linkedin',
'instagram',
'google_id',
'google_token',
'google_refresh_token',
'github_id',
'github_token',
'github_refresh_token',
'last_login_at',
'last_login_provider',
'last_login_ip',
'last_login_user_agent',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
/**
* Get the user's full name.
*
* @return string
*/
public function getNameAttribute(): string
{
return trim($this->first_name . ' ' . ($this->last_name ?? ''));
}
public function loginHistories()
{
return $this->hasMany(LoginHistory::class);
}
public function role()
{
return $this->belongsTo(Role::class);
}
/**
* Check if user is admin
*/
public function isAdmin(): bool
{
return $this->role && $this->role->name === 'admin';
}
/**
* Check if user is customer
*/
public function isCustomer(): bool
{
return $this->role && $this->role->name === 'customer';
}
/**
* Check if user is suspended
*/
public function isSuspended(): bool
{
return $this->status === 'suspended';
}
public function apiKeys()
{
return $this->hasMany(ApiKey::class);
}
public function certificates()
{
return $this->hasMany(Certificate::class);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View File

@@ -0,0 +1,429 @@
<?php
namespace App\Services;
use App\Models\CaCertificate;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
class OpenSslService
{
/**
* Generate Root and Intermediate CA certificates.
*/
public function setupCa()
{
if (CaCertificate::count() > 0) {
return false;
}
$rootConfig = Config::get('openssl.ca_root');
$int4096Config = Config::get('openssl.ca_4096');
$int2048Config = Config::get('openssl.ca_2048');
// Create a basic temporary openssl config for CA extensions
$configContent = "[req]\ndistinguished_name = req\n[v3_ca]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer\nbasicConstraints = critical, CA:true\nkeyUsage = critical, digitalSignature, cRLSign, keyCertSign";
$configFile = tempnam(sys_get_temp_dir(), 'ca_conf_');
file_put_contents($configFile, $configContent);
try {
// Root CA (4096-bit)
$rootKey = openssl_pkey_new([
'private_key_bits' => 4096,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'config' => $configFile
]);
if (!$rootKey) throw new \Exception('Failed to generate Root Key: ' . openssl_error_string());
$rootCsr = openssl_csr_new($rootConfig, $rootKey, ['digest_alg' => 'sha256', 'config' => $configFile]);
if (!$rootCsr) throw new \Exception('Failed to generate Root CSR: ' . openssl_error_string());
// Generate a random serial
$serial = $this->generateSerialNumber();
$rootCert = openssl_csr_sign($rootCsr, null, $rootKey, 10950, [
'digest_alg' => 'sha256',
'x509_extensions' => 'v3_ca',
'config' => $configFile,
], $serial);
if (!$rootCert) throw new \Exception('Failed to sign Root Cert: ' . openssl_error_string());
if (!openssl_x509_export($rootCert, $rootCertPem)) throw new \Exception('Failed to export Root Cert');
if (!openssl_pkey_export($rootKey, $rootKeyPem, null, ['config' => $configFile])) throw new \Exception('Failed to export Root Key');
$rootDetails = openssl_x509_parse($rootCertPem);
// Prefer serialNumberHex if available (PHP 8.0+)
$serialHex = isset($rootDetails['serialNumberHex'])
? $this->formatHex($rootDetails['serialNumberHex'])
: $this->formatSerialToHex($rootDetails['serialNumber']);
CaCertificate::create([
'ca_type' => 'root',
'cert_content' => $rootCertPem,
'key_content' => $rootKeyPem,
'serial_number' => $serialHex,
'common_name' => $rootDetails['subject']['CN'] ?? 'Root CA',
'organization' => $rootDetails['subject']['O'] ?? null,
'valid_from' => date('Y-m-d H:i:s', $rootDetails['validFrom_time_t']),
'valid_to' => date('Y-m-d H:i:s', $rootDetails['validTo_time_t']),
]);
// Intermediate CA 4096-bit
$int4096Key = openssl_pkey_new([
'private_key_bits' => 4096,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'config' => $configFile
]);
if (!$int4096Key) throw new \Exception('Failed to generate Int-4096 Key: ' . openssl_error_string());
$int4096Csr = openssl_csr_new($int4096Config, $int4096Key, ['digest_alg' => 'sha256', 'config' => $configFile]);
if (!$int4096Csr) throw new \Exception('Failed to generate Int-4096 CSR: ' . openssl_error_string());
$int4096Cert = openssl_csr_sign($int4096Csr, $rootCert, $rootKey, 10950, [
'digest_alg' => 'sha256',
'x509_extensions' => 'v3_ca',
'config' => $configFile,
], $this->generateSerialNumber());
if (!$int4096Cert) throw new \Exception('Failed to sign Int-4096 Cert: ' . openssl_error_string());
if (!openssl_x509_export($int4096Cert, $int4096CertPem)) throw new \Exception('Failed to export Int-4096 Cert');
if (!openssl_pkey_export($int4096Key, $int4096KeyPem, null, ['config' => $configFile])) throw new \Exception('Failed to export Int-4096 Key');
$int4096Details = openssl_x509_parse($int4096CertPem);
$serialHex4096 = isset($int4096Details['serialNumberHex'])
? $this->formatHex($int4096Details['serialNumberHex'])
: $this->formatSerialToHex($int4096Details['serialNumber']);
CaCertificate::create([
'ca_type' => 'intermediate_4096',
'cert_content' => $int4096CertPem,
'key_content' => $int4096KeyPem,
'serial_number' => $serialHex4096,
'common_name' => $int4096Details['subject']['CN'] ?? 'Intermediate CA 4096',
'organization' => $int4096Details['subject']['O'] ?? null,
'valid_from' => date('Y-m-d H:i:s', $int4096Details['validFrom_time_t']),
'valid_to' => date('Y-m-d H:i:s', $int4096Details['validTo_time_t']),
]);
// Intermediate CA 2048-bit
$int2048Key = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'config' => $configFile
]);
if (!$int2048Key) throw new \Exception('Failed to generate Int-2048 Key: ' . openssl_error_string());
$int2048Csr = openssl_csr_new($int2048Config, $int2048Key, ['digest_alg' => 'sha256', 'config' => $configFile]);
if (!$int2048Csr) throw new \Exception('Failed to generate Int-2048 CSR: ' . openssl_error_string());
$int2048Cert = openssl_csr_sign($int2048Csr, $rootCert, $rootKey, 10950, [
'digest_alg' => 'sha256',
'x509_extensions' => 'v3_ca',
'config' => $configFile,
], $this->generateSerialNumber());
if (!$int2048Cert) throw new \Exception('Failed to sign Int-2048 Cert: ' . openssl_error_string());
if (!openssl_x509_export($int2048Cert, $int2048CertPem)) throw new \Exception('Failed to export Int-2048 Cert');
if (!openssl_pkey_export($int2048Key, $int2048KeyPem, null, ['config' => $configFile])) throw new \Exception('Failed to export Int-2048 Key');
$int2048Details = openssl_x509_parse($int2048CertPem);
$serialHex2048 = isset($int2048Details['serialNumberHex'])
? $this->formatHex($int2048Details['serialNumberHex'])
: $this->formatSerialToHex($int2048Details['serialNumber']);
CaCertificate::create([
'ca_type' => 'intermediate_2048',
'cert_content' => $int2048CertPem,
'key_content' => $int2048KeyPem,
'serial_number' => $serialHex2048,
'common_name' => $int2048Details['subject']['CN'] ?? 'Intermediate CA 2048',
'organization' => $int2048Details['subject']['O'] ?? null,
'valid_from' => date('Y-m-d H:i:s', $int2048Details['validFrom_time_t']),
'valid_to' => date('Y-m-d H:i:s', $int2048Details['validTo_time_t']),
]);
return true;
} finally {
if (file_exists($configFile)) unlink($configFile);
}
}
/**
* Generate a domain certificate (Leaf).
*/
public function generateLeaf($data)
{
$keyBits = $data['key_bits'] ?? 2048;
$issuerType = (int)$keyBits === 4096 ? 'intermediate_4096' : 'intermediate_2048';
$intermediate = CaCertificate::where('ca_type', $issuerType)->first();
if (!$intermediate) {
throw new \Exception("Intermediate CA ({$issuerType}) not found. Please setup CA first.");
}
$dn = [
"countryName" => $data['country'],
"stateOrProvinceName" => $data['state'],
"localityName" => $data['locality'],
"organizationName" => $data['organization'],
"commonName" => $data['common_name']
];
$cn = $data['common_name'];
$userSan = $data['san'] ?? '';
// Parse user input: split by comma, trim, filter empty
$entries = array_filter(array_map('trim', explode(',', $userSan)));
// Always include CN as the first DNS entry
array_unshift($entries, $cn);
$sanArray = array_unique(array_map(function($entry) {
if (str_starts_with($entry, 'IP:') || str_starts_with($entry, 'DNS:')) {
return $entry;
}
return filter_var($entry, FILTER_VALIDATE_IP) ? "IP:$entry" : "DNS:$entry";
}, $entries));
$sanString = implode(', ', $sanArray);
$configFile = null;
try {
$configContent = "[req]\ndistinguished_name = req\nreq_extensions = v3_req\nprompt = no\n[req_distinguished_name]\nCN = $cn\n[v3_req]\nsubjectAltName = $sanString";
$configFile = tempnam(sys_get_temp_dir(), 'openssl_');
file_put_contents($configFile, $configContent);
$keyBits = $data['key_bits'] ?? 2048;
$privKey = openssl_pkey_new([
'private_key_bits' => (int)$keyBits,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'config' => $configFile
]);
if (!$privKey) throw new \Exception('Failed to generate Private Key: ' . openssl_error_string());
\Log::debug("Generating Leaf with SAN: " . $sanString);
$csr = openssl_csr_new($dn, $privKey, [
'digest_alg' => 'sha256',
'req_extensions' => 'v3_req',
'config' => $configFile
]);
if (!$csr) {
$err = openssl_error_string();
\Log::error("CSR Creation Failed: " . $err);
throw new \Exception('Failed to generate CSR: ' . $err);
}
$serial = $this->generateSerialNumber();
\Log::debug("Signing CSR with serial: " . $serial);
$cert = openssl_csr_sign($csr, $intermediate->cert_content, $intermediate->key_content, 365, [
'digest_alg' => 'sha256',
'x509_extensions' => 'v3_req',
'config' => $configFile,
], $serial);
if (!$cert) {
$err = openssl_error_string();
\Log::error("Certificate Signing Failed: " . $err);
throw new \Exception('Failed to sign Certificate: ' . $err);
}
// Verification: check if serial was actually applied
$certInfo = openssl_x509_parse($cert);
$actualSerialHex = isset($certInfo['serialNumberHex'])
? $this->formatHex($certInfo['serialNumberHex'])
: $this->formatSerialToHex($certInfo['serialNumber']);
\Log::debug("Certificate signed. Embedded Serial: " . $actualSerialHex);
if (!openssl_x509_export($cert, $certPem)) throw new \Exception('Failed to export Certificate');
if (!openssl_pkey_export($privKey, $keyPem, null, ['config' => $configFile])) throw new \Exception('Failed to export Private Key');
if (!openssl_csr_export($csr, $csrPem)) throw new \Exception('Failed to export CSR');
return [
'cert' => $certPem,
'key' => $keyPem,
'csr' => $csrPem,
'serial' => $actualSerialHex,
'valid_from' => isset($certInfo['validFrom_time_t']) ? date('Y-m-d H:i:s', $certInfo['validFrom_time_t']) : null,
'valid_to' => isset($certInfo['validTo_time_t']) ? date('Y-m-d H:i:s', $certInfo['validTo_time_t']) : null,
];
} finally {
if ($configFile && file_exists($configFile)) {
unlink($configFile);
}
}
}
/**
* Generate a unique serial number.
* Generates a large random integer for the serial number.
*/
/**
* Generate a unique serial number.
* Generates a 20-byte (160-bit) positive integer string for X.509 compliance.
*/
/**
* Generate a unique serial number.
* Note: strictly returns an int because openssl_csr_sign() in PHP requires type int.
* This limits us to 64-bit entropy (PHP_INT_MAX), enabling approx 9e18 combinations.
* While X.509 uses 160-bit, PHP's native extension wrapper limits this.
*/
protected function generateSerialNumber(): int
{
try {
return random_int(1, PHP_INT_MAX);
} catch (\Exception $e) {
return time();
}
}
/**
* Format a hex string (from serialNumberHex).
*/
public function formatHex($hex)
{
// Capitalize and add colons
$hex = strtoupper($hex);
return implode(':', str_split($hex, 2));
}
/**
* Renew (Re-sign) a CA Certificate.
*/
public function renewCaCertificate(CaCertificate $cert, int $days)
{
$configFile = null;
try {
// 1. Prepare Config
$configContent = "[req]\ndistinguished_name = req\n[v3_ca]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer\nbasicConstraints = critical, CA:true\nkeyUsage = critical, digitalSignature, cRLSign, keyCertSign";
$configFile = tempnam(sys_get_temp_dir(), 'renew_ca_');
file_put_contents($configFile, $configContent);
// 2. Get Private Key
$privKey = openssl_pkey_get_private($cert->key_content);
if (!$privKey) throw new \Exception('Failed to load Private Key');
// 3. Get Subject DN from existing Cert
$certInfo = openssl_x509_parse($cert->cert_content);
$dn = $certInfo['subject'];
// Reconstruct DN array for openssl_csr_new if needed,
// but we can just use the existing subject.
// CAUTION: openssl_csr_new expects proper array keys (e.g. commonName, organizationName)
// openssl_x509_parse returns abbreviated keys (CN, O, C, etc.) often.
// We need to map them back or pass the DN info carefully.
$dnMap = [
'CN' => 'commonName',
'O' => 'organizationName',
'OU' => 'organizationalUnitName',
'C' => 'countryName',
'ST' => 'stateOrProvinceName',
'L' => 'localityName',
'emailAddress' => 'emailAddress'
];
$newDn = [];
foreach ($dn as $key => $value) {
if (isset($dnMap[$key])) {
$newDn[$dnMap[$key]] = $value;
}
}
// 4. Generate New CSR
// Note: We use the SAME private key
$csr = openssl_csr_new($newDn, $privKey, ['digest_alg' => 'sha256', 'config' => $configFile]);
if (!$csr) throw new \Exception('Failed to generate Renewal CSR: ' . openssl_error_string());
// 5. Determine Signer (Issuer)
$issuerCert = null;
$issuerKey = null;
if ($cert->ca_type === 'root') {
// Root signs itself
$issuerCert = null;
$issuerKey = $privKey;
} else {
// Intermediate is signed by Root
$root = CaCertificate::where('ca_type', 'root')->first();
if (!$root) throw new \Exception('Root CA not found for signing intermediate renewal.');
$issuerCert = $root->cert_content;
$issuerKey = openssl_pkey_get_private($root->key_content);
}
// 6. Sign CSR
$serial = $this->generateSerialNumber();
$newCert = openssl_csr_sign($csr, $issuerCert, $issuerKey, $days, [
'digest_alg' => 'sha256',
'x509_extensions' => 'v3_ca',
'config' => $configFile,
], $serial);
if (!$newCert) throw new \Exception('Failed to sign Renewal Cert: ' . openssl_error_string());
// 7. Export
if (!openssl_x509_export($newCert, $newCertPem)) throw new \Exception('Failed to export Renewal Cert');
// 8. Parse new details
$newInfo = openssl_x509_parse($newCertPem);
$newSerialHex = isset($newInfo['serialNumberHex'])
? $this->formatHex($newInfo['serialNumberHex'])
: $this->formatSerialToHex($newInfo['serialNumber']);
return [
'cert_content' => $newCertPem,
'serial_number' => $newSerialHex,
'valid_from' => date('Y-m-d H:i:s', $newInfo['validFrom_time_t']),
'valid_to' => date('Y-m-d H:i:s', $newInfo['validTo_time_t']),
];
} finally {
if ($configFile && file_exists($configFile)) unlink($configFile);
}
}
/**
* Fallback format a decimal serial number to hex string.
*/
public function formatSerialToHex($decimal)
{
// Sanitize input: remove anything that isn't a digit or minus sign
// Note: serial numbers shouldn't be negative here normally, but just cleaning string
$cleaned = preg_replace('/[^0-9]/', '', (string)$decimal);
if ($cleaned === '') {
$cleaned = '0';
}
// Support large integers that might be strings (bcmath based conversion)
if (function_exists('bcdiv')) {
$hex = '';
$value = $cleaned;
// Basic check if it's a valid BCMath number
if (!preg_match('/^\d+$/', $value)) {
$value = '0';
}
// Loop while value > 0
while (bccomp($value, '0') > 0) {
$mod = bcmod($value, '16');
$hex = dechex((int)$mod) . $hex;
$value = bcdiv($value, '16', 0);
}
$hex = $hex ?: '0';
} else {
// Fallback for smaller integers if bcmath missing
$hex = dechex((int)$cleaned);
}
// Ensure even length
if (strlen($hex) % 2 !== 0) {
$hex = '0' . $hex;
}
return strtoupper(implode(':', str_split($hex, 2)));
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Http;
trait RevokesSocialTokens
{
/**
* Revoke social provider token
*/
protected function revokeSocialToken($provider, $token)
{
try {
if ($provider === 'google') {
Http::post('https://oauth2.googleapis.com/revoke', [
'token' => $token,
]);
} elseif ($provider === 'github') {
$clientId = config('services.github.client_id');
$clientSecret = config('services.github.client_secret');
Http::withBasicAuth($clientId, $clientSecret)
->delete("https://api.github.com/applications/{$clientId}/grant", [
'access_token' => $token,
]);
}
\Log::info("Revoked {$provider} token.");
} catch (\Exception $e) {
\Log::error("Failed to revoke {$provider} token: " . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class CalenderArea extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.calender-area');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\common;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class CommonGridShape extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.common.common-grid-shape');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\common;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class ComponentCard extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.common.component-card');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\View\Components\common;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class DropdownMenu extends Component
{
public function __construct()
{
//
}
public function render(): View|Closure|string
{
return view('components.common.dropdown-menu');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\common;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class PageBreadcrumb extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.common.page-breadcrumb');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\common;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Preloader extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.common.preloader');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\common;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class TableDropdown extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.common.table-dropdown');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\common;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class ThemeToggle extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.common.theme-toggle');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\ecommerce;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class CustomerDemographic extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ecommerce.customer-demographic');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\View\Components\ecommerce;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class EcommerceMetrics extends Component
{
public function __construct()
{
//
}
public function render(): View|Closure|string
{
return view('components.ecommerce.ecommerce-metrics');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\View\Components\ecommerce;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class MonthlySale extends Component
{
public function __construct()
{
//
}
public function render(): View|Closure|string
{
return view('components.ecommerce.monthly-sale');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\View\Components\ecommerce;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class MonthlyTarget extends Component
{
public function __construct()
{
//
}
public function render(): View|Closure|string
{
return view('components.ecommerce.monthly-target');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\ecommerce;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class RecentOrders extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ecommerce.recent-orders');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\ecommerce;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class StatisticsChart extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ecommerce.statistics-chart');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class DatePicker extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.date-picker');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class CheckboxComponent extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.checkbox-component');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class DefaultInputs extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.default-inputs');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Dropzone extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.dropzone');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class FileInputExample extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.file-input-example');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class InputGroup extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.input-group');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class InputStates extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.input-states');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class RadioButtons extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.radio-buttons');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class SelectInputs extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.select-inputs');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class TextAreaInputs extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.text-area-inputs');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\FormElements;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class ToggleSwitch extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.form-elements.toggle-switch');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\input;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Radio extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.input.radio');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\form\select;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class MultipleSelect extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.form.select.multiple-select');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\header;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class NotificationDropdown extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.header.notification-dropdown');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\header;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class UserDropdown extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.header.user-dropdown');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\profile;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class AddressCard extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.profile.address-card');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\profile;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class PersonalInfoCard extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.profile.personal-info-card');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\profile;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class ProfileCard extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.profile.profile-card');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\tables\BasicTables;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class BasicTablesFive extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.tables.basic-tables.basic-tables-five');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\tables\BasicTables;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class BasicTablesFour extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.tables.basic-tables.basic-tables-four');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\tables\BasicTables;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class BasicTablesOne extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.tables.basic-tables.basic-tables-one');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\tables\BasicTables;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class BasicTablesThree extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.tables.basic-tables.basic-tables-three');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\tables\BasicTables;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class BasicTablesTwo extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.tables.basic-tables.basic-tables-two');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\ui;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Alert extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ui.alert');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\ui;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Avatar extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ui.avatar');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\ui;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Badge extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ui.badge');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\ui;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Button extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ui.button');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\View\Components\ui;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Modal extends Component
{
// /**
// * Create a new component instance.
// */
// public function __construct()
// {
// //
// }
// /**
// * Get the view / contents that represent the component.
// */
// public function render(): View|Closure|string
// {
// return view('components.ui.modal');
// }
public $isOpen;
public $showCloseButton;
public $isFullscreen;
public $modalId;
/**
* Create a new component instance.
*/
public function __construct(
$isOpen = false,
$showCloseButton = true,
$isFullscreen = false,
$modalId = null
) {
$this->isOpen = $isOpen;
$this->showCloseButton = $showCloseButton;
$this->isFullscreen = $isFullscreen;
$this->modalId = $modalId ?? 'modal-' . uniqid();
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ui.modal');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\ui;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class YoutubeEmbed extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.ui.youtube-embed');
}
}

18
artisan Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

26
bootstrap/app.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->redirectTo(
guests: '/signin',
users: '/dashboard'
);
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
'api_key' => \App\Http\Middleware\CheckApiKey::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();

2
bootstrap/cache/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

5
bootstrap/providers.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
return [
App\Providers\AppServiceProvider::class,
];

80
composer.json Normal file
View File

@@ -0,0 +1,80 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": [
"laravel",
"framework"
],
"license": "MIT",
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/socialite": "^5.24",
"laravel/tinker": "^2.10.1"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.0",
"pestphp/pest-plugin-laravel": "^4.0"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

9731
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

126
config/app.php Normal file
View File

@@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];

115
config/auth.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

108
config/cache.php Normal file
View File

@@ -0,0 +1,108 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
];

174
config/database.php Normal file
View File

@@ -0,0 +1,174 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
];

80
config/filesystems.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

132
config/logging.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

119
config/mail.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
];

31
config/openssl.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
return [
'ca_root' => [
'countryName' => env('CA_ROOT_COUNTRY_NAME'),
'organizationName' => env('CA_ROOT_ORGANIZATION_NAME'),
'organizationalUnitName' => env('CA_ROOT_ORGANIZATIONAL_UNIT_NAME'),
'commonName' => env('CA_ROOT_COMMON_NAME'),
],
'ca_4096' => [
'countryName' => env('CA_4096_COUNTRY_NAME'),
'organizationName' => env('CA_4096_ORGANIZATION_NAME'),
'organizationalUnitName' => env('CA_4096_ORGANIZATIONAL_UNIT_NAME'),
'commonName' => env('CA_4096_COMMON_NAME'),
],
'ca_2048' => [
'countryName' => env('CA_2048_COUNTRY_NAME'),
'organizationName' => env('CA_2048_ORGANIZATION_NAME'),
'organizationalUnitName' => env('CA_2048_ORGANIZATIONAL_UNIT_NAME'),
'commonName' => env('CA_2048_COMMON_NAME'),
],
'ca_leaf_default' => [
'countryName' => env('CA_LEAF_DEFAULT_COUNTRY_NAME'),
'localityName' => env('CA_LEAF_DEFAULT_LOCALITY'),
'stateOrProvinceName' => env('CA_LEAF_DEFAULT_STATE'),
'organizationName' => env('CA_LEAF_DEFAULT_ORGANIZATION_NAME'),
'commonName' => env('CA_LEAF_DEFAULT_COMMON_NAME'),
],
];

112
config/queue.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];

50
config/services.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],
'resend' => [
'key' => env('RESEND_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('GITHUB_REDIRECT_URI'),
],
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT_URI'),
],
];

217
config/session.php Normal file
View File

@@ -0,0 +1,217 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain and all subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
];

1
database/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.sqlite*

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use App\Models\ApiKey;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class ApiKeyFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = ApiKey::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->words(3, true),
'key' => Str::random(64),
'last_used_at' => null,
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'first_name' => fake()->firstName(),
'last_name' => fake()->lastName(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

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