diff --git a/app/Http/Controllers/OTSController.php b/app/Http/Controllers/OTSController.php new file mode 100644 index 0000000..e707e4c --- /dev/null +++ b/app/Http/Controllers/OTSController.php @@ -0,0 +1,33 @@ +validated(); + + OneTimeSecret::create(($validated + ['expires_at' => now()->addMinutes(30)])); + + return back(); + } + + public function create(): Response + { + return Inertia::render('ots/Create'); + } + + public function show(OneTimeSecret $oneTimeSecret): Response + { + $oneTimeSecret->delete(); + + return Inertia::render('ots/Show', ['oneTimeSecret' => $oneTimeSecret]); + } +} diff --git a/app/Http/Requests/StoreOTSRequest.php b/app/Http/Requests/StoreOTSRequest.php new file mode 100644 index 0000000..ce8ddb4 --- /dev/null +++ b/app/Http/Requests/StoreOTSRequest.php @@ -0,0 +1,21 @@ +|string> + */ + public function rules(): array + { + return [ + 'secret' => 'required|string', + ]; + } +} diff --git a/app/Models/OneTimeSecret.php b/app/Models/OneTimeSecret.php new file mode 100644 index 0000000..8ec94d2 --- /dev/null +++ b/app/Models/OneTimeSecret.php @@ -0,0 +1,64 @@ +|OneTimeSecret newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret query() + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret whereExpiresAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret whereRecipientId($value) + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret whereRecipientType($value) + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret whereSecret($value) + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret whereTeamId($value) + * @method static \Illuminate\Database\Eloquent\Builder|OneTimeSecret whereUpdatedAt($value) + * + * @mixin \Eloquent + */ +#[Fillable(['secret', 'expires_at'])] +class OneTimeSecret extends Model +{ + /** @use HasFactory */ + use HasFactory, HasUuids; + + public function team(): BelongsTo + { + return $this->belongsTo(Team::class); + } + + public function recipient(): MorphTo + { + return $this->morphTo(); + } + + protected function casts(): array + { + return [ + 'secret' => 'encrypted', + 'expires_at' => 'datetime', + ]; + } +} diff --git a/app/Models/Team.php b/app/Models/Team.php index c6b5c2d..4a44252 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphMany; /** * @property int $id @@ -23,6 +24,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * @property-read TeamMember|null $pivot * @property-read Collection $members * @property-read int|null $members_count + * @property-read Collection $receivedOneTimeSecrets + * @property-read int|null $received_one_time_secrets_count * * @method static Builder|Team newModelQuery() * @method static Builder|Team newQuery() @@ -53,6 +56,11 @@ class Team extends Model return $this->hasMany(TeamInvite::class); } + public function receivedOneTimeSecrets(): MorphMany + { + return $this->morphMany(OneTimeSecret::class, 'recipient'); + } + #[Scope] public function scopeVisibleTo(Builder $query, User $user): Builder { diff --git a/app/Models/User.php b/app/Models/User.php index 5b7aeee..d06c7cc 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\DatabaseNotification; use Illuminate\Notifications\DatabaseNotificationCollection; @@ -35,6 +36,8 @@ use Laravel\Passkeys\Passkey; * @property-read int|null $notifications_count * @property-read Collection $passkeys * @property-read int|null $passkeys_count + * @property-read Collection $receivedOneTimeSecrets + * @property-read int|null $received_one_time_secrets_count * @property-read TeamMember|null $pivot * @property-read Collection $teams * @property-read int|null $teams_count @@ -64,6 +67,19 @@ class User extends Authenticatable implements MustVerifyEmail, PasskeyUser /** @use HasFactory */ use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable; + public function teams(): BelongsToMany + { + return $this->belongsToMany(Team::class, 'team_members') + ->using(TeamMember::class) + ->withPivot(['role']) + ->withTimestamps(); + } + + public function receivedOneTimeSecrets(): MorphMany + { + return $this->morphMany(OneTimeSecret::class, 'recipient'); + } + /** * Get the attributes that should be cast. * @@ -77,12 +93,4 @@ class User extends Authenticatable implements MustVerifyEmail, PasskeyUser 'two_factor_confirmed_at' => 'datetime', ]; } - - public function teams(): BelongsToMany - { - return $this->belongsToMany(Team::class, 'team_members') - ->using(TeamMember::class) - ->withPivot(['role']) - ->withTimestamps(); - } } diff --git a/database/migrations/2026_10_08_094152_create_one_time_secrets_table.php b/database/migrations/2026_10_08_094152_create_one_time_secrets_table.php new file mode 100644 index 0000000..084f2c9 --- /dev/null +++ b/database/migrations/2026_10_08_094152_create_one_time_secrets_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + $table->nullableMorphs('recipient'); + $table->foreignIdFor(Team::class)->nullable()->constrained(); + $table->text('secret'); + $table->dateTime('expires_at'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('one_time_secrets'); + } +}; diff --git a/resources/js/app.ts b/resources/js/app.ts index 786e3c1..81ccc23 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,9 +1,9 @@ -import { createInertiaApp } from '@inertiajs/vue3'; -import { initializeTheme } from '@/composables/useAppearance'; +import {createInertiaApp} from '@inertiajs/vue3'; +import {initializeTheme} from '@/composables/useAppearance'; import AppLayout from '@/layouts/AppLayout.vue'; import AuthLayout from '@/layouts/AuthLayout.vue'; import SettingsLayout from '@/layouts/settings/Layout.vue'; -import { initializeFlashToast } from '@/lib/flashToast'; +import {initializeFlashToast} from '@/lib/flashToast'; const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; @@ -11,7 +11,7 @@ void createInertiaApp({ title: (title) => (title ? `${title} - ${appName}` : appName), layout: (name) => { switch (true) { - case name === 'Welcome': + case name.startsWith('ots/'): return null; case name.startsWith('auth/'): return AuthLayout; diff --git a/resources/js/components/ui/textarea/Textarea.vue b/resources/js/components/ui/textarea/Textarea.vue new file mode 100644 index 0000000..df9b174 --- /dev/null +++ b/resources/js/components/ui/textarea/Textarea.vue @@ -0,0 +1,24 @@ + + +