@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (app()->environment('testing')) {
|
||||
$target = DB::selectOne('SELECT current_database() AS database, inet_server_port() AS port');
|
||||
throw_unless(str_ends_with($target->database, '_test') && (int) $target->port === 55441, RuntimeException::class, 'Isolated QA required');
|
||||
} else {
|
||||
throw_unless(config('portal_profiles.authenticated_installation') === true, RuntimeException::class, 'Authenticated profile installation required');
|
||||
}
|
||||
Schema::table('users', fn (Blueprint $t) => $t->unsignedBigInteger('portal_access_revision')->default(0));
|
||||
Schema::table('opportunities', function (Blueprint $t) {
|
||||
$t->foreignId('owner_user_id')->nullable()->constrained('users')->restrictOnDelete();
|
||||
$t->foreignUuid('organization_id')->nullable()->constrained('organizations')->restrictOnDelete();
|
||||
$t->jsonb('workflow_context')->nullable();
|
||||
});
|
||||
Schema::create('portal_profile_requests', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->foreignUuid('organization_id')->nullable()->constrained('organizations')->restrictOnDelete();
|
||||
$t->string('representation_type');
|
||||
$t->jsonb('opportunity_types');
|
||||
$t->jsonb('document_ids');
|
||||
$t->text('reason');
|
||||
$t->string('status')->default('pending');
|
||||
$t->unsignedBigInteger('version')->default(1);
|
||||
$t->uuid('creation_key')->unique();
|
||||
$t->foreignId('decided_by')->nullable()->constrained('users')->restrictOnDelete();
|
||||
$t->text('decision_reason')->nullable();
|
||||
$t->timestampTz('decided_at')->nullable();
|
||||
$t->timestampsTz();
|
||||
});
|
||||
Schema::create('portal_role_assignments', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->string('role');
|
||||
$t->foreignUuid('organization_id')->nullable()->constrained('organizations')->restrictOnDelete();
|
||||
$t->jsonb('opportunity_types');
|
||||
$t->jsonb('responsibilities');
|
||||
$t->string('policy_version')->default('profiles-v1-20261010');
|
||||
$t->string('source');
|
||||
$t->foreignUuid('request_id')->nullable()->constrained('portal_profile_requests')->restrictOnDelete();
|
||||
$t->foreignId('approved_by')->constrained('users')->restrictOnDelete();
|
||||
$t->timestampTz('starts_at');
|
||||
$t->timestampTz('expires_at')->nullable();
|
||||
$t->timestampTz('revoked_at')->nullable();
|
||||
$t->timestampsTz();
|
||||
$t->index(['user_id', 'role']);
|
||||
});
|
||||
Schema::create('portal_superuser_pair', function (Blueprint $t) {
|
||||
$t->unsignedSmallInteger('id')->primary();
|
||||
$t->foreignId('first_user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->foreignId('second_user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->unsignedBigInteger('version')->default(1);
|
||||
$t->foreignId('approved_by')->constrained('users')->restrictOnDelete();
|
||||
$t->timestampsTz();
|
||||
});
|
||||
Schema::create('portal_security_approvals', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary();
|
||||
$t->string('kind');
|
||||
$t->foreignId('requested_by')->constrained('users')->restrictOnDelete();
|
||||
$t->foreignId('subject_user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->jsonb('scope');
|
||||
$t->text('reason');
|
||||
$t->foreignId('approved_by')->nullable()->constrained('users')->restrictOnDelete();
|
||||
$t->timestampTz('expires_at');
|
||||
$t->timestampTz('consumed_at')->nullable();
|
||||
$t->timestampsTz();
|
||||
});
|
||||
DB::statement("ALTER TABLE portal_role_assignments ADD CONSTRAINT portal_role_name CHECK (role IN ('promoter','validator','publisher','auditor')), ADD CONSTRAINT portal_role_independent CHECK (approved_by <> user_id), ADD CONSTRAINT portal_role_period CHECK (expires_at IS NULL OR expires_at > starts_at)");
|
||||
DB::statement('ALTER TABLE portal_superuser_pair ADD CONSTRAINT portal_pair_exact CHECK (id=1 AND first_user_id <> second_user_id AND version>0)');
|
||||
DB::statement("ALTER TABLE portal_profile_requests ADD CONSTRAINT portal_request_status CHECK (status IN ('pending','approved','rejected','withdrawn')), ADD CONSTRAINT portal_request_independent CHECK (decided_by IS NULL OR decided_by <> user_id)");
|
||||
DB::statement('ALTER TABLE portal_security_approvals ADD CONSTRAINT portal_security_independent CHECK (approved_by IS NULL OR approved_by <> requested_by)');
|
||||
DB::unprepared('CREATE OR REPLACE FUNCTION guard_portal_superuser_pair() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION \'Superuser pair cannot be removed; use atomic replacement\'; END; $$; CREATE TRIGGER portal_pair_no_delete BEFORE DELETE ON portal_superuser_pair FOR EACH ROW EXECUTE FUNCTION guard_portal_superuser_pair(); CREATE TRIGGER portal_pair_no_truncate BEFORE TRUNCATE ON portal_superuser_pair FOR EACH STATEMENT EXECUTE FUNCTION guard_portal_superuser_pair()');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach (['portal_role_assignments', 'portal_profile_requests', 'portal_superuser_pair', 'portal_security_approvals'] as $table) {
|
||||
throw_if(DB::table($table)->exists(), RuntimeException::class, 'Preserve authorization history; disable access and reconcile restore');
|
||||
}
|
||||
throw_if(DB::table('opportunities')->whereNotNull('owner_user_id')->orWhereNotNull('organization_id')->orWhereNotNull('workflow_context')->exists(), RuntimeException::class, 'Preserve scoped opportunities');
|
||||
throw_if(DB::table('users')->where('portal_access_revision', '<>', 0)->exists(), RuntimeException::class, 'Preserve session authorization history');
|
||||
DB::unprepared('DROP TRIGGER portal_pair_no_delete ON portal_superuser_pair; DROP TRIGGER portal_pair_no_truncate ON portal_superuser_pair; DROP FUNCTION guard_portal_superuser_pair()');
|
||||
Schema::dropIfExists('portal_security_approvals');
|
||||
Schema::dropIfExists('portal_superuser_pair');
|
||||
Schema::dropIfExists('portal_role_assignments');
|
||||
Schema::dropIfExists('portal_profile_requests');
|
||||
Schema::table('opportunities', function (Blueprint $t) {
|
||||
$t->dropConstrainedForeignId('owner_user_id');
|
||||
$t->dropConstrainedForeignId('organization_id');
|
||||
$t->dropColumn('workflow_context');
|
||||
});
|
||||
Schema::table('users', fn (Blueprint $t) => $t->dropColumn('portal_access_revision'));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user