Add users, teams, authentication, profile/login/register/navbar views

This commit is contained in:
Andras Bacsai 2023-03-24 14:54:17 +01:00
parent 436d22e385
commit e47d493776
36 changed files with 1106 additions and 66 deletions

View File

@ -0,0 +1,40 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\CreatesNewUsers;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique(User::class),
],
'password' => $this->passwordRules(),
])->validate();
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Actions\Fortify;
use Laravel\Fortify\Rules\Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array|string>
*/
protected function passwordRules(): array
{
return ['required', 'string', new Password, 'confirmed'];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
use PasswordValidationRules;
/**
* Validate and reset the user's forgotten password.
*
* @param array<string, string> $input
*/
public function reset(User $user, array $input): void
{
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
use PasswordValidationRules;
/**
* Validate and update the user's password.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current_password:web'],
'password' => $this->passwordRules(),
], [
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
}
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Livewire;
use App\Models\Team;
use Livewire\Component;
class SwitchTeam extends Component
{
public function switch_to($team_id)
{
if (!auth()->user()->teams->contains($team_id)) {
return;
}
$team_to_switch_to = Team::find($team_id);
if (!$team_to_switch_to) {
return;
}
session(['currentTeam' => $team_to_switch_to]);
return redirect(request()->header('Referer'));
}
public function render()
{
return view('livewire.switch-team');
}
}

18
app/Models/BaseModel.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Xaevik\Cuid2\Cuid2;
abstract class BaseModel extends Model
{
protected static function boot()
{
parent::boot();
static::creating(function (Model $model) {
$model->uuid = (string) new Cuid2();
});
}
}

13
app/Models/Team.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
class Team extends BaseModel
{
protected $casts = [
'personal_team' => 'boolean',
];
protected $fillable = [
'name',
];
}

View File

@ -2,11 +2,12 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Xaevik\Cuid2\Cuid2;
class User extends Authenticatable
{
@ -41,4 +42,28 @@ class User extends Authenticatable
protected $casts = [
'email_verified_at' => 'datetime',
];
protected static function boot()
{
parent::boot();
static::creating(function (Model $model) {
$model->uuid = (string) new Cuid2();
});
}
public function teams()
{
return $this->belongsToMany(Team::class);
}
public function currentTeam()
{
return $this->belongsTo(Team::class);
}
public function otherTeams()
{
$team_id = session('currentTeam')->id;
return auth()->user()->teams->filter(function ($team) use ($team_id) {
return $team->id != $team_id;
});
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Models\User;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Fortify::loginView(fn () => view('auth.login'));
Fortify::registerView(fn () => view('auth.register'));
Fortify::authenticateUsing(function (Request $request) {
$user = User::where('email', $request->email)->with('teams')->first();
if (
$user &&
Hash::check($request->password, $user->password)
) {
session(['currentTeam' => $user->currentTeam = $user->teams->firstWhere('personal_team', true)]);
return $user;
}
});
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
RateLimiter::for('login', function (Request $request) {
$email = (string) $request->email;
return Limit::perMinute(5)->by($email.$request->ip());
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}

View File

@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string
*/
public const HOME = '/home';
public const HOME = '/';
/**
* Define your route model bindings, pattern filters, and other route configuration.

View File

@ -7,13 +7,15 @@
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.2",
"laravel/fortify": "^1.16",
"laravel/framework": "^10.0",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8",
"livewire/livewire": "^2.12",
"spatie/laravel-activitylog": "^4.7",
"spatie/laravel-data": "^3.2",
"spatie/laravel-ray": "^1.32"
"spatie/laravel-ray": "^1.32",
"xaevik/cuid2": "^1.7"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",

344
composer.lock generated
View File

@ -4,8 +4,62 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c6bfcd94dc256f9124e84a380eaaf556",
"content-hash": "0860841db6def79a62c268b0858d1f8a",
"packages": [
{
"name": "bacon/bacon-qr-code",
"version": "2.0.8",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phly/keep-a-changelog": "^2.1",
"phpunit/phpunit": "^7 | ^8 | ^9",
"spatie/phpunit-snapshot-assertions": "^4.2.9",
"squizlabs/php_codesniffer": "^3.4"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
},
"time": "2022-12-07T17:46:57+00:00"
},
{
"name": "brick/math",
"version": "0.10.2",
@ -62,6 +116,56 @@
],
"time": "2022-08-10T22:54:19+00:00"
},
{
"name": "dasprid/enum",
"version": "1.0.4",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f",
"reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 | ^8 | ^9",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.4"
},
"time": "2023-03-01T18:44:03+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
@ -1024,6 +1128,69 @@
],
"time": "2021-10-07T12:57:01+00:00"
},
{
"name": "laravel/fortify",
"version": "v1.16.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/fortify.git",
"reference": "e626fc70fcd940d01326c6c44512398cccc3113c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/fortify/zipball/e626fc70fcd940d01326c6c44512398cccc3113c",
"reference": "e626fc70fcd940d01326c6c44512398cccc3113c",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^2.0",
"ext-json": "*",
"illuminate/support": "^8.82|^9.0|^10.0",
"php": "^7.3|^8.0",
"pragmarx/google2fa": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^6.0|^7.0|^8.0",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Fortify\\FortifyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Fortify\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Backend controllers and scaffolding for Laravel authentication.",
"keywords": [
"auth",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify"
},
"time": "2023-01-06T15:57:08+00:00"
},
{
"name": "laravel/framework",
"version": "v10.3.3",
@ -2319,6 +2486,73 @@
],
"time": "2023-02-08T01:06:31+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v2.6.3",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "58c3f47f650c94ec05a151692652a868995d2938"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938",
"reference": "58c3f47f650c94ec05a151692652a868995d2938",
"shasum": ""
},
"require": {
"php": "^7|^8"
},
"require-dev": {
"phpunit/phpunit": "^6|^7|^8|^9",
"vimeo/psalm": "^1|^2|^3|^4"
},
"type": "library",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"time": "2022-06-14T06:56:20+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
@ -2603,6 +2837,58 @@
},
"time": "2021-10-28T11:13:42+00:00"
},
{
"name": "pragmarx/google2fa",
"version": "v8.0.1",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3",
"reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1.0|^2.0",
"php": "^7.1|^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.18",
"phpunit/phpunit": "^7.5.15|^8.5|^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"PragmaRX\\Google2FA\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "A One Time Password Authentication package, compatible with Google Authenticator.",
"keywords": [
"2fa",
"Authentication",
"Two Factor Authentication",
"google2fa"
],
"support": {
"issues": "https://github.com/antonioribeiro/google2fa/issues",
"source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1"
},
"time": "2022-06-13T21:57:56+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
@ -6384,6 +6670,62 @@
},
"time": "2022-06-03T18:03:27+00:00"
},
{
"name": "xaevik/cuid2",
"version": "1.7.0",
"source": {
"type": "git",
"url": "git@gitlab.com:xaevik/php-cuid2.git",
"reference": "e435e5005b2e9e9e9d30b64025749c0b7aa5dcf7"
},
"dist": {
"type": "zip",
"url": "https://gitlab.com/api/v4/projects/xaevik%2Fphp-cuid2/repository/archive.zip?sha=e435e5005b2e9e9e9d30b64025749c0b7aa5dcf7",
"reference": "e435e5005b2e9e9e9d30b64025749c0b7aa5dcf7",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.29",
"ext-ctype": "*",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "^3.7",
"vimeo/psalm": "^5.4"
},
"suggest": {
"ext-gmp": "*"
},
"type": "library",
"autoload": {
"files": [
"src/compat.php"
],
"psr-4": {
"Xaevik\\Cuid2\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Alan Brault",
"email": "alan.brault@visus.io"
}
],
"description": "A PHP library for generating collision-resistant ids (CUIDs).",
"keywords": [
"cuid",
"identifier"
],
"abandoned": "visus/cuid2",
"time": "2023-02-21T14:01:02+00:00"
},
{
"name": "zbateson/mail-mime-parser",
"version": "2.4.0",

View File

@ -190,6 +190,7 @@
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,

147
config/fortify.php Normal file
View File

@ -0,0 +1,147 @@
<?php
use App\Providers\RouteServiceProvider;
use Laravel\Fortify\Features;
return [
/*
|--------------------------------------------------------------------------
| Fortify Guard
|--------------------------------------------------------------------------
|
| Here you may specify which authentication guard Fortify will use while
| authenticating users. This value should correspond with one of your
| guards that is already present in your "auth" configuration file.
|
*/
'guard' => 'web',
/*
|--------------------------------------------------------------------------
| Fortify Password Broker
|--------------------------------------------------------------------------
|
| Here you may specify which password broker Fortify can use when a user
| is resetting their password. This configured value should match one
| of your password brokers setup in your "auth" configuration file.
|
*/
'passwords' => 'users',
/*
|--------------------------------------------------------------------------
| Username / Email
|--------------------------------------------------------------------------
|
| This value defines which model attribute should be considered as your
| application's "username" field. Typically, this might be the email
| address of the users but you are free to change this value here.
|
| Out of the box, Fortify expects forgot password and reset password
| requests to have a field named 'email'. If the application uses
| another name for the field you may define it below as needed.
|
*/
'username' => 'email',
'email' => 'email',
/*
|--------------------------------------------------------------------------
| Home Path
|--------------------------------------------------------------------------
|
| Here you may configure the path where users will get redirected during
| authentication or password reset when the operations are successful
| and the user is authenticated. You are free to change this value.
|
*/
'home' => RouteServiceProvider::HOME,
/*
|--------------------------------------------------------------------------
| Fortify Routes Prefix / Subdomain
|--------------------------------------------------------------------------
|
| Here you may specify which prefix Fortify will assign to all the routes
| that it registers with the application. If necessary, you may change
| subdomain under which all of the Fortify routes will be available.
|
*/
'prefix' => '',
'domain' => null,
/*
|--------------------------------------------------------------------------
| Fortify Routes Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Fortify will assign to the routes
| that it registers with the application. If necessary, you may change
| these middleware but typically this provided default is preferred.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/
'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
],
/*
|--------------------------------------------------------------------------
| Register View Routes
|--------------------------------------------------------------------------
|
| Here you may specify if the routes returning views should be disabled as
| you may not need them when building your own application. This may be
| especially true if you're writing a custom single-page application.
|
*/
'views' => true,
/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Some of the Fortify features are optional. You may disable the features
| by removing them from this array. You're free to only remove some of
| these features or you can even remove all of these if you need to.
|
*/
'features' => [
Features::registration(),
Features::resetPasswords(),
// Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
// 'window' => 0,
]),
],
];

View File

@ -13,6 +13,8 @@ public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->boolean('is_root_user')->default(false);
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();

View File

@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Fortify\Fortify;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret')
->after('password')
->nullable();
$table->text('two_factor_recovery_codes')
->after('two_factor_secret')
->nullable();
if (Fortify::confirmsTwoFactorAuthentication()) {
$table->timestamp('two_factor_confirmed_at')
->after('two_factor_recovery_codes')
->nullable();
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(array_merge([
'two_factor_secret',
'two_factor_recovery_codes',
], Fortify::confirmsTwoFactorAuthentication() ? [
'two_factor_confirmed_at',
] : []));
});
}
};

View File

@ -16,6 +16,7 @@ public function up(): void
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->string('team_id');
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->boolean('personal_team')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('teams');
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('team_user', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id');
$table->foreignId('user_id');
$table->string('role')->nullable();
$table->timestamps();
$table->unique(['team_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('team_user');
}
};

View File

@ -2,21 +2,17 @@
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// \App\Models\User::factory(10)->create();
// \App\Models\User::factory()->create([
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
if (env('APP_ENV') === 'local') {
$this->call([
UserSeeder::class,
TeamSeeder::class,
]);
}
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Database\Seeders;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class TeamSeeder extends Seeder
{
public function run(): void
{
$root_user = User::find(1);
$normal_user = User::find(2);
$root_user_personal_team = Team::create([
'name' => "Root Team",
'personal_team' => true,
]);
$root_user_other_team = Team::create([
'name' => "Root User's Other Team",
'personal_team' => false,
]);
$normal_user_personal_team = Team::create([
'name' => 'Normal Team',
'personal_team' => true,
]);
DB::table('team_user')->insert([
'team_id' => $root_user_personal_team->id,
'user_id' => $root_user->id,
'role' => 'admin',
]);
DB::table('team_user')->insert([
'team_id' => $root_user_other_team->id,
'user_id' => $root_user->id,
'role' => 'admin',
]);
DB::table('team_user')->insert([
'team_id' => $normal_user_personal_team->id,
'user_id' => $normal_user->id,
'role' => 'admin',
]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Database\Seeders;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class UserSeeder extends Seeder
{
public function run(): void
{
User::factory()->create([
'id' => 1,
'name' => 'Root User',
'email' => 'test@example.com',
'is_root_user' => true,
]);
User::factory()->create([
'id' => 2,
'name' => 'Normal User',
'email' => 'test2@example.com',
]);
}
}

View File

@ -0,0 +1,20 @@
<x-layout>
<div>
<form action="/login" method="POST">
@csrf
<input type="text" name="email" placeholder="email" @env('local') value="test@example.com" @endenv
autofocus />
<input type="password" name="password" placeho lder="Password" @env('local') value="password" @endenv />
<button type="submit">Login</button>
</form>
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
</div>
</x-layout>

View File

@ -0,0 +1,20 @@
<x-layout>
<form action="/register" method="POST">
@csrf
<input type="text" name="name" placeholder="name" @env('local') value="Andras Bacsai" @endenv />
<input type="text" name="email" placeholder="email" @env('local') value="andras@bacsai.com" @endenv />
<input type="password" name="password" placeholder="Password" @env('local') value="password" @endenv />
<input type="password" name="password_confirmation" placeholder="Password"
@env('local') value="password" @endenv />
<button type="submit">Register</button>
</form>
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
</x-layout>

View File

@ -1,3 +0,0 @@
<button wire:loading.remove {{ $attributes }} class="btn btn-primary rounded-none btn-xs no-animation"> {{ $slot }} </button>
<button wire:loading class="btn btn-disabled rounded-none btn-xs no-animation"> {{ $slot }}</button>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="light" class="h-full">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
@ -10,11 +10,9 @@
@livewireStyles
</head>
<body class="h-full">
@auth
<body>
<x-navbar />
@endauth
<main class="h-full pt-10 p-4">
<main>
{{ $slot }}
</main>

View File

@ -1,15 +1,15 @@
<nav class="flex space-x-4 p-2 fixed right-0">
@env('local')
<a href="/run-command">Run command</a>
{{-- <livewire:create-token /> --}}
<a href="/debug">Debug</a>
<a href="/debug/logs" target="_blank">Debug Logs</a>
{{-- <a href="/debug/inertia">Debug(inertia)</a> --}}
@endenv
<a href="/">Dashboard</a>
<nav>
<a href="/">Home</a>
@guest
<a href="/login">Login</a>
<a href="/register">Register</a>
@endguest
@auth
<a href="/demo">Demo</a>
<a href="/profile">Profile</a>
<form action="/logout" method="POST">
@csrf
<button type="submit">Logout</button>
</form>
@endauth
</nav>

View File

@ -1,5 +1,3 @@
<x-layout>
<livewire:run-command />
</x-layout>

View File

@ -0,0 +1,10 @@
<x-layout>
<h1>
Coolify v4 🎉
</h1>
@guest
<p>
To see the demo, please <a href='/login'>login<a> or <a href='/register'>register<a>.
</p>
@endguest
</x-layout>

View File

@ -1,7 +1,8 @@
<div>
<div>
<label for="command">
<input class="py-2 rounded ring-1" id="command" wire:model="command" type="text" />
<input autofocus class="py-2 rounded ring-1" id="command" wire:model="command" type="text"
wire:keydown.enter="runCommand" />
</label>
<button wire:click="runCommand">Run command</button>
@ -20,8 +21,7 @@
<div>
Activity: <span>{{ $activity?->id ?? 'waiting' }}</span>
</div>
<pre style="width: 100%;overflow-y: scroll;"
@if ($isKeepAliveOn || $manualKeepAlive) wire:poll.750ms="polling" @endif>{{ data_get($activity, 'description') }}</pre>
<pre style="width: 100%;overflow-y: scroll;" @if ($isKeepAliveOn || $manualKeepAlive) wire:poll.750ms="polling" @endif>{{ data_get($activity, 'description') }}</pre>
<div>
<div>Details:</div>
<pre style="width: 100%;overflow-y: scroll;">{{ json_encode(data_get($activity, 'properties'), JSON_PRETTY_PRINT) }}</pre>

View File

@ -0,0 +1,6 @@
<div>
@foreach (auth()->user()->otherTeams() as $team)
<button wire:key="{{ $team->id }}" wire:click="switch_to('{{ $team->id }}')">Switch to:
{{ $team->name }}</button>
@endforeach
</div>

View File

@ -0,0 +1,17 @@
<x-layout>
<div>
<div>
<h3>User</h3>
<p>Name: {{ auth()->user()->name }}</p>
<p>Id: {{ auth()->user()->id }}</p>
<p>Uuid: {{ auth()->user()->uuid }}</p>
</div>
<div>
<h3>Current Team</h3>
<p>Name: {{ session('currentTeam')->name }}</p>
<p>Id: {{ session('currentTeam')->id }}</p>
<p>Uuid: {{ session('currentTeam')->uuid }}</p>
</div>
<livewire:switch-team>
</div>
</x-layout>

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? 'Coolify' }}</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
@vite(['resources/js/app.js', 'resources/css/app.css'])
</head>
<body class="antialiased">
<h1 class="text-3xl font-bold">
Coolify v4
</h1>
<p class="mt-4">
<a href="/demo"> See demo </a>
</p>
</body>
</html>

View File

@ -14,7 +14,14 @@
*/
Route::get('/', function () {
return view('welcome');
return view('home');
});
Route::view('/demo', 'demo');
Route::middleware(['auth'])->group(function () {
Route::get('/profile', function () {
return view('profile');
});
Route::get('/demo', function () {
return view('demo');
});
});

View File