diff --git a/README.md b/README.md index c6cce3a..0a5b061 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,38 @@ php artisan serve Access [the GraphiQL UI](https://github.com/graphql/graphiql/blob/main/packages/graphiql/README.md) at `/graphiql`. +In order to log in through Sanctum, find out the email of the seeded user: + +```graphql +{ + users { + data { + email + } + } +} +``` + +Then, log in with the following mutation: + +```graphql +mutation { + login(email: "", password: "password") { + id + } +} +``` + +To validate you are in fact logged in, run the following query: + +```graphql +{ + me { + id + } +} +``` + ## Minimalism In order to keep maintenance as simple as possible, diff --git a/app/GraphQL/Mutations/Login.php b/app/GraphQL/Mutations/Login.php new file mode 100644 index 0000000..8a8c2df --- /dev/null +++ b/app/GraphQL/Mutations/Login.php @@ -0,0 +1,35 @@ +attempt($args)) { + throw new Error('Invalid credentials.'); + } + + $user = $guard->user(); + assert($user instanceof User, 'must receive User after successful login'); + + return $user; + } +} diff --git a/app/GraphQL/Mutations/Logout.php b/app/GraphQL/Mutations/Logout.php new file mode 100644 index 0000000..4fe6097 --- /dev/null +++ b/app/GraphQL/Mutations/Logout.php @@ -0,0 +1,27 @@ +user(); + + $guard->logout(); + + return $user; + } +} diff --git a/composer.json b/composer.json index 958817f..dcba391 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "require": { "php": "^8.2", "laravel/framework": "^11", + "laravel/sanctum": "^4", "laravel/tinker": "^2.9", "mll-lab/laravel-graphiql": "^3", "nuwave/lighthouse": "^6" diff --git a/composer.lock b/composer.lock index 1d98ff5..ddb9a74 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ff4a1f0427cc8e7e2ecf5c7fa6777f15", + "content-hash": "5e7f26bf951cb97cdd07906a393bd861", "packages": [ { "name": "brick/math", @@ -1427,6 +1427,70 @@ }, "time": "2024-12-30T15:53:31+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.0.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "698064236a46df016e64a7eb059b1414e0b281df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/698064236a46df016e64a7eb059b1414e0b281df", + "reference": "698064236a46df016e64a7eb059b1414e0b281df", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/database": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2024-12-11T16:40:21+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.1", diff --git a/config/graphiql.php b/config/graphiql.php new file mode 100644 index 0000000..ec586cf --- /dev/null +++ b/config/graphiql.php @@ -0,0 +1,64 @@ + [ + '/graphiql' => [ + 'name' => 'graphiql', + 'middleware' => ['web'], + // 'prefix' => '', + // 'domain' => 'graphql.' . env('APP_DOMAIN', 'localhost'), + + /* + |-------------------------------------------------------------------------- + | Default GraphQL endpoint + |-------------------------------------------------------------------------- + | + | The default endpoint that the GraphiQL UI is set to. + | It assumes you are running GraphQL on the same domain + | as GraphiQL, but can be set to any URL. + | + */ + + 'endpoint' => '/graphql', + + /* + |-------------------------------------------------------------------------- + | Subscription endpoint + |-------------------------------------------------------------------------- + | + | The default subscription endpoint the GraphiQL UI uses to connect to. + | Tries to connect to the `endpoint` value if `null` as ws://{{endpoint}} + | + | Example: `ws://your-endpoint` or `wss://your-endpoint` + | + */ + + 'subscription-endpoint' => env('GRAPHIQL_SUBSCRIPTION_ENDPOINT', null), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Control GraphiQL availability + |-------------------------------------------------------------------------- + | + | Control if the GraphiQL UI is accessible at all. + | This allows you to disable it in certain environments, + | for example you might not want it active in production. + | + */ + + 'enabled' => env('GRAPHIQL_ENABLED', true), +]; diff --git a/config/lighthouse.php b/config/lighthouse.php new file mode 100644 index 0000000..9014a2e --- /dev/null +++ b/config/lighthouse.php @@ -0,0 +1,54 @@ + false, to disable the default route + | registration and take full control. + | + */ + + 'route' => [ + /* + * The URI the endpoint responds to, e.g. mydomain.com/graphql. + */ + 'uri' => '/graphql', + + /* + * Lighthouse creates a named route for convenient URL generation and redirects. + */ + 'name' => 'graphql', + + /* + * Beware that middleware defined here runs before the GraphQL execution phase, + * make sure to return spec-compliant responses in case an error is thrown. + */ + 'middleware' => [ + // Ensures the request is not vulnerable to cross-site request forgery. + // Nuwave\Lighthouse\Http\Middleware\EnsureXHR::class, + + // Always set the `Accept: application/json` header. + Nuwave\Lighthouse\Http\Middleware\AcceptJson::class, + + Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + + // Logs in a user if they are authenticated. In contrast to Laravel's 'auth' + // middleware, this delegates auth and permission checks to the field level. + Nuwave\Lighthouse\Http\Middleware\AttemptAuthentication::class, + + // Logs every incoming GraphQL query. + // Nuwave\Lighthouse\Http\Middleware\LogGraphQLQueries::class, + ], + + /* + * The `prefix`, `domain` and `where` configuration options are optional. + */ + // 'prefix' => '', + // 'domain' => '', + // 'where' => [], + ], +]; diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..12b41ba --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,47 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1,127.0.0.1:8000,::1')), + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. If this value is null, personal access tokens do + | not expire. This won't tweak the lifetime of first-party sessions. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'verify_csrf_token' => \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class, + 'encrypt_cookies' => \Illuminate\Cookie\Middleware\EncryptCookies::class, + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..4e0f66c --- /dev/null +++ b/config/session.php @@ -0,0 +1,201 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | 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 immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + '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', null), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | While using one of the framework's cache driven session backends you may + | list a cache store that should be used for these sessions. This value + | must match with one of the application's configured cache "stores". + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE', null), + + /* + |-------------------------------------------------------------------------- + | 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 cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + '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 are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | 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 if it can not 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. You are free to modify this option if needed. + | + */ + + '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" since this is a secure default value. + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => 'lax', + +]; diff --git a/database/migrations/2024_11_13_083152_create_sessions_table.php b/database/migrations/2024_11_13_083152_create_sessions_table.php new file mode 100644 index 0000000..5fce475 --- /dev/null +++ b/database/migrations/2024_11_13_083152_create_sessions_table.php @@ -0,0 +1,25 @@ +string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sessions'); + } +}; diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 6772060..c10de1f 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -4,4 +4,12 @@ scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date") "A datetime string with format `Y-m-d H:i:s`, e.g. `2018-05-23 13:43:32`." scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime") +type Mutation { + "Log in to a new session and get the user." + login(email: String!, password: String!): User! + + "Log out from the current session, showing the user one last time." + logout: User @guard +} + #import models/*.graphql diff --git a/resources/views/vendor/graphiql/index.blade.php b/resources/views/vendor/graphiql/index.blade.php new file mode 100644 index 0000000..4c3b53e --- /dev/null +++ b/resources/views/vendor/graphiql/index.blade.php @@ -0,0 +1,94 @@ + + +@php use MLL\GraphiQL\DownloadAssetsCommand; @endphp + + + + + GraphiQL + + + + + + + + + +
Loading...
+ + + + + + diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100755 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore