├── js
├── src
│ ├── common
│ │ ├── index.ts
│ │ ├── models
│ │ │ ├── Record.js
│ │ │ ├── Token.js
│ │ │ ├── Scope.js
│ │ │ └── Client.js
│ │ ├── utils
│ │ │ └── randomString.js
│ │ └── extend.js
│ ├── admin
│ │ ├── index.js
│ │ ├── pages
│ │ │ ├── TokensPage.js
│ │ │ ├── ClientsPage.js
│ │ │ ├── ScopesPage.js
│ │ │ └── IndexPage.js
│ │ └── components
│ │ │ ├── SettingsPage.js
│ │ │ ├── AddTokenModal.js
│ │ │ ├── EditClientModal.js
│ │ │ └── EditScopeModal.js
│ └── forum
│ │ ├── components
│ │ ├── ScopeComponent.js
│ │ ├── user
│ │ │ └── AuthorizedPage.js
│ │ └── oauth
│ │ │ └── AuthorizePage.js
│ │ └── index.js
├── admin.ts
├── forum.ts
├── webpack.config.js
├── tsconfig.json
├── package.json
└── dist
│ ├── forum.js
│ ├── admin.js
│ └── forum.js.map
├── less
├── forum
│ ├── box.less
│ ├── column.less
│ ├── card.less
│ └── oauth.less
├── forum.less
└── admin.less
├── src
├── Models
│ ├── Jwt.php
│ ├── AccessToken.php
│ ├── RefreshToken.php
│ ├── AuthorizationCode.php
│ ├── Record.php
│ ├── Scope.php
│ └── Client.php
├── Api
│ ├── Controller
│ │ ├── ListClientController.php
│ │ ├── DeleteScopeController.php
│ │ ├── DeleteClientController.php
│ │ ├── CreateScopeController.php
│ │ ├── DeleteAllRecordController.php
│ │ ├── ListRecordController.php
│ │ ├── ShowClientController.php
│ │ ├── DeleteExpiredTokenController.php
│ │ ├── ListScopeController.php
│ │ ├── CreateClientController.php
│ │ ├── DeleteUserRecordController.php
│ │ ├── DeleteRecordController.php
│ │ ├── UpdateScopeController.php
│ │ ├── UpdateClientController.php
│ │ └── CreateTokenController.php
│ └── Serializer
│ │ ├── ScopeUserSerializer.php
│ │ ├── RecordSerializer.php
│ │ ├── AccessTokenSerializer.php
│ │ ├── ScopeSerializer.php
│ │ ├── ClientPublicSerializer.php
│ │ └── ClientSerializer.php
├── Middlewares
│ ├── UnsetCsrfMiddleware.php
│ ├── UserCredentialsMiddleware.php
│ ├── ResourceScopeFieldsMiddleware.php
│ └── ResourceScopeAuthMiddleware.php
├── Controllers
│ ├── TokenController.php
│ ├── ApiUserController.php
│ ├── AuthorizeController.php
│ └── AuthorizeFetchController.php
├── OAuth.php
└── Storage.php
├── migrations
├── 2023_09_29_add_columns_to_oauth_scopes_table.php
├── 2023_09_29_add_columns_to_oauth_clients_table.php
├── 2024_04_18_add_columns_to_oauth_scopes_table.php
├── 2023_09_28_create_oauth_jwt_table.php
├── 2024_02_25_create_oauth_records_table.php
├── 2023_09_28_create_oauth_scopes_table.php
├── 2023_10_02_add_default_record_to_oauth_scopes_table.php
├── 2023_09_28_create_oauth_access_tokens_table.php
├── 2023_09_28_create_oauth_refresh_tokens_table.php
├── 2023_09_28_create_oauth_clients_table.php
└── 2023_09_28_create_oauth_authorization_codes_table.php
├── README.md
├── LICENSE.md
├── locale
├── zh-Hans.yml
└── en.yml
├── composer.json
├── docs
├── zh.md
└── en.md
└── extend.php
/js/src/common/index.ts:
--------------------------------------------------------------------------------
1 | export { default as extend } from './extend';
2 |
--------------------------------------------------------------------------------
/js/admin.ts:
--------------------------------------------------------------------------------
1 | export * from './src/common';
2 | export * from './src/admin';
3 |
--------------------------------------------------------------------------------
/js/forum.ts:
--------------------------------------------------------------------------------
1 | export * from './src/common';
2 | export * from './src/forum';
3 |
--------------------------------------------------------------------------------
/js/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('flarum-webpack-config')();
2 |
--------------------------------------------------------------------------------
/js/src/common/models/Record.js:
--------------------------------------------------------------------------------
1 | import Model from 'flarum/common/Model';
2 |
3 | export default class Record extends Model {
4 | client = Model.hasOne('client');
5 | user_id = Model.attribute('user_id');
6 | authorized_at = Model.attribute('authorized_at', Model.transformDate);
7 | }
8 |
--------------------------------------------------------------------------------
/js/src/common/models/Token.js:
--------------------------------------------------------------------------------
1 | import Model from 'flarum/common/Model';
2 |
3 | export default class Token extends Model {
4 | access_token = Model.attribute('access_token');
5 | client_id = Model.attribute('client_id');
6 | user_id = Model.attribute('user_id');
7 | expires = Model.attribute('expires', Model.transformDate);
8 | scope = Model.attribute('scope');
9 | }
10 |
--------------------------------------------------------------------------------
/js/src/common/utils/randomString.js:
--------------------------------------------------------------------------------
1 | export function randomString(len) {
2 | len = len || 8;
3 | let $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
4 | let maxPos = $chars.length;
5 | let str = '';
6 | for (let i = 0; i < len; i++) {
7 | //0~32的整数
8 | str += $chars.charAt(Math.floor(Math.random() * (maxPos + 1)));
9 | }
10 | return str;
11 | }
12 |
--------------------------------------------------------------------------------
/js/src/common/extend.js:
--------------------------------------------------------------------------------
1 | import Extend from 'flarum/common/extenders';
2 | import Client from "./models/Client";
3 | import Scope from "./models/Scope";
4 | import Record from "./models/Record";
5 | import Token from "./models/Token";
6 |
7 | export default [
8 | new Extend.Store()
9 | .add('oauth-clients', Client)
10 | .add('oauth-scopes', Scope)
11 | .add('oauth-records', Record)
12 | .add('oauth-tokens', Token)
13 | ];
14 |
--------------------------------------------------------------------------------
/less/forum/box.less:
--------------------------------------------------------------------------------
1 | .oauth-main.oauth-box {
2 | position: relative;
3 | width: 100%;
4 | max-width: 376px;
5 | margin: 0 auto;
6 | box-sizing: border-box;
7 | box-shadow: 0 0 15px 0 @shadow-color;
8 | border-radius: @border-radius;
9 | }
10 |
11 | .oauth-main.oauth-box::before {
12 | backdrop-filter: blur(20px);
13 | content: '';
14 | position: absolute;
15 | width: 100%;
16 | height: 100%;
17 | background: hsla(0, 0%, 100%, .3);
18 | border-radius: @border-radius;
19 | }
20 |
--------------------------------------------------------------------------------
/src/Models/Jwt.php:
--------------------------------------------------------------------------------
1 | {
4 | app.extensionData
5 | .for('foskym-oauth-center')
6 | .registerPage(SettingsPage)
7 | .registerPermission(
8 | {
9 | icon: 'fas fa-user-friends',
10 | label: app.translator.trans('foskym-oauth-center.admin.permission.use_oauth'),
11 | permission: 'foskym-oauth-center.use-oauth',
12 | },
13 | 'view',
14 | 95
15 | );
16 | });
17 |
--------------------------------------------------------------------------------
/js/src/common/models/Client.js:
--------------------------------------------------------------------------------
1 | import Model from 'flarum/common/Model';
2 |
3 | export default class Client extends Model {
4 | client_id = Model.attribute('client_id');
5 | client_secret = Model.attribute('client_secret');
6 | redirect_uri = Model.attribute('redirect_uri');
7 | grant_types = Model.attribute('grant_types');
8 | scope = Model.attribute('scope');
9 | user_id = Model.attribute('user_id');
10 | client_name = Model.attribute('client_name');
11 | client_icon = Model.attribute('client_icon');
12 | client_desc = Model.attribute('client_desc');
13 | client_home = Model.attribute('client_home');
14 | }
15 |
--------------------------------------------------------------------------------
/src/Models/Record.php:
--------------------------------------------------------------------------------
1 | belongsTo(Client::class, 'client_id', 'client_id');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/less/forum/card.less:
--------------------------------------------------------------------------------
1 | .oauth-main.oauth-card {
2 | position: relative;
3 | margin: 0 auto;
4 | box-sizing: border-box;
5 | box-shadow: 0 0 15px 0 @shadow-color;
6 | border-radius: @border-radius;
7 | }
8 |
9 | .oauth-main.oauth-card::before {
10 | backdrop-filter: blur(20px);
11 | content: '';
12 | position: absolute;
13 | width: 100%;
14 | height: 100%;
15 | background: hsla(0, 0%, 100%, .3);
16 | border-radius: 12px;
17 | }
18 |
19 | .oauth-card {
20 | .oauth-body {
21 | display: flex;
22 | flex-direction: row-reverse;
23 | justify-content: space-between;
24 | flex-wrap: wrap;
25 | border-radius: 0 0 @border-radius @border-radius;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/migrations/2023_09_29_add_columns_to_oauth_scopes_table.php:
--------------------------------------------------------------------------------
1 | ['string', 'length' => 500, 'default' => null, 'nullable' => true],
16 | 'scope_icon' => ['string', 'length' => 500, 'default' => null, 'nullable' => true],
17 | 'scope_desc' => ['string', 'length' => 1000, 'default' => null, 'nullable' => true],
18 | ]);
19 |
--------------------------------------------------------------------------------
/src/Models/Scope.php:
--------------------------------------------------------------------------------
1 | get();
22 | // return $this->where('resource_path', $path)->first();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Api/Controller/ListClientController.php:
--------------------------------------------------------------------------------
1 | assertAdmin();
20 |
21 | return Client::all();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/js/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use Flarum's tsconfig as a starting point
3 | "extends": "flarum-tsconfig",
4 | // This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
5 | // and also tells your Typescript server to read core's global typings for
6 | // access to `dayjs` and `$` in the global namespace.
7 | "include": [
8 | "src/**/*",
9 | "../vendor/*/*/js/dist-typings/@types/**/*",
10 | //
11 | //
12 | "@types/**/*"
13 | ],
14 | "compilerOptions": {
15 | // This will output typings to `dist-typings`
16 | "declarationDir": "./dist-typings",
17 | "baseUrl": ".",
18 | "paths": {
19 | "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"],
20 | //
21 | //
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/migrations/2023_09_29_add_columns_to_oauth_clients_table.php:
--------------------------------------------------------------------------------
1 | ['string', 'length' => 500, 'default' => null, 'nullable' => true],
16 | 'client_icon' => ['string', 'length' => 500, 'default' => null, 'nullable' => true],
17 | 'client_desc' => ['string', 'length' => 1000, 'default' => null, 'nullable' => true],
18 | 'client_home' => ['string', 'length' => 200, 'default' => null, 'nullable' => true],
19 | ]);
20 |
--------------------------------------------------------------------------------
/src/Api/Controller/DeleteScopeController.php:
--------------------------------------------------------------------------------
1 | getQueryParams(), 'id');
18 | RequestUtil::getActor($request)
19 | ->assertAdmin();
20 |
21 | $scope = Scope::find($id);
22 |
23 | $scope->delete();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Api/Controller/DeleteClientController.php:
--------------------------------------------------------------------------------
1 | getQueryParams(), 'id');
19 | RequestUtil::getActor($request)
20 | ->assertAdmin();
21 |
22 | $client = Client::find($id);
23 |
24 | $client->delete();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/migrations/2024_04_18_add_columns_to_oauth_scopes_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if (!$schema->hasTable('oauth_scopes')) {
17 | return;
18 | }
19 |
20 | if (!$schema->hasColumn('oauth_scopes', 'visible_fields')) {
21 | $schema->table('oauth_scopes', function (Blueprint $table) {
22 | $table->string('visible_fields', 4000)->nullable()->after('method');
23 | });
24 | }
25 | },
26 | 'down' => function (Builder $schema) {
27 |
28 | },
29 | ];
30 |
--------------------------------------------------------------------------------
/src/Api/Controller/CreateScopeController.php:
--------------------------------------------------------------------------------
1 | assertAdmin();
20 |
21 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes');
22 |
23 | return Scope::create([
24 | 'scope' => Arr::get($attributes, 'scope'),
25 | ]);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Models/Client.php:
--------------------------------------------------------------------------------
1 | client_id = $client_id;
25 | $client->client_secret = $client_secret;
26 | $client->user_id = $user_id;
27 |
28 | return $client;
29 | }
30 |
31 | public function record()
32 | {
33 | return $this->hasMany(Record::class, 'client_id', 'client_id');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/migrations/2023_09_28_create_oauth_jwt_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if ($schema->hasTable('oauth_jwt')) {
17 | return;
18 | }
19 | $schema->create('oauth_jwt', function (Blueprint $table) {
20 | $table->increments('id');
21 | $table->string('client_id', 80);
22 | $table->string('subject', 80)->nullable();
23 | $table->string('public_key', 2000);
24 | });
25 | },
26 | 'down' => function (Builder $schema) {
27 | $schema->dropIfExists('oauth_jwt');
28 | },
29 | ];
30 |
--------------------------------------------------------------------------------
/migrations/2024_02_25_create_oauth_records_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if ($schema->hasTable('oauth_records')) {
17 | return;
18 | }
19 | $schema->create('oauth_records', function (Blueprint $table) {
20 | $table->increments('id');
21 | $table->string('client_id', 80);
22 | $table->string('user_id', 80)->nullable();
23 | $table->timestamp('authorized_at');
24 | });
25 | },
26 | 'down' => function (Builder $schema) {
27 | $schema->dropIfExists('oauth_records');
28 | },
29 | ];
30 |
--------------------------------------------------------------------------------
/less/forum.less:
--------------------------------------------------------------------------------
1 | @import url('./forum/oauth');
2 |
3 | .AuthorizedRecords {
4 | list-style: none;
5 | padding-inline-start: 0;
6 |
7 | .AuthorizedRecord-content {
8 | display: inline-flex;
9 | justify-content: space-between;
10 | width: 100%;
11 |
12 | .AuthorizedRecord-left {
13 | display: inline-flex;
14 |
15 | h3 {
16 | line-height: 1;
17 | margin-block-start: 0;
18 | margin-block-end: 0.5em;
19 | }
20 |
21 | .AuthorizedRecord-icon {
22 | display: inline-block;
23 | width: 40px;
24 | height: 40px;
25 | margin-right: 10px;
26 | overflow: hidden;
27 | background-color: #f0f0f0;
28 |
29 | img {
30 | width: 100%;
31 | height: 100%;
32 | }
33 | }
34 | }
35 |
36 | .AuthorizedRecord-right {
37 | display: grid;
38 |
39 | time {
40 | font-size: 0.8em;
41 | color: #666;
42 | float: right;
43 | }
44 |
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/migrations/2023_09_28_create_oauth_scopes_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if ($schema->hasTable('oauth_scopes')) {
17 | return;
18 | }
19 | $schema->create('oauth_scopes', function (Blueprint $table) {
20 | $table->increments('id');
21 | $table->string('scope', 80);
22 | $table->string('resource_path', 500);
23 | $table->string('method', 20);
24 | $table->boolean('is_default')->nullable();
25 | });
26 | },
27 | 'down' => function (Builder $schema) {
28 | $schema->dropIfExists('oauth_scopes');
29 | },
30 | ];
31 |
--------------------------------------------------------------------------------
/src/Api/Controller/DeleteAllRecordController.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
21 | }
22 | protected function delete(ServerRequestInterface $request)
23 | {
24 | $actor = RequestUtil::getActor($request);
25 | $actor->assertAdmin();
26 |
27 | $records = Record::where('user_id', '!=', 0);
28 | $records->delete();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Api/Serializer/ScopeUserSerializer.php:
--------------------------------------------------------------------------------
1 | $model->scope,
25 | "is_default" => $model->is_default,
26 | "scope_name" => $model->scope_name,
27 | "scope_icon" => $model->scope_icon,
28 | "scope_desc" => $model->scope_desc,
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Api/Controller/ListRecordController.php:
--------------------------------------------------------------------------------
1 | getQueryParams(), 'page', 0);
19 |
20 | $actor = RequestUtil::getActor($request);
21 | $actor->assertRegistered();
22 |
23 | $pageSize = 10;
24 | $skip = $page * $pageSize;
25 | $records = Record::where('user_id', $actor->id)
26 | ->orderBy('authorized_at', 'desc')
27 | ->skip($skip)
28 | ->take($pageSize)
29 | ->get();
30 |
31 | return $records;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/migrations/2023_10_02_add_default_record_to_oauth_scopes_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if (!$schema->hasTable('oauth_scopes')) {
17 | return;
18 | }
19 | $schema->getConnection()->table('oauth_scopes')->insert([
20 | 'scope' => 'user.read',
21 | 'resource_path' => '/api/user',
22 | 'method' => 'GET',
23 | 'is_default' => 1,
24 | 'scope_name' => '获取用户信息',
25 | 'scope_icon' => 'fas fa-user',
26 | 'scope_desc' => '访问该用户({user})的个人信息等',
27 | ]);
28 | },
29 | 'down' => function (Builder $schema) {
30 |
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OAuth Center
2 |
3 |  [](https://packagist.org/packages/foskym/flarum-oauth-center) [](https://packagist.org/packages/foskym/flarum-oauth-center)
4 |
5 | A [Flarum](http://flarum.org) extension. Allow user to authorize the third clients
6 |
7 | ## Installation
8 |
9 | Install with composer:
10 |
11 | ```sh
12 | composer require foskym/flarum-oauth-center:"*"
13 | ```
14 |
15 | ## Updating
16 |
17 | ```sh
18 | composer update foskym/flarum-oauth-center:"*"
19 | php flarum migrate
20 | php flarum cache:clear
21 | ```
22 |
23 | ## Usage
24 |
25 | - [中文文档](/docs/zh.md)
26 | - [English Docs](/docs/en.md)
27 |
28 | ## Links
29 |
30 | - [Packagist](https://packagist.org/packages/foskym/flarum-oauth-center)
31 | - [GitHub](https://github.com/foskym/flarum-oauth-center)
32 | - [Discuss](https://discuss.flarum.org/d/33413-oauth-center)
33 |
--------------------------------------------------------------------------------
/src/Api/Controller/ShowClientController.php:
--------------------------------------------------------------------------------
1 | getQueryParams(), 'client_id');
19 |
20 | $actor = RequestUtil::getActor($request);
21 | $actor->assertRegistered();
22 |
23 | if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) {
24 | return [];
25 | }
26 |
27 | $client = Client::where('client_id', $client_id)->get()->first();
28 |
29 | return $client;
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 FoskyM
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/src/Api/Controller/DeleteExpiredTokenController.php:
--------------------------------------------------------------------------------
1 | assertAdmin();
21 |
22 | AccessToken::where('expires', '<', date('Y-m-d H:i:s'))->delete();
23 |
24 | RefreshToken::where('expires', '<', date('Y-m-d H:i:s'))->delete();
25 |
26 | AuthorizationCode::where('expires', '<', date('Y-m-d H:i:s'))->delete();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Api/Serializer/RecordSerializer.php:
--------------------------------------------------------------------------------
1 | client->makeHidden(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id']);
23 | return [
24 | "id" => $model->id,
25 | "client" => $model->client,
26 | "user_id" => $model->user_id,
27 | "authorized_at" => $model->authorized_at
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/migrations/2023_09_28_create_oauth_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if ($schema->hasTable('oauth_access_tokens')) {
17 | return;
18 | }
19 | $schema->create('oauth_access_tokens', function (Blueprint $table) {
20 | $table->increments('id');
21 | $table->string('access_token', 40);
22 | $table->string('client_id', 80);
23 | $table->string('user_id', 80)->nullable();
24 | $table->timestamp('expires');
25 | $table->string('scope', 4000)->nullable();
26 | });
27 | },
28 | 'down' => function (Builder $schema) {
29 | $schema->dropIfExists('oauth_access_tokens');
30 | },
31 | ];
32 |
--------------------------------------------------------------------------------
/migrations/2023_09_28_create_oauth_refresh_tokens_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if ($schema->hasTable('oauth_refresh_tokens')) {
17 | return;
18 | }
19 | $schema->create('oauth_refresh_tokens', function (Blueprint $table) {
20 | $table->increments('id');
21 | $table->string('refresh_token', 40);
22 | $table->string('client_id', 80);
23 | $table->string('user_id', 80)->nullable();
24 | $table->timestamp('expires');
25 | $table->string('scope', 4000)->nullable();
26 | });
27 | },
28 | 'down' => function (Builder $schema) {
29 | $schema->dropIfExists('oauth_refresh_tokens');
30 | },
31 | ];
32 |
--------------------------------------------------------------------------------
/src/Api/Serializer/AccessTokenSerializer.php:
--------------------------------------------------------------------------------
1 | $model->id,
26 | "access_token" => $model->access_token,
27 | "client_id" => $model->client_id,
28 | "user_id" => $model->user_id,
29 | "expires" => $model->expires,
30 | "scope" => $model->scope
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Api/Controller/ListScopeController.php:
--------------------------------------------------------------------------------
1 | assertAdmin();
22 | } catch (\Exception $e) {
23 | $actor->assertRegistered();
24 | if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) {
25 | return [];
26 | }
27 | $this->serializer = ScopeUserSerializer::class;
28 | }
29 |
30 | return Scope::all();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/migrations/2023_09_28_create_oauth_clients_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if ($schema->hasTable('oauth_clients')) {
17 | return;
18 | }
19 | $schema->create('oauth_clients', function (Blueprint $table) {
20 | $table->increments('id');
21 | $table->string('client_id', 80);
22 | $table->string('client_secret', 80)->nullable();
23 | $table->string('redirect_uri', 2000)->nullable();
24 | $table->string('grant_types', 80)->nullable();
25 | $table->string('scope', 4000)->nullable();
26 | $table->string('user_id', 80)->nullable();
27 | });
28 | },
29 | 'down' => function (Builder $schema) {
30 | $schema->dropIfExists('oauth_clients');
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/js/src/forum/components/ScopeComponent.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/forum/app';
2 | import Component from 'flarum/common/Component';
3 | import Tooltip from 'flarum/common/components/Tooltip';
4 |
5 | export default class ScopeComponent extends Component {
6 | view() {
7 | const { scope, client } = this.attrs;
8 | return (
9 |
10 |
11 | {
12 | (scope.scope_icon().indexOf('fa-') > -1) ?
13 |
:
15 |
})
16 | }
17 |
18 |
19 |
20 | {scope.scope_name()}
21 |
22 |
23 | {
24 | scope.scope_desc()
25 | .replace('{client_name}', client.client_name())
26 | .replace('{user}', app.session.user.attribute('displayName'))
27 | }
28 |
29 |
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Api/Controller/CreateClientController.php:
--------------------------------------------------------------------------------
1 | assertAdmin();
20 |
21 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes');
22 |
23 | if ($client = Client::where('client_id', Arr::get($attributes, 'client_id'))->first()) {
24 | return $client;
25 | }
26 |
27 | return Client::create([
28 | 'client_id' => Arr::get($attributes, 'client_id'),
29 | 'client_secret' => Arr::get($attributes, 'client_secret'),
30 | 'user_id' => $actor->id,
31 | ]);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Api/Serializer/ScopeSerializer.php:
--------------------------------------------------------------------------------
1 | $model->id,
25 | "scope" => $model->scope,
26 | "resource_path" => $model->resource_path,
27 | "method" => $model->method,
28 | "visible_fields" => $model->visible_fields,
29 | "is_default" => $model->is_default,
30 | "scope_name" => $model->scope_name,
31 | "scope_icon" => $model->scope_icon,
32 | "scope_desc" => $model->scope_desc,
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Api/Serializer/ClientPublicSerializer.php:
--------------------------------------------------------------------------------
1 | $model->id,
25 | "client_id" => $model->client_id,
26 | "redirect_uri" => $model->redirect_uri,
27 | "grant_types" => $model->grant_types,
28 | "scope" => $model->scope,
29 | "client_name" => $model->client_name,
30 | "client_icon" => $model->client_icon,
31 | "client_desc" => $model->client_desc,
32 | "client_home" => $model->client_home
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/js/src/forum/index.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/forum/app';
2 | import {extend} from 'flarum/common/extend';
3 | import UserPage from 'flarum/forum/components/UserPage';
4 | import LinkButton from 'flarum/common/components/LinkButton';
5 | import AuthorizePage from "./components/oauth/AuthorizePage";
6 | import AuthorizedPage from "./components/user/AuthorizedPage";
7 | app.initializers.add('foskym/flarum-oauth-center', () => {
8 | app.routes['oauth.authorize'] = {
9 | path: '/oauth/authorize',
10 | component: AuthorizePage
11 | };
12 |
13 | app.routes['user.authorized'] = {
14 | path: '/u/:username/authorized',
15 | component: AuthorizedPage
16 | };
17 | extend(UserPage.prototype, 'navItems', function (items) {
18 | if (app.session.user && app.session.user.id() === this.user.id()) {
19 | items.add(
20 | 'authorized',
21 | LinkButton.component(
22 | {
23 | href: app.route('user.authorized', { username: this.user.username() }),
24 | icon: 'fas fa-user-friends',
25 | },
26 | [
27 | app.translator.trans('foskym-oauth-center.forum.page.label.authorized'),
28 | ]
29 | ),
30 | -110
31 | );
32 | }
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/migrations/2023_09_28_create_oauth_authorization_codes_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
16 | if ($schema->hasTable('oauth_authorization_codes')) {
17 | return;
18 | }
19 | $schema->create('oauth_authorization_codes', function (Blueprint $table) {
20 | $table->increments('id');
21 | $table->string('authorization_code', 40);
22 | $table->string('client_id', 80);
23 | $table->string('user_id', 80)->nullable();
24 | $table->string('redirect_uri', 2000)->nullable();
25 | $table->timestamp('expires');
26 | $table->string('scope', 4000)->nullable();
27 | $table->string('id_token', 1000)->nullable();
28 | });
29 | },
30 | 'down' => function (Builder $schema) {
31 | $schema->dropIfExists('oauth_authorization_codes');
32 | },
33 | ];
34 |
--------------------------------------------------------------------------------
/src/Api/Controller/DeleteUserRecordController.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
21 | }
22 | protected function delete(ServerRequestInterface $request)
23 | {
24 | $actor = RequestUtil::getActor($request);
25 | $actor->assertRegistered();
26 |
27 | $allow_delete_records = (bool) $this->settings->get('foskym-oauth-center.allow_delete_records');
28 |
29 | if ($allow_delete_records) {
30 | $user_id = $actor->id;
31 |
32 | $record = Record::where('user_id', $user_id);
33 | $record->delete();
34 | }
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Api/Controller/DeleteRecordController.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
21 | }
22 | protected function delete(ServerRequestInterface $request)
23 | {
24 | $actor = RequestUtil::getActor($request);
25 | $actor->assertRegistered();
26 |
27 | $allow_delete_records = (bool) $this->settings->get('foskym-oauth-center.allow_delete_records');
28 |
29 | if ($allow_delete_records) {
30 | $id = Arr::get($request->getQueryParams(), 'id');
31 |
32 | $record = Record::find($id);
33 | $record->delete();
34 | }
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@foskym/flarum-oauth-center",
3 | "private": true,
4 | "version": "0.0.0",
5 | "devDependencies": {
6 | "flarum-webpack-config": "^2.0.0",
7 | "webpack": "^5.65.0",
8 | "webpack-cli": "^4.9.1",
9 | "prettier": "^2.5.1",
10 | "@flarum/prettier-config": "^1.0.0",
11 | "flarum-tsconfig": "^1.0.2",
12 | "typescript": "^4.5.4",
13 | "typescript-coverage-report": "^0.6.1"
14 | },
15 | "scripts": {
16 | "dev": "webpack --mode development --watch",
17 | "build": "webpack --mode production",
18 | "analyze": "cross-env ANALYZER=true npm run build",
19 | "format": "prettier --write src",
20 | "format-check": "prettier --check src",
21 | "clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
22 | "build-typings": "npm run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && npm run post-build-typings",
23 | "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'",
24 | "check-typings": "tsc --noEmit --emitDeclarationOnly false",
25 | "check-typings-coverage": "typescript-coverage-report"
26 | },
27 | "prettier": "@flarum/prettier-config"
28 | }
29 |
--------------------------------------------------------------------------------
/src/Middlewares/UnsetCsrfMiddleware.php:
--------------------------------------------------------------------------------
1 | getUri()->getPath();
30 | if (in_array($path, $uri)) {
31 | $request = $request->withAttribute('bypassCsrfToken', true);
32 | }
33 |
34 | return $handler->handle($request);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Api/Serializer/ClientSerializer.php:
--------------------------------------------------------------------------------
1 | $model->id,
25 | "client_id" => $model->client_id,
26 | "client_secret" => $model->client_secret,
27 | "redirect_uri" => $model->redirect_uri,
28 | "grant_types" => $model->grant_types,
29 | "scope" => $model->scope,
30 | "user_id" => $model->user_id,
31 | "client_name" => $model->client_name,
32 | "client_icon" => $model->client_icon,
33 | "client_desc" => $model->client_desc,
34 | "client_home" => $model->client_home
35 | ];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Controllers/TokenController.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
29 | }
30 |
31 | public function handle(ServerRequestInterface $request): ResponseInterface
32 | {
33 | $oauth = new OAuth($this->settings);
34 | $server = $oauth->server();
35 |
36 | $body = $server->handleTokenRequest($oauth->request()::createFromGlobals())
37 | ->getResponseBody();
38 | return new JsonResponse(json_decode($body, true));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Api/Controller/UpdateScopeController.php:
--------------------------------------------------------------------------------
1 | assertAdmin();
20 |
21 | $id = Arr::get($request->getQueryParams(), 'id');
22 | $scope = Scope::find($id);
23 |
24 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []);
25 |
26 | collect(['scope', 'resource_path', 'method', 'visible_fields', 'is_default', 'scope_name', 'scope_icon', 'scope_desc'])
27 | ->each(function (string $attribute) use ($scope, $attributes) {
28 | if (($val = Arr::get($attributes, $attribute)) !== null) {
29 | $scope->$attribute = $val;
30 | }
31 | });
32 |
33 | $scope->save();
34 |
35 | return $scope;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Middlewares/UserCredentialsMiddleware.php:
--------------------------------------------------------------------------------
1 | getUri()->getPath();
23 | if (in_array($path, $uri) && Arr::get($request->getParsedBody(), 'grant_type', '') === 'password') {
24 | if ($user = User::where('username', Arr::get($request->getParsedBody(), 'username', ''))->first()) {
25 | if (!$user->hasPermission('foskym-oauth-center.use-oauth')) {
26 | return new JsonResponse([ 'error' => 'no_permission', 'error_description' => 'Don\'t have the permissions of oauth' ]);
27 | }
28 | }
29 | }
30 |
31 | return $handler->handle($request);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Api/Controller/UpdateClientController.php:
--------------------------------------------------------------------------------
1 | assertAdmin();
20 |
21 | $id = Arr::get($request->getQueryParams(), 'id');
22 | $client = Client::find($id);
23 |
24 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []);
25 |
26 | collect(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'client_name', 'client_desc', 'client_icon', 'client_home'])
27 | ->each(function (string $attribute) use ($client, $attributes) {
28 | if (($val = Arr::get($attributes, $attribute)) !== null) {
29 | if (($attribute == 'grant_types' || $attribute == 'scope') && $val === '')
30 | $client->$attribute = null;
31 | else
32 | $client->$attribute = $val;
33 | }
34 | });
35 |
36 | $client->save();
37 |
38 | return $client;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/js/src/admin/pages/TokensPage.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/admin/app';
2 | import Page from 'flarum/common/components/Page';
3 | import Button from 'flarum/common/components/Button';
4 | import Alert from 'flarum/common/components/Alert';
5 | import AddTokenModal from '../components/AddTokenModal';
6 |
7 | export default class TokensPage extends Page {
8 | translationPrefix = 'foskym-oauth-center.admin.tokens.';
9 | scopes = [];
10 |
11 | oninit(vnode) {
12 | super.oninit(vnode);
13 |
14 | this.fields = [];
15 |
16 | // app.store.find('oauth-tokens').then(r => {
17 | // this.scopes = r;
18 | // m.redraw();
19 | // });
20 | }
21 |
22 | view() {
23 | return (
24 |
25 | {[
26 | Button.component({
27 | type: 'button',
28 | className: 'Button',
29 | onclick: () => {
30 | this.showAddModal();
31 | }
32 | }, app.translator.trans('foskym-oauth-center.admin.tokens.add_token')),
33 | Button.component({
34 | type: 'button',
35 | className: 'Button',
36 | onclick: () => {
37 | this.deleteExpiredTokens();
38 | }
39 | }, app.translator.trans('foskym-oauth-center.admin.tokens.delete_token'))
40 | ]}
41 |
42 | );
43 | }
44 |
45 | deleteExpiredTokens() {
46 | app.request({
47 | method: 'DELETE',
48 | url: '/api/oauth-tokens/expired'
49 | }).then(() => {
50 | app.alerts.show(
51 | Alert,
52 | {type: 'success'},
53 | 'success!'
54 | )
55 | });
56 | }
57 |
58 | showAddModal() {
59 | app.modal.show(AddTokenModal, {});
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Controllers/ApiUserController.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
29 | }
30 |
31 | public function handle(ServerRequestInterface $request): ResponseInterface
32 | {
33 | $actor = RequestUtil::getActor($request);
34 | $actor = $actor->toArray();
35 | $data = [
36 | 'id' => $actor['id'],
37 | 'username' => $actor['username'],
38 | 'nickname' => $actor['nickname'],
39 | 'avatar_url' => $actor['avatar_url'],
40 | 'email' => $actor['email'],
41 | 'is_email_confirmed' => $actor['is_email_confirmed'],
42 | 'joined_at' => $actor['joined_at'],
43 | 'last_seen_at' => $actor['last_seen_at'],
44 | 'discussion_count' => $actor['discussion_count'],
45 | 'comment_count' => $actor['comment_count'],
46 | ];
47 | return new JsonResponse($data);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/locale/zh-Hans.yml:
--------------------------------------------------------------------------------
1 | foskym-oauth-center:
2 | admin:
3 | permission:
4 | use_oauth: 使用 OAuth 授权
5 | page:
6 | index: 首页
7 | clients: 应用管理
8 | scopes: 权限管理
9 | tokens: 令牌管理
10 | settings:
11 | display_mode: 显示模式
12 | access_lifetime: 访问令牌有效期(秒)
13 | allow_implicit: 允许隐式授权(response_type=token)
14 | enforce_state: 强制状态验证(state 参数)
15 | require_exact_redirect_uri: 需要精确的重定向 URI
16 | authorization_method_fetch: 授权使用 Fetch 方式请求(非直接转向页面)
17 | allow_delete_records: 允许删除授权记录
18 | clients:
19 | client_id: 应用 ID
20 | client_secret: 应用密钥
21 | redirect_uri: 回调地址
22 | grant_types: 授权类型
23 | scope: 权限
24 | client_name: 名称
25 | client_desc: 描述
26 | client_icon: 图标
27 | client_home: 主页
28 | add_button: 添加应用
29 | edit_client: 编辑应用
30 | scopes:
31 | scope: 权限标识
32 | resource_path: 资源路径
33 | method: 请求方法
34 | is_default: 默认
35 | scope_name: 名称
36 | scope_icon: 图标
37 | scope_desc: 描述
38 | visible_fields: 可见字段
39 | add_button: 添加权限
40 | edit_scope: 编辑权限
41 | tokens:
42 | add_token: 添加令牌
43 | delete_token: 删除过期令牌
44 | action:
45 | delete_all_records_button: 删除所有记录
46 | delete_all_records_confirm: 确认删除所有记录
47 | delete_all_records_success: 删除所有记录成功
48 |
49 | forum:
50 | page:
51 | title:
52 | authorize: 授权
53 | label:
54 | authorized: 授权记录
55 | authorize:
56 | require_these_scopes: 该应用需要以下权限
57 | access: 授权访问
58 | agree: 授权
59 | deny: 拒绝
60 | authorized:
61 | no_records: 暂无记录
62 | no_more_records: 没有更多记录了
63 | load_more: 加载更多
64 | delete_button: 删除此记录
65 | delete_all_button: 删除所有记录
66 | delete_all_confirm: 确认删除所有记录
67 |
--------------------------------------------------------------------------------
/src/Api/Controller/CreateTokenController.php:
--------------------------------------------------------------------------------
1 | assertAdmin();
20 |
21 | $attributes = Arr::get($request->getParsedBody(), 'data.attributes');
22 |
23 | $access_token = Arr::get($attributes, 'access_token');
24 | $client_id = Arr::get($attributes, 'client_id');
25 | $user_id = Arr::get($attributes, 'user_id');
26 | $scope = Arr::get($attributes, 'scope');
27 | $expires = date('Y-m-d H:i:s', strtotime(Arr::get($attributes, 'expires')) ?: time() + 3600 * 24);
28 |
29 | if (!$access_token || !$client_id || !$user_id || !$scope) {
30 | throw new \Exception('Invalid token attributes');
31 | }
32 |
33 | if (AccessToken::where('access_token', Arr::get($attributes, 'access_token'))->first()) {
34 | return AccessToken::where('access_token', Arr::get($attributes, 'access_token'))->first();
35 | }
36 |
37 | return AccessToken::create([
38 | 'access_token' => Arr::get($attributes, 'access_token'),
39 | 'client_id' => Arr::get($attributes, 'client_id'),
40 | 'user_id' => Arr::get($attributes, 'user_id'),
41 | 'expires' => $expires,
42 | 'scope' => Arr::get($attributes, 'scope'),
43 | ]);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/js/src/admin/components/SettingsPage.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/admin/app';
2 | import ExtensionPage from 'flarum/admin/components/ExtensionPage';
3 | import Button from 'flarum/common/components/Button';
4 | import IndexPage from "../pages/IndexPage";
5 | import ClientsPage from "../pages/ClientsPage";
6 | import ScopesPage from "../pages/ScopesPage";
7 | import TokensPage from "../pages/TokensPage";
8 |
9 | export default class SettingsPage extends ExtensionPage {
10 | translationPrefix = 'foskym-oauth-center.admin.page.';
11 | pages = {
12 | index: IndexPage,
13 | clients: ClientsPage,
14 | scopes: ScopesPage,
15 | tokens: TokensPage
16 | };
17 | icons = {
18 | index: 'home',
19 | clients: 'network-wired',
20 | scopes: 'user-lock',
21 | tokens: 'key',
22 | };
23 |
24 | content() {
25 | const page = m.route.param().page || 'index';
26 |
27 | return (
28 |
29 |
30 |
31 | {this.menuButtons(page)}
32 |
33 |
34 |
35 |
36 | {this.pageContent(page)}
37 |
38 |
39 | );
40 | }
41 |
42 | // Return button menus
43 | menuButtons(page) {
44 | return Object.keys(this.pages).map(key =>
45 | Button.component({
46 | className: `Button ${page === key ? 'item-selected' : ''}`,
47 | onclick: () => m.route.set(
48 | app.route('extension', {
49 | id: 'foskym-oauth-center',
50 | page: key
51 | })
52 | ),
53 | icon: this.iconForPage(key),
54 | }, app.translator.trans(this.translationPrefix + key))
55 | );
56 | }
57 |
58 | iconForPage(page) {
59 | return `fas fa-${this.icons[page]}` || '';
60 | }
61 |
62 | pageContent(page) {
63 | const PageComponent = this.pages[page];
64 | return PageComponent ? : null;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/js/src/admin/components/AddTokenModal.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/admin/app';
2 | import Modal from 'flarum/common/components/Modal';
3 | import Button from 'flarum/common/components/Button';
4 | import Stream from 'flarum/common/utils/Stream';
5 | export default class AddTokenModal extends Modal {
6 | oninit(vnode) {
7 | super.oninit(vnode);
8 |
9 | this.fields = [
10 | 'access_token',
11 | 'client_id',
12 | 'user_id',
13 | 'expires',
14 | 'scope'
15 | ];
16 |
17 | this.values = this.fields.reduce((values, key) => {
18 | values[key] = Stream('');
19 | return values;
20 | }, {});
21 | }
22 |
23 | className() {
24 | return 'AddTokenModal';
25 | }
26 |
27 | title() {
28 | return app.translator.trans('foskym-oauth-center.admin.tokens.add_token');
29 | }
30 |
31 | content() {
32 | return (
33 |
50 | );
51 | }
52 |
53 | onsubmit(e) {
54 | e.preventDefault();
55 | this.loading = true;
56 |
57 | const token = app.store.createRecord('oauth-tokens');
58 |
59 | const tokenData = this.fields.reduce((data, key) => {
60 | data[key] = this.values[key]();
61 | return data;
62 | }, {});
63 |
64 | token.save(tokenData).then(() => {
65 | this.loading = false;
66 | m.redraw();
67 | this.hide();
68 | });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/OAuth.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
30 | }
31 | public function response(): Response
32 | {
33 | return new Response;
34 | }
35 | public function request(): Request
36 | {
37 | return new Request;
38 | }
39 |
40 | public function storage(): Storage
41 | {
42 | return new Storage;
43 | }
44 | public function server(): Server
45 | {
46 | $storage = new Storage;
47 | $server = new Server($storage, array(
48 | 'allow_implicit' => $this->settings->get('foskym-oauth-center.allow_implicit') == "1",
49 | 'enforce_state' => $this->settings->get('foskym-oauth-center.enforce_state') == "1",
50 | 'require_exact_redirect_uri' => $this->settings->get('foskym-oauth-center.require_exact_redirect_uri') == "1",
51 | 'access_lifetime' => $this->settings->get('foskym-oauth-center.access_lifetime') == "" ? 3600 : $this->settings->get('foskym-oauth-center.access_lifetime'),
52 | ));
53 | $server->addGrantType(new AuthorizationCode($storage));
54 | $server->addGrantType(new ClientCredentials($storage));
55 | $server->addGrantType(new UserCredentials($storage));
56 | $server->addGrantType(new RefreshToken($storage));
57 | return $server;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/less/admin.less:
--------------------------------------------------------------------------------
1 | .OAuthCenter {
2 | padding-top: 0 !important;
3 | margin-top: 0 !important;
4 |
5 | .hidden {
6 | display: none;
7 | }
8 |
9 | .pull-right {
10 | float: right;
11 | }
12 |
13 | .oauth-menu {
14 | background: @control-bg;
15 | margin-bottom: 15px;
16 |
17 |
18 | .Button {
19 | padding: 12px 13px;
20 | border-radius: 0;
21 |
22 | &.item-selected {
23 | background: darken(@control-bg, 10%);
24 | }
25 | }
26 | }
27 |
28 | .OAuthCenterPage-container {
29 | max-width: 100% !important;
30 | }
31 |
32 | .OAuthCenter-clientsPage, .OAuthCenter-scopesPage {
33 | table {
34 | width: 100%;
35 |
36 | td, th {
37 | padding: 3px 5px;
38 | }
39 |
40 | th {
41 | text-align: left;
42 | }
43 | }
44 |
45 | .Checkbox {
46 | padding: 0 10px;
47 | }
48 |
49 | .FormControl {
50 | background: @body-bg;
51 | border-color: @control-bg;
52 |
53 | // We set the same as Flarum default, but with more specificity
54 | &:focus,
55 | &.focus {
56 | border-color: @primary-color;
57 | }
58 | }
59 | }
60 | }
61 | .OAuthCenter-Checkbox {
62 | .Checkbox-display {
63 | font-size: 22px;
64 | }
65 | }
66 | .OAuthCenter-Columns {
67 | display: flex;
68 | flex-wrap: wrap;
69 | justify-content: space-between;
70 |
71 | .OAuthCenter-Column {
72 | flex: 1;
73 | margin-right: 25px;
74 | margin-bottom: 20px;
75 |
76 | &:last-child {
77 | margin-right: 0;
78 | }
79 | }
80 | }
81 | @media (max-width: 992px) {
82 | .OAuthCenter-Columns {
83 | display: block;
84 | .OAuthCenter-Column {
85 | margin-right: 0;
86 | }
87 | }
88 | }
89 | .OAuthCenter-FormGroup-Column {
90 | width: 48%;
91 | display: inline-block;
92 | vertical-align: text-top;
93 | &:last-child {
94 | text-align: center;
95 | }
96 | }
97 |
98 | @media (min-width: 992px) {
99 | .OAuthCenter {
100 | .OAuthCenterPage-container {
101 | max-width: 600px;
102 | margin: 0;
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/locale/en.yml:
--------------------------------------------------------------------------------
1 | foskym-oauth-center:
2 | admin:
3 | permission:
4 | use_oauth: Use OAuth
5 | page:
6 | index: home
7 | clients: Manage Clients
8 | scopes: Manage Scopes
9 | tokens: Manage Tokens
10 | settings:
11 | display_mode: Display Mode
12 | access_lifetime: Access Token Lifetime (seconds)
13 | allow_implicit: Allow Implicit Grant (response_type=token)
14 | enforce_state: Enforce State Validation (state parameter)
15 | require_exact_redirect_uri: Require Exact Redirect URI
16 | authorization_method_fetch: Authorization Method Fetch (Not directly redirecting to)
17 | allow_delete_records: Allow Delete Authorization Records
18 | clients:
19 | client_id: Client ID
20 | client_secret: Client Secret
21 | redirect_uri: Redirect URI
22 | grant_types: Grant Types
23 | scope: Scope
24 | client_name: Name
25 | client_desc: Description
26 | client_icon: Icon
27 | client_home: HomePage
28 | add_button: Add Client
29 | edit_client: Edit Client
30 | scopes:
31 | scope: Scope
32 | resource_path: Resource Path
33 | method: Method
34 | is_default: Default
35 | scope_name: Name
36 | scope_icon: Icon
37 | scope_desc: Description
38 | visible_fields: Visible Fields
39 | add_button: Add Scope
40 | edit_scope: Edit Scope
41 | tokens:
42 | add_token: Add Token
43 | delete_token: Delete Token
44 | action:
45 | delete_all_records_button: Delete all records
46 | delete_all_records_confirm: Confirm delete all records
47 | delete_all_records_success: Delete all records successfully
48 |
49 | forum:
50 | page:
51 | title:
52 | authorize: Authorize
53 | label:
54 | authorized: Authorized Logs
55 | authorize:
56 | require_these_scopes: Require these scopes
57 | access: Access to
58 | agree: Agree
59 | deny: Deny
60 | authorized:
61 | no_records: No records
62 | no_more_records: No more records
63 | load_more: Load More
64 | delete_button: Delete this log
65 | delete_all_button: Delete all logs
66 | delete_all_confirm: Confirm delete all logs
67 |
--------------------------------------------------------------------------------
/src/Controllers/AuthorizeController.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
31 | }
32 |
33 | public function handle(ServerRequestInterface $request): ResponseInterface
34 | {
35 | $actor = RequestUtil::getActor($request);
36 | $actor->assertRegistered();
37 |
38 | if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) {
39 | return new HtmlResponse('Don\'t have the permissions of oauth');
40 | }
41 |
42 | $params = $request->getParsedBody();
43 |
44 | $oauth = new OAuth($this->settings);
45 | $server = $oauth->server();
46 | $request = $oauth->request()::createFromGlobals();
47 | $response = $oauth->response();
48 |
49 | if (!$server->validateAuthorizeRequest($request, $response)) {
50 | $response->send();
51 | die;
52 | }
53 |
54 | $is_authorized = (bool) Arr::get($params, 'is_authorized', 0);
55 | if ($is_authorized) {
56 | Record::create([
57 | 'client_id' => Arr::get($params, 'client_id'),
58 | 'user_id' => $actor->id,
59 | 'authorized_at' => date('Y-m-d H:i:s')
60 | ]);
61 | }
62 | $server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id);
63 | $response->send();
64 | die;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Middlewares/ResourceScopeFieldsMiddleware.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
28 | }
29 | public function process(Request $request, RequestHandlerInterface $handler): Response
30 | {
31 | $actor = RequestUtil::getActor($request);
32 | $response = $handler->handle($request);
33 | if ($scopes = $request->getAttribute('oauth.scopes')) {
34 | return $this->filterResponse($response, $scopes);
35 | }
36 | return $response;
37 | }
38 |
39 | private function filterResponse(Response $response, $scopes)
40 | {
41 | $data = [];
42 | $origin = json_decode($response->getBody(), true);
43 |
44 | $visible_fields = [];
45 |
46 | foreach ($scopes as $scope) {
47 | $visible_fields = array_merge($visible_fields, explode(',', $scope['visible_fields']));
48 | }
49 |
50 | $visible_fields = array_unique(array_values(array_filter($visible_fields)));
51 |
52 | if (empty($visible_fields)) {
53 | return $response;
54 | }
55 |
56 | foreach ($visible_fields as $visible_field) {
57 | if (Arr::has($origin, $visible_field)) {
58 | Arr::set($data, $visible_field, Arr::get($origin, $visible_field));
59 | }
60 | }
61 |
62 | return new JsonResponse($data, $response->getStatusCode());
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "foskym/flarum-oauth-center",
3 | "description": "Allow user to authorize the third clients",
4 | "keywords": [
5 | "flarum",
6 | "user",
7 | "oauth",
8 | "center"
9 | ],
10 | "type": "flarum-extension",
11 | "license": "MIT",
12 | "require": {
13 | "flarum/core": "^1.8",
14 | "bshaffer/oauth2-server-php": "*"
15 | },
16 | "authors": [
17 | {
18 | "name": "FoskyM",
19 | "email": "i@fosky.top",
20 | "role": "Developer",
21 | "homepage": "https://fosky.top"
22 | }
23 | ],
24 | "autoload": {
25 | "psr-4": {
26 | "FoskyM\\OAuthCenter\\": "src/"
27 | }
28 | },
29 | "extra": {
30 | "flarum-extension": {
31 | "title": "OAuth Center",
32 | "category": "",
33 | "icon": {
34 | "name": "fas fa-user-friends",
35 | "color": "#fff",
36 | "backgroundColor": "#c12c1f"
37 | }
38 | },
39 | "flarum-cli": {
40 | "modules": {
41 | "admin": true,
42 | "forum": true,
43 | "js": true,
44 | "jsCommon": true,
45 | "css": true,
46 | "locale": true,
47 | "gitConf": true,
48 | "githubActions": true,
49 | "prettier": true,
50 | "typescript": true,
51 | "bundlewatch": false,
52 | "backendTesting": true,
53 | "editorConfig": true,
54 | "styleci": true
55 | }
56 | }
57 | },
58 | "minimum-stability": "dev",
59 | "prefer-stable": true,
60 | "autoload-dev": {
61 | "psr-4": {
62 | "FoskyM\\OAuthCenter\\Tests\\": "tests/"
63 | }
64 | },
65 | "scripts": {
66 | "test": [
67 | "@test:unit",
68 | "@test:integration"
69 | ],
70 | "test:unit": "phpunit -c tests/phpunit.unit.xml",
71 | "test:integration": "phpunit -c tests/phpunit.integration.xml",
72 | "test:setup": "@php tests/integration/setup.php"
73 | },
74 | "scripts-descriptions": {
75 | "test": "Runs all tests.",
76 | "test:unit": "Runs all unit tests.",
77 | "test:integration": "Runs all integration tests.",
78 | "test:setup": "Sets up a database for use with integration tests. Execute this only once."
79 | },
80 | "require-dev": {
81 | "flarum/testing": "^1.0.0"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Controllers/AuthorizeFetchController.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
30 | }
31 |
32 | public function handle(ServerRequestInterface $request): ResponseInterface
33 | {
34 | $actor = RequestUtil::getActor($request);
35 | $actor->assertRegistered();
36 |
37 | if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) {
38 | return new JsonResponse([ 'error' => 'no_permission', 'error_description' => 'Don\'t have the permissions of oauth' ]);
39 | }
40 |
41 | $params = $request->getParsedBody();
42 |
43 | $oauth = new OAuth($this->settings);
44 | $server = $oauth->server();
45 | $request = $oauth->request()::createFromGlobals();
46 | $response = $oauth->response();
47 |
48 | if (!$server->validateAuthorizeRequest($request, $response)) {
49 | return new JsonResponse(json_decode($response->getResponseBody(), true));
50 | }
51 |
52 | $is_authorized = Arr::get($params, 'is_authorized', 0);
53 | $server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id);
54 | if ($is_authorized) {
55 | Record::create([
56 | 'client_id' => Arr::get($params, 'client_id'),
57 | 'user_id' => $actor->id,
58 | 'authorized_at' => date('Y-m-d H:i:s')
59 | ]);
60 | // $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=') + 5, 40);
61 | return new JsonResponse([
62 | 'location' => $response->getHttpHeader('Location')
63 | ]);
64 | }
65 |
66 | return new JsonResponse(json_decode($response->getResponseBody(), true));
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/js/src/admin/components/EditClientModal.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/admin/app';
2 | import Modal from 'flarum/common/components/Modal';
3 | import Button from 'flarum/common/components/Button';
4 | import Stream from 'flarum/common/utils/Stream';
5 | export default class EditClientModal extends Modal {
6 | oninit(vnode) {
7 | super.oninit(vnode);
8 |
9 | this.client = this.attrs.client;
10 | this.fields = [
11 | 'client_id',
12 | 'client_secret',
13 | 'redirect_uri',
14 | 'grant_types',
15 | 'scope',
16 |
17 | 'client_name',
18 | 'client_desc',
19 | 'client_icon',
20 | 'client_home'
21 | ];
22 |
23 | this.values = this.fields.reduce((values, key) => {
24 | values[key] = Stream(this.client[key]() || '');
25 | return values;
26 | }, {});
27 | }
28 |
29 | className() {
30 | return 'EditClientModal Modal--large';
31 | }
32 |
33 | title() {
34 | return app.translator.trans('foskym-oauth-center.admin.clients.edit_client');
35 | }
36 |
37 | content() {
38 | return (
39 |
69 | );
70 | }
71 |
72 | onsubmit(e) {
73 | e.preventDefault();
74 |
75 | this.loading = true;
76 |
77 | const clientData = this.fields.reduce((data, key) => {
78 | data[key] = this.values[key]();
79 | return data;
80 | }, {});
81 |
82 | this.client.save(clientData).then(() => {
83 | this.loading = false;
84 | m.redraw();
85 | this.hide();
86 | });
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/js/src/admin/pages/ClientsPage.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/admin/app';
2 | import Page from 'flarum/common/components/Page';
3 | import Button from 'flarum/common/components/Button';
4 | import EditClientModal from '../components/EditClientModal';
5 | import {randomString} from "../../common/utils/randomString";
6 |
7 | export default class ClientsPage extends Page {
8 | translationPrefix = 'foskym-oauth-center.admin.clients.';
9 | clients = [];
10 |
11 | oninit(vnode) {
12 | super.oninit(vnode);
13 |
14 | app.store.find('oauth-clients').then(r => {
15 | this.clients = r;
16 | m.redraw();
17 | });
18 | }
19 |
20 | view() {
21 | return (
22 |
23 | {
24 | m('.Form-group', [
25 | m('table', [
26 | m('thead', m('tr', [
27 | ['client_id', 'client_name'].map(key => m('th', app.translator.trans(this.translationPrefix + key))),
28 | m('th'),
29 | ])),
30 | m('tbody', [
31 | this.clients.map((client, index) => m('tr', [
32 | ['client_id', 'client_name'].map(key =>
33 | m('td', client[key]())
34 | ),
35 | m('td', [
36 | Button.component({
37 | className: 'Button Button--icon',
38 | icon: 'fas fa-edit',
39 | onclick: () => this.showEditModal(client),
40 | }),
41 | Button.component({
42 | className: 'Button Button--icon',
43 | icon: 'fas fa-times',
44 | onclick: () => {
45 | client.delete();
46 | this.clients.splice(index, 1);
47 | },
48 | }),
49 | ]),
50 | ])),
51 | m('tr', m('td', {
52 | colspan: 2,
53 | }, Button.component({
54 | className: 'Button Button--block',
55 | onclick: () => {
56 | const client_id = randomString(32);
57 | const client_secret = randomString(32);
58 | app.store.createRecord('oauth-clients').save({
59 | client_id: client_id,
60 | client_secret: client_secret,
61 | }).then(client => {
62 | this.clients.push(client);
63 | this.showEditModal(client);
64 | });
65 | },
66 | }, app.translator.trans(this.translationPrefix + 'add_button')))),
67 | ]),
68 | ]),
69 | ])
70 | }
71 |
72 | );
73 | }
74 |
75 | showEditModal(client) {
76 | app.modal.show(EditClientModal, { client: client });
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/js/src/admin/pages/ScopesPage.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/admin/app';
2 | import Page from 'flarum/common/components/Page';
3 | import Button from 'flarum/common/components/Button';
4 | import {randomString} from "../../common/utils/randomString";
5 | import EditScopeModal from "../components/EditScopeModal";
6 |
7 | export default class ScopesPage extends Page {
8 | translationPrefix = 'foskym-oauth-center.admin.scopes.';
9 | scopes = [];
10 |
11 | oninit(vnode) {
12 | super.oninit(vnode);
13 |
14 | this.fields = [
15 | 'scope',
16 | 'resource_path',
17 | 'method',
18 | 'is_default',
19 | 'scope_name',
20 | 'scope_icon',
21 | 'scope_desc'
22 | ];
23 |
24 | app.store.find('oauth-scopes').then(r => {
25 | this.scopes = r;
26 | m.redraw();
27 | });
28 | }
29 |
30 | view() {
31 | return (
32 |
33 | {
34 | m('.Form-group', [
35 | m('table', [
36 | m('thead', m('tr', [
37 | ['scope', 'scope_name'].map(key => m('th', app.translator.trans(this.translationPrefix + key))),
38 | m('th'),
39 | ])),
40 | m('tbody', [
41 | this.scopes.map((scope, index) => m('tr', [
42 | ['scope', 'scope_name'].map(key =>
43 | m('td', scope[key]())
44 | ),
45 | m('td', [
46 | Button.component({
47 | className: 'Button Button--icon',
48 | icon: 'fas fa-edit',
49 | onclick: () => this.showEditModal(scope),
50 | }),
51 | Button.component({
52 | className: 'Button Button--icon',
53 | icon: 'fas fa-times',
54 | onclick: () => {
55 | scope.delete();
56 | this.scopes.splice(index, 1);
57 | },
58 | }),
59 | ]),
60 | ])),
61 | m('tr', m('td', {
62 | colspan: 2,
63 | }, Button.component({
64 | className: 'Button Button--block',
65 | onclick: () => {
66 | app.store.createRecord('oauth-scopes')
67 | .save({
68 | 'scope': 'Scope.' + randomString(8),
69 | 'resource_path': '/api/' + randomString(4),
70 | 'method': 'GET',
71 | })
72 | .then(scope => {
73 | this.scopes.push(scope);
74 | this.showEditModal(scope);
75 | });
76 | },
77 | }, app.translator.trans(this.translationPrefix + 'add_button')))),
78 | ]),
79 | ]),
80 | ])
81 | }
82 |
83 | );
84 | }
85 |
86 | showEditModal(scope) {
87 | app.modal.show(EditScopeModal, {scope: scope});
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/docs/zh.md:
--------------------------------------------------------------------------------
1 | ### 配置项
2 | 
3 | #### 配置项说明
4 | - `允许隐式授权` response_type=token 的方式,令牌直接通过 hash 返回给客户端,详细说明可百度
5 | - `强制状态验证` state 参数必须存在
6 | - `精确的重定向 URI` 传参时的重定向 URI 必须和创建应用时填写的一致
7 | - `令牌有效期` 令牌的有效期,单位为秒
8 |
9 | ### 创建应用
10 | 
11 | #### 应用创建说明
12 | - `应用名称` 应用的名称
13 | - `应用描述` 应用的描述
14 | - `应用图标` 应用的图标,可选
15 | - `应用主页` 应用的主页,可选
16 | - `应用回调地址` 应用的回调地址,必填,多个地址使用空格分隔(不推荐单个应用使用多个地址)
17 | - `权限` 可选(不清楚的话不要填)
18 | - `授权类型` 可选(不清楚的话不要填)
19 | - `应用 ID` 和 `应用密钥` 用于客户端认证,添加应用时自动生成,不要泄露给其他人
20 |
21 | ### 设置资源控制器的权限 (user.read 项是默认生成的权限)
22 | 
23 | 大部分人只需要 `user.read` 权限即可,如果你需要更多的权限,可以在这里添加(或许你需要先了解一下 OAuth 中的 scope)
24 | #### 权限说明
25 | - `权限标识` 权限唯一标识符,用于区分,可参考 `Github` 的权限标识
26 | - `资源路径` 需要鉴权的资源路径
27 | - `请求方法` 资源路径鉴权的请求方法,一般为 `GET`
28 | - `默认` 勾选此项后哪怕传参时 scope 参数中无此权限标识,也会默认添加此权限
29 | - `名称` 权限的名称,用于显示
30 | - `描述` 权限的描述,用于显示,可使用 `{user}` `{client_name}` 变量指代用户和客户端名称
31 | - `图标` 支持 `FontAwesome` 图标和普通图片
32 |
33 | ### 本插件相关路径
34 | #### 授权
35 | `/oauth/authorize`
36 |
37 | | 参数 | 说明 | 必填 | 默认值 | 示例 |
38 | | --- | --- | --- | --- | --- |
39 | | client_id | 应用 ID | 是 | 无 | 123456 |
40 | | response_type | 授权类型 | 是 | 无 | code 或 token |
41 | | redirect_uri | 重定向 URI | 是 | 应用回调地址 | https://example.com/oauth/callback |
42 | | scope | 权限 | 否 | 无 | user.read |
43 | | state | 状态 | 否 | 无 | 123456 |
44 |
45 | 示例:
46 | ```
47 | GET https://example.com/oauth/authorize?client_id=123456&response_type=code&redirect_uri=https://user.example.com/oauth/callback&scope=user.read&state=123456
48 | ```
49 |
50 | #### 令牌
51 | `/oauth/token`
52 |
53 | | 参数 | 说明 | 必填 | 默认值 | 示例 |
54 | | --- | --- | --- | --- | --- |
55 | | client_id | 应用 ID | 是 | 无 | 123456 |
56 | | client_secret | 应用密钥 | 是 | 无 | 123456 |
57 | | grant_type | 授权类型 | 是 | 无 | authorization_code 或 refresh_token |
58 | | code | 授权码 | 授权类型为 authorization_code 时必填 | 无 | 123456 |
59 | | refresh_token | 刷新令牌 | 授权类型为 refresh_token 时必填 | 无 | 123456 |
60 | | redirect_uri | 重定向 URI | 授权类型为 authorization_code 时必填 | 应用回调地址 | https://example.com/oauth/callback |
61 |
62 | 示例:
63 |
64 | ```
65 | POST https://example.com/oauth/token
66 |
67 | Payload: client_id=123456&client_secret=123456&grant_type=authorization_code&code=123456&redirect_uri=https://example.com/oauth/callback
68 | ```
69 |
70 | #### 资源(用户)
71 | `/api/user`
72 |
73 | | 参数 | 说明 | 必填 | 默认值 | 示例 |
74 | | --- | --- | --- | --- | --- |
75 | | access_token | 访问令牌 | 是 | 无 | 123456 |
76 |
77 | 示例:
78 |
79 | ```
80 | GET https://example.com/api/user?access_token=123456
81 | ```
82 |
83 | ### 和常规的 OAuth 应用一样使用
84 | 
85 |
86 | ### 授权后获取令牌
87 | 
88 |
89 | ### 使用令牌获取资源 (使用 get 或 header 方式)
90 | 
91 | 
92 |
--------------------------------------------------------------------------------
/src/Middlewares/ResourceScopeAuthMiddleware.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
29 | }
30 | public function process(Request $request, RequestHandlerInterface $handler): Response
31 | {
32 | if (!$request->getAttribute('originalUri')) {
33 | return $handler->handle($request);
34 | }
35 |
36 | $headerLine = $request->getHeaderLine('authorization');
37 |
38 | $parts = explode(';', $headerLine);
39 |
40 | if (isset($parts[0]) && Str::startsWith($parts[0], self::TOKEN_PREFIX)) {
41 | $token = substr($parts[0], strlen(self::TOKEN_PREFIX));
42 | } else {
43 | $token = Arr::get($request->getQueryParams(), 'access_token', '');
44 | }
45 | $path = $request->getAttribute('originalUri')->getPath();
46 |
47 | if ($token !== '' && $scopes = Scope::get_path_scope($path)) {
48 | try {
49 | $oauth = new OAuth($this->settings);
50 | $server = $oauth->server();
51 | $oauth_request = $oauth->request()::createFromGlobals();
52 | $has_scope = false;
53 | $filtered_scopes = [];
54 | foreach ($scopes as $scope) {
55 | if (strtolower($request->getMethod()) === strtolower($scope->method)) {
56 | if ($server->verifyResourceRequest($oauth_request, null, $scope->scope)) {
57 | $token = $server->getAccessTokenData($oauth_request);
58 | if (!in_array($scope->scope, explode(' ', $token['scope']))) {
59 | continue;
60 | }
61 | $filtered_scopes[] = $scope;
62 | $has_scope = true;
63 | }
64 | }
65 | }
66 | if (!$has_scope) {
67 | return new JsonResponse(json_decode($server->getResponse()->getResponseBody(), true));
68 | }
69 | $actor = User::find($token['user_id']);
70 | $request = RequestUtil::withActor($request, $actor);
71 | $request = $request->withAttribute('oauth.scopes', $filtered_scopes);
72 | $request = $request->withAttribute('bypassCsrfToken', true);
73 | $request = $request->withoutAttribute('session');
74 |
75 | } catch (ValidationException $exception) {
76 |
77 | $handler = resolve(IlluminateValidationExceptionHandler::class);
78 |
79 | $error = $handler->handle($exception);
80 |
81 | return (new JsonApiFormatter())->format($error, $request);
82 | }
83 |
84 | }
85 |
86 | return $handler->handle($request);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/js/src/admin/components/EditScopeModal.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/admin/app';
2 | import Modal from 'flarum/common/components/Modal';
3 | import Button from 'flarum/common/components/Button';
4 | import Stream from 'flarum/common/utils/Stream';
5 | import Select from 'flarum/common/components/Select';
6 | import Checkbox from 'flarum/common/components/Checkbox';
7 |
8 | export default class EditScopeModal extends Modal {
9 | oninit(vnode) {
10 | super.oninit(vnode);
11 |
12 | this.scope = this.attrs.scope;
13 | this.fields = [
14 | 'scope',
15 | 'resource_path',
16 | 'method',
17 | 'is_default',
18 | 'scope_name',
19 | 'scope_icon',
20 | 'scope_desc',
21 |
22 | 'visible_fields',
23 | ];
24 |
25 | this.values = this.fields.reduce((values, key) => {
26 | values[key] = Stream(this.scope[key]() || '');
27 | return values;
28 | }, {});
29 | }
30 |
31 | className() {
32 | return 'EditScopeModal Modal--large';
33 | }
34 |
35 | title() {
36 | return app.translator.trans('foskym-oauth-center.admin.scopes.edit_scope');
37 | }
38 |
39 | content() {
40 | return (
41 |
68 | );
69 | }
70 |
71 | onsubmit(e) {
72 | e.preventDefault();
73 |
74 | this.loading = true;
75 |
76 | const scopeData = this.fields.reduce((data, key) => {
77 | data[key] = this.values[key]();
78 | return data;
79 | }, {});
80 |
81 | this.scope.save(scopeData).then(() => {
82 | this.loading = false;
83 | m.redraw();
84 | this.hide();
85 | });
86 | }
87 |
88 | renderFormGroups(fields) {
89 | return fields.map(key =>
90 |
91 |
92 | {key === 'method' ? Select.component({
93 | options: {
94 | 'GET': 'GET',
95 | 'POST': 'POST',
96 | 'PUT': 'PUT',
97 | 'DELETE': 'DELETE',
98 | 'PATCH': 'PATCH',
99 | },
100 | value: this.values[key](),
101 | onchange: this.values[key],
102 | }) : key === 'is_default' ? Checkbox.component({
103 | className: 'OAuthCenter-Checkbox',
104 | state: this.values[key]() === 1 || false,
105 | onchange: (checked) => this.values[key](checked ? 1 : 0),
106 | }) : }
107 |
108 | )
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/extend.php:
--------------------------------------------------------------------------------
1 | js(__DIR__.'/js/dist/forum.js')
26 | ->css(__DIR__.'/less/forum.less')
27 | ->route('/oauth/authorize', 'oauth.authorize'),
28 |
29 | (new Extend\Frontend('admin'))
30 | ->js(__DIR__.'/js/dist/admin.js')
31 | ->css(__DIR__.'/less/admin.less'),
32 | new Extend\Locales(__DIR__.'/locale'),
33 |
34 | (new Extend\Routes('forum'))
35 | ->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class)
36 | ->post('/oauth/authorize/fetch', 'oauth.authorize.fetch', Controllers\AuthorizeFetchController::class)
37 | ->post('/oauth/token', 'oauth.token', Controllers\TokenController::class),
38 |
39 | (new Extend\Routes('api'))
40 | ->get('/oauth-clients', 'oauth.clients.list', Api\Controller\ListClientController::class)
41 | ->post('/oauth-clients', 'oauth.clients.create', Api\Controller\CreateClientController::class)
42 | ->get('/oauth-clients/{client_id}', 'oauth.clients.show', Api\Controller\ShowClientController::class)
43 | ->patch('/oauth-clients/{id}', 'oauth.clients.update', Api\Controller\UpdateClientController::class)
44 | ->delete('/oauth-clients/{id}', 'oauth.clients.delete', Api\Controller\DeleteClientController::class)
45 |
46 | ->get('/oauth-scopes', 'oauth.scopes.list', Api\Controller\ListScopeController::class)
47 | ->post('/oauth-scopes', 'oauth.scopes.create', Api\Controller\CreateScopeController::class)
48 | ->patch('/oauth-scopes/{id}', 'oauth.scopes.update', Api\Controller\UpdateScopeController::class)
49 | ->delete('/oauth-scopes/{id}', 'oauth.scopes.delete', Api\Controller\DeleteScopeController::class)
50 |
51 | ->post('/oauth-tokens', 'oauth.tokens.create', Api\Controller\CreateTokenController::class)
52 | ->delete('/oauth-tokens/expired', 'oauth.tokens.delete.expired', Api\Controller\DeleteExpiredTokenController::class)
53 |
54 | ->get('/oauth-records', 'oauth.records.list', Api\Controller\ListRecordController::class)
55 | ->delete('/oauth-records/all', 'oauth.records.delete.all', Api\Controller\DeleteAllRecordController::class)
56 | ->delete('/oauth-records/user', 'oauth.records.delete.user', Api\Controller\DeleteUserRecordController::class)
57 | ->delete('/oauth-records/{id}', 'oauth.records.delete', Api\Controller\DeleteRecordController::class)
58 |
59 | ->get('/user', 'user.show', Controllers\ApiUserController::class),
60 |
61 | (new Extend\Settings)
62 | ->serializeToForum('foskym-oauth-center.display_mode', 'foskym-oauth-center.display_mode')
63 | ->serializeToForum('foskym-oauth-center.allow_implicit', 'foskym-oauth-center.allow_implicit', 'boolval')
64 | ->serializeToForum('foskym-oauth-center.enforce_state', 'foskym-oauth-center.enforce_state', 'boolval')
65 | ->serializeToForum('foskym-oauth-center.require_exact_redirect_uri', 'foskym-oauth-center.require_exact_redirect_uri', 'boolval')
66 | ->serializeToForum('foskym-oauth-center.authorization_method_fetch', 'foskym-oauth-center.authorization_method_fetch', 'boolval')
67 | ->serializeToForum('foskym-oauth-center.allow_delete_records', 'foskym-oauth-center.allow_delete_records', 'boolval'),
68 |
69 | (new Extend\Middleware('api'))
70 | ->insertAfter(AuthenticateWithHeader::class, ResourceScopeAuthMiddleware::class)
71 | ->add(ResourceScopeFieldsMiddleware::class),
72 | (new Extend\Middleware('forum'))
73 | ->insertBefore(CheckCsrfToken::class, UnsetCsrfMiddleware::class)
74 | ->insertAfter(CheckCsrfToken::class, UserCredentialsMiddleware::class),
75 | ];
76 |
--------------------------------------------------------------------------------
/docs/en.md:
--------------------------------------------------------------------------------
1 | ### setting
2 | 
3 |
4 | - `Allow Implicit Grant` a way to return token directly to client, you can google it
5 | - `Enforce State Validation` `state` must be provided
6 | - `Require Exact Redirect URI` url in `redirect_uri` should be exactly the same as the one of client
7 | - `Access Token Lifetime`
8 |
9 | ### create a client
10 | 
11 | #### instructions
12 | - `Name` name of client
13 | - `Description` description of client
14 | - `Icon` icon of client, optional
15 | - `Homepage` homepage of client, optional
16 | - `Redirect URI` redirect uri of client, required, multiple uri should be separated by space (not recommended)
17 | - `Scopes` optional (don't fill it if you don't know)
18 | - `Grant Types` optional (don't fill it if you don't know)
19 | - `Client ID` and `Client Secret` used for client authentication, generated automatically, don't share it with others
20 |
21 | ### set scope for your resources (user.read is default scope)
22 | 
23 | most people only need `user.read` scope, if you need more, you can add it here (maybe you need to know something about OAuth scope first)
24 | #### instructions
25 | - `Scope ID` unique identifier of scope, used for distinguish, you can refer to `Github` scope
26 | - `Resource Path` resource path of scope
27 | - `Request Method` request method of resource path, usually `GET`
28 | - `Default` if checked, this scope will be added even if it's not in `scope` parameter
29 | - `Name` name of scope, used for display
30 | - `Description` description of scope, used for display, you can use `{user}` `{client_name}` variable to represent user and client name
31 | - `Icon` support `FontAwesome` icon and normal image
32 |
33 | ### uri
34 | #### authorize
35 | `/oauth/authorize`
36 |
37 | | param | description | required | default | example |
38 | | --- | --- | --- | --- | --- |
39 | | client_id | client id | yes | none | 123456 |
40 | | response_type | grant type | yes | none | code or token |
41 | | redirect_uri | redirect uri | yes | client redirect uri | https://example.com/oauth/callback |
42 | | scope | scope | no | none | user.read |
43 | | state | state | no | none | 123456 |
44 |
45 | example:
46 | ```
47 | GET https://example.com/oauth/authorize?client_id=123456&response_type=code&redirect_uri=https://user.example.com/oauth/callback&scope=user.read&state=123456
48 | ```
49 |
50 | #### token
51 | `/oauth/token`
52 |
53 | | param | description | required | default | example |
54 | | --- | --- | --- | --- | --- |
55 | | client_id | client id | yes | none | 123456 |
56 | | client_secret | client secret | yes | none | 123456 |
57 | | grant_type | grant type | yes | none | authorization_code or refresh_token |
58 | | code | authorization code | required when grant type is authorization_code | none | 123456 |
59 | | refresh_token | refresh token | required when grant type is refresh_token | none | 123456 |
60 | | redirect_uri | redirect uri | required when grant type is authorization_code | client redirect uri | https://example.com/oauth/callback |
61 |
62 | example:
63 | ```
64 | POST https://example.com/oauth/token
65 |
66 | Payload: client_id=123456&client_secret=123456&grant_type=authorization_code&code=123456&redirect_uri=https://example.com/oauth/callback
67 | ```
68 |
69 | ### resource
70 | `/api/user`
71 |
72 | | param | description | required | default | example |
73 | | --- |--------------|-----|---------| --- |
74 | | access_token | access token | yes | none | 123456 |
75 |
76 | ### example
77 |
78 | ```
79 | GET https://example.com/api/user?access_token=123456
80 | ```
81 |
82 | ### do it as normal OAuth client
83 | 
84 |
85 | ### get access token after authorized
86 | 
87 |
88 | ### using token to access resources (get or header)
89 | 
90 | 
91 |
--------------------------------------------------------------------------------
/js/src/forum/components/user/AuthorizedPage.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/forum/app';
2 | import UserPage from 'flarum/forum/components/UserPage';
3 | import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
4 | import Placeholder from 'flarum/common/components/Placeholder';
5 | import Button from 'flarum/common/components/Button';
6 |
7 | export default class AuthorizedPage extends UserPage {
8 | records = [];
9 | loading = true;
10 | nomore = false;
11 | page = 0;
12 | oninit(vnode) {
13 | super.oninit(vnode);
14 | this.loadUser(m.route.param('username'));
15 | this.loadRecords();
16 | }
17 | loadRecords() {
18 | app.store.find('oauth-records', { page: this.page }).then((records) => {
19 | this.records = this.records.concat(records);
20 | this.loading = false;
21 | if (records.length < 10) {
22 | this.nomore = true;
23 | }
24 | m.redraw();
25 | });
26 | }
27 | loadMore() {
28 | this.loadRecords((this.page += 1));
29 | }
30 | content() {
31 | if (this.records.length === 0) {
32 | return ;
33 | }
34 |
35 | let allow_delete = app.forum.attribute('foskym-oauth-center.allow_delete_records');
36 | console.log(allow_delete);
37 |
38 | return (
39 |
40 | {allow_delete && (
41 |
59 | )}
60 |
61 |
62 | {this.records.map((record) => (
63 | -
64 |
65 |
66 |
.client_icon})
67 |
68 |
73 |
{record.attribute('client').client_desc}
74 |
75 |
76 |
77 |
78 |
79 | {allow_delete && (
80 |
91 | )}
92 |
93 |
94 |
95 |
96 | ))}
97 |
98 |
99 | {this.loading &&
}
100 |
101 | {!this.loading && !this.nomore && (
102 |
103 |
106 |
107 | )}
108 |
109 | {this.nomore &&
}
110 |
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/less/forum/oauth.less:
--------------------------------------------------------------------------------
1 | body:has(.oauth-area) {
2 | overflow-y: auto;
3 | }
4 |
5 | .oauth-area {
6 | display: block !important;
7 | position: relative;
8 | left: 0;
9 | top: 0;
10 | padding: 110px 0;
11 | min-height: 100%;
12 | box-sizing: border-box;
13 | h3 {
14 | margin-block-start: 0;
15 | margin-block-end: 0;
16 | color: @heading-color;
17 | }
18 | }
19 |
20 | .oauth-header, .oauth-body {
21 | padding: 20px;
22 | background-color: @control-bg;
23 | }
24 |
25 | .oauth-header {
26 | backdrop-filter: blur(0);
27 | text-align: center;
28 | border-radius: @border-radius @border-radius 0 0;
29 | h2 {
30 | margin-bottom: 8px;
31 | font-weight: 600;
32 | font-size: 40px;
33 | color: @heading-color;
34 | }
35 | p {
36 | font-weight: 400;
37 | font-size: 20px;
38 | color: @control-color;
39 | }
40 | }
41 |
42 | .oauth-body {
43 | border-radius: 0 0 @border-radius @border-radius;
44 | .oauth-form-item {
45 | position: relative;
46 | margin-bottom: 15px;
47 | clear: both;
48 | *zoom: 1;
49 | &:after {
50 | content: '\20';
51 | clear: both;
52 | *zoom: 1;
53 | display: block;
54 | height: 0;
55 | }
56 | }
57 | }
58 |
59 |
60 | .oauth-icon {
61 | position: absolute;
62 | left: 4px;
63 | top: 1px;
64 | width: auto;
65 | line-height: 35px;
66 | text-align: center;
67 | color: @control-color;
68 | padding: 0 8px;
69 | font-size: 14px;
70 | }
71 |
72 | label:before {
73 | color: @control-color;
74 | }
75 |
76 | .oauth-scope-area {
77 | padding-top: 10px;
78 | padding-bottom: 10px;
79 | overflow: auto;
80 | max-height: 350px;
81 | position: relative;
82 | }
83 |
84 | .oauth-scope {
85 | margin-top: 15px;
86 | &:first-child {
87 | margin-top: 0;
88 | }
89 | }
90 |
91 | .oauth-scope, .oauth-scope-body {
92 | overflow: hidden;
93 | zoom: 1;
94 | }
95 |
96 | .oauth-scope-body, .oauth-scope-left, .oauth-scope-right {
97 | display: table-cell;
98 | vertical-align: top;
99 | }
100 |
101 | .oauth-scope-left, .oauth-scope > .pull-left {
102 | padding-right: 10px;
103 | min-width: 42px;
104 | text-align: center;
105 | }
106 |
107 | img.oauth-scope-object {
108 | display: block;
109 | vertical-align: middle;
110 | border: 0;
111 | width: 32px;
112 | height: 32px;
113 | }
114 |
115 | i.oauth-scope-object {
116 | color: @heading-color;
117 | }
118 |
119 | .oauth-scope-body {
120 | width: 10000px;
121 | padding-left: 8px;
122 | }
123 |
124 | .oauth-scope-heading {
125 | margin-top: 0;
126 | font-weight: 800;
127 | color: @heading-color;
128 | margin-block-end: 0;
129 | font-size: 12px;
130 | }
131 |
132 | .oauth-scope-body small {
133 | font-weight: 500;
134 | font-size: 12px;
135 | color: @control-color;
136 | }
137 |
138 | .oauth-btn-group {
139 | display: flex;
140 | margin-top: 15px;
141 | }
142 |
143 | .oauth-info {
144 | text-align: center;
145 | padding-bottom: 20px;
146 | position: relative;
147 | img {
148 | width: 96px;
149 | border-radius: 50%;
150 | box-shadow: 1px 0 0 0 #e8e8e8, 0 1px 0 0 #e8e8e8, 1px 1px 0 0 #e8e8e8, inset 1px 0 0 0 #e8e8e8, inset 0 1px 0 0 #e8e8e8;
151 | transition: all .3s;
152 | }
153 | img:hover {
154 | box-shadow: 0 2px 8px @shadow-color;
155 | }
156 | i {
157 | top: -36px;
158 | position: relative;
159 | padding: 0 10px;
160 | color: @control-color;
161 | }
162 | span {
163 | display: none;
164 | }
165 | }
166 |
167 | .oauth-user {
168 | width: 100%;
169 | text-align: center;
170 | display: flex;
171 | flex-wrap: wrap;
172 | justify-content: center;
173 | padding-bottom: 20px;
174 | position: relative;
175 | .oauth-username {
176 | display: grid;
177 | padding-left: 10px;
178 | padding-top: 2px;
179 | text-align: left;
180 | }
181 | }
182 |
183 |
184 | @media (min-width: 768px) {
185 | .oauth-user {
186 | display: none;
187 | }
188 | }
189 |
190 | @import "box.less";
191 | @import "card.less";
192 | @import "column.less";
193 |
194 |
195 | .oauth-box, .oauth-card {
196 | @media screen and (max-width: 768px) {
197 | .oauth-area {
198 | padding-top: 60px
199 | }
200 |
201 | .oauth-main {
202 | width: 300px
203 | }
204 |
205 | .oauth-header, .oauth-body {
206 | padding: 10px
207 | }
208 | }
209 |
210 | @media screen and (max-width: 600px) {
211 | .oauth-area {
212 | padding-top: 0
213 | }
214 |
215 | .oauth-main {
216 | width: 100%;
217 | }
218 |
219 | .oauth-main::before {
220 | box-shadow: none !important;
221 | }
222 |
223 | .oauth-body {
224 | box-shadow: 0 5px 10px -5px @shadow-color;
225 | }
226 | }
227 | }
228 |
229 | .oauth-card, .oauth-column {
230 | .oauth-scope-area {
231 | padding-top: 0;
232 | width: 30%;
233 | }
234 |
235 | .oauth-info, .oauth-form {
236 | width: 60%;
237 | }
238 |
239 | @media screen and (max-width: 768px) {
240 | .oauth-info, .oauth-form, .oauth-scope-area {
241 | width: 100%;
242 | }
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/js/src/admin/pages/IndexPage.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/admin/app';
2 | import Page from 'flarum/common/components/Page';
3 | import FieldSet from 'flarum/common/components/FieldSet';
4 | import Button from 'flarum/common/components/Button';
5 | import saveSettings from 'flarum/admin/utils/saveSettings';
6 | import Stream from 'flarum/common/utils/Stream';
7 | import Select from 'flarum/common/components/Select';
8 | import Switch from 'flarum/common/components/Switch';
9 |
10 | export default class IndexPage extends Page {
11 | oninit(vnode) {
12 | super.oninit(vnode);
13 |
14 | this.saving = false;
15 |
16 | this.fields = [
17 | 'display_mode',
18 | 'access_lifetime',
19 |
20 | 'allow_delete_records',
21 | 'allow_implicit',
22 | 'enforce_state',
23 | 'require_exact_redirect_uri',
24 | 'authorization_method_fetch',
25 | ];
26 | const settings = app.data.settings;
27 | this.values = this.fields.reduce((values, key) => {
28 | key = 'foskym-oauth-center.' + key;
29 | values[key] = Stream(settings[key] || '');
30 | return values;
31 | }, {});
32 |
33 | this.values['foskym-oauth-center.display_mode'] = this.values['foskym-oauth-center.display_mode']() || 'box';
34 |
35 | for (let i = 2; i < this.fields.length; i++) {
36 | this.values['foskym-oauth-center.' + this.fields[i]] = settings['foskym-oauth-center.' + this.fields[i]] === '1';
37 | }
38 | }
39 |
40 | view() {
41 | return (
42 |
43 |
90 |
91 |
92 |
112 |
113 |
114 | );
115 | }
116 |
117 | saveSingleSetting(setting, value) {
118 | if (this.saving) return;
119 |
120 | this.saving = true;
121 |
122 | this.values['foskym-oauth-center.' + setting] = value;
123 |
124 | saveSettings({ ['foskym-oauth-center.' + setting]: value })
125 | .then(() => app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.settings.saved_message')))
126 | .catch(() => {})
127 | .finally(() => {
128 | this.saving = false;
129 | m.redraw();
130 | });
131 | }
132 |
133 | onsubmit(e) {
134 | e.preventDefault();
135 |
136 | if (this.saving) return;
137 |
138 | this.saving = true;
139 |
140 | const settings = {};
141 |
142 | settings['foskym-oauth-center.access_lifetime'] = this.values['foskym-oauth-center.access_lifetime']();
143 |
144 | if (settings['foskym-oauth-center.access_lifetime'] === '') {
145 | settings['foskym-oauth-center.access_lifetime'] = 3600;
146 | }
147 |
148 | saveSettings(settings)
149 | .then(() => app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.settings.saved_message')))
150 | .catch(() => {})
151 | .finally(() => {
152 | this.saving = false;
153 | m.redraw();
154 | });
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/js/src/forum/components/oauth/AuthorizePage.js:
--------------------------------------------------------------------------------
1 | import app from 'flarum/forum/app';
2 | import {extend} from 'flarum/common/extend';
3 | import Page from 'flarum/common/components/Page';
4 | import IndexPage from 'flarum/forum/components/IndexPage';
5 | import LogInModal from 'flarum/forum/components/LogInModal';
6 | import extractText from 'flarum/common/utils/extractText';
7 | import Tooltip from 'flarum/common/components/Tooltip';
8 | import Button from 'flarum/common/components/Button';
9 | import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
10 | import avatar from 'flarum/common/helpers/avatar';
11 |
12 | import ScopeComponent from '../ScopeComponent';
13 |
14 | export default class AuthorizePage extends IndexPage {
15 | params = [];
16 | client = null;
17 | scopes = null;
18 | client_scope = [];
19 | loading = true;
20 | is_authorized = false;
21 | submit_loading = false;
22 | display_mode = 'box';
23 |
24 | oninit(vnode) {
25 | super.oninit(vnode);
26 | if (!app.session.user) {
27 | setTimeout(() => app.modal.show(LogInModal), 500);
28 | }
29 |
30 | this.display_mode = app.forum.attribute('foskym-oauth-center.display_mode') || 'box';
31 |
32 | const params = m.route.param();
33 |
34 | if (params.client_id == null || params.response_type == null || params.redirect_uri == null) {
35 | m.route.set('/');
36 | return;
37 | }
38 |
39 | this.params = params;
40 |
41 | Promise.all([
42 | app.store.find('oauth-clients', params.client_id),
43 | app.store.find('oauth-scopes')
44 | ]).then(([client, scopes]) => {
45 | if (!client) {
46 | m.route.set('/');
47 | return;
48 | }
49 |
50 | this.client = client;
51 | this.scopes = scopes;
52 |
53 | let uris = this.client.redirect_uri().split(' ');
54 |
55 | if (app.forum.attribute('foskym-oauth-center.require_exact_redirect_uri') && !uris.includes(params.redirect_uri)) {
56 | m.route.set('/');
57 | return;
58 | }
59 |
60 | if (!app.forum.attribute('foskym-oauth-center.allow_implicit') && params.response_type === 'token') {
61 | m.route.set('/');
62 | return;
63 | }
64 |
65 | if (app.forum.attribute('foskym-oauth-center.enforce_state') && params.state == null) {
66 | m.route.set('/');
67 | return;
68 | }
69 |
70 | let scopes_temp = params.scope ? params.scope.split(' ') : (this.client.scope() || '').split(' ');
71 |
72 | let default_scopes = this.scopes.filter(scope => scope.is_default() === 1).map(scope => scope.scope());
73 |
74 | this.client_scope = scopes_temp.filter((scope, index) => scopes_temp.indexOf(scope) === index);
75 | this.client_scope = this.client_scope.concat(default_scopes).filter(scope => scope !== '');
76 |
77 | this.loading = false;
78 | m.redraw();
79 | });
80 | }
81 |
82 | setTitle() {
83 | app.setTitle(extractText(app.translator.trans('foskym-oauth-center.forum.page.title.authorize')));
84 | app.setTitleCount(0);
85 | }
86 |
87 | view() {
88 | if (!this.client || this.loading) {
89 | return ;
90 | }
91 | return (
92 |
93 |
94 |
95 |
96 |
105 |
106 |
107 |
108 | {avatar(app.session.user, {className: 'oauth-avatar'})}
109 |
110 | {app.session.user.username()}
111 | {app.session.user.displayName()}
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
{this.client.client_name()}
124 |
125 |
126 |
{app.translator.trans('foskym-oauth-center.forum.authorize.require_these_scopes')}
127 | {
128 | this.client_scope
129 | .filter(scope => scope)
130 | .map(scope => {
131 | let scope_info = this.scopes.find(s => s.scope() === scope);
132 | return scope_info && ;
133 | })
134 | }
135 |
136 |
152 |
153 |
154 |
155 |
156 |
157 | );
158 | }
159 | deny(e) {
160 | this.is_authorized = false;
161 | }
162 | agree(e) {
163 | this.is_authorized = true;
164 | }
165 |
166 | onsubmit(e) {
167 | e.preventDefault();
168 | this.submit_loading = true;
169 | if (app.forum.attribute('foskym-oauth-center.authorization_method_fetch')) {
170 | app.request({
171 | method: 'POST',
172 | url: '/oauth/authorize/fetch',
173 | body: {
174 | ...this.params,
175 | is_authorized: this.is_authorized,
176 | }
177 | }).then((params) => {
178 | window.location.href = params.location;
179 | });
180 | } else {
181 | e.target.submit();
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/js/dist/forum.js:
--------------------------------------------------------------------------------
1 | (()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var r in o)t.o(o,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:o[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e),t.d(e,{extend:()=>h});const o=flarum.core.compat["common/extenders"];var r=t.n(o);function n(t,e){return n=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},n(t,e)}function a(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,n(t,e)}const i=flarum.core.compat["common/Model"];var s=t.n(i),c=function(t){function e(){for(var e,o=arguments.length,r=new Array(o),n=0;n-1?m("i",{class:"oauth-scope-object fa-2x "+e.scope_icon(),style:"margin-left:2px;"}):m("img",{class:"oauth-scope-object",src:e.scope_icon(),style:"width:32px"})),m("div",{class:"oauth-scope-body"},m("h6",{class:"oauth-scope-heading"},e.scope_name()),m("small",null,e.scope_desc().replace("{client_name}",o.client_name()).replace("{user}",f().session.user.attribute("displayName")))))},e}(t.n(q)()),I=function(t){function e(){for(var e,o=arguments.length,r=new Array(o),n=0;n{var t={n:e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return t.d(n,{a:n}),n},d:(e,n)=>{for(var o in n)t.o(n,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:n[o]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e),t.d(e,{extend:()=>h});const n=flarum.core.compat["common/extenders"];var o=t.n(n);function s(t,e){return s=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},s(t,e)}function i(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,s(t,e)}const a=flarum.core.compat["common/Model"];var r=t.n(a),c=function(t){function e(){for(var e,n=arguments.length,o=new Array(n),s=0;sfirst();
46 |
47 | return $client && $client['client_secret'] == $client_secret;
48 | }
49 |
50 | /**
51 | * @param string $client_id
52 | * @return bool
53 | */
54 | public function isPublicClient($client_id)
55 | {
56 | $client = Models\Client::where('client_id', $client_id)->first();
57 |
58 | if (!$client) {
59 | return false;
60 | }
61 |
62 | return empty($client['client_secret']);
63 | }
64 |
65 | /**
66 | * @param string $client_id
67 | * @return array|mixed
68 | */
69 | public function getClientDetails($client_id)
70 | {
71 | $client = Models\Client::where('client_id', $client_id)->first();
72 |
73 | return $client;
74 | }
75 |
76 | /**
77 | * @param string $client_id
78 | * @param null|string $client_secret
79 | * @param null|string $redirect_uri
80 | * @param null|array $grant_types
81 | * @param null|string $scope
82 | * @param null|string $user_id
83 | * @return bool
84 | */
85 | public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
86 | {
87 | // if it exists, update it.
88 | if ($this->getClientDetails($client_id)) {
89 | return Models\Client::where('client_id', $client_id)->update([
90 | 'client_secret' => $client_secret,
91 | 'redirect_uri' => $redirect_uri,
92 | 'grant_types' => $grant_types,
93 | 'scope' => $scope,
94 | 'user_id' => $user_id,
95 | ]);
96 | } else {
97 | return Models\Client::create([
98 | 'client_id' => $client_id,
99 | 'client_secret' => $client_secret,
100 | 'redirect_uri' => $redirect_uri,
101 | 'grant_types' => $grant_types,
102 | 'scope' => $scope,
103 | 'user_id' => $user_id,
104 | ]);
105 | }
106 | }
107 |
108 | /**
109 | * @param $client_id
110 | * @param $grant_type
111 | * @return bool
112 | */
113 | public function checkRestrictedGrantType($client_id, $grant_type)
114 | {
115 | $details = $this->getClientDetails($client_id);
116 | if (isset($details['grant_types'])) {
117 | $grant_types = explode(' ', $details['grant_types']);
118 |
119 | return in_array($grant_type, (array) $grant_types);
120 | }
121 |
122 | // if grant_types are not defined, then none are restricted
123 | return true;
124 | }
125 |
126 | /* OAuth2\Storage\AccessTokenInterface */
127 |
128 | /**
129 | * @param string $access_token
130 | * @return array|bool|mixed|null
131 | */
132 | public function getAccessToken($access_token)
133 | {
134 | if ($token = Models\AccessToken::where('access_token', $access_token)->first()) {
135 | $token['expires'] = strtotime($token['expires']);
136 | return $token;
137 | }
138 | return false;
139 | }
140 |
141 | /**
142 | * @param string $access_token
143 | * @param mixed $client_id
144 | * @param mixed $user_id
145 | * @param int $expires
146 | * @param string $scope
147 | * @return bool
148 | */
149 | public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
150 | {
151 | $expires = date('Y-m-d H:i:s', $expires);
152 |
153 | if ($this->getAccessToken($access_token)) {
154 | return Models\AccessToken::where('access_token', $access_token)->update([
155 | 'client_id' => $client_id,
156 | 'user_id' => $user_id,
157 | 'expires' => $expires,
158 | 'scope' => $scope,
159 | ]);
160 | } else {
161 | return Models\AccessToken::create([
162 | 'access_token' => $access_token,
163 | 'client_id' => $client_id,
164 | 'user_id' => $user_id,
165 | 'expires' => $expires,
166 | 'scope' => $scope,
167 | ]);
168 | }
169 | }
170 |
171 | /**
172 | * @param $access_token
173 | * @return bool
174 | */
175 | public function unsetAccessToken($access_token)
176 | {
177 | return Models\AccessToken::where('access_token', $access_token)->delete();
178 | }
179 |
180 | /* OAuth2\Storage\AuthorizationCodeInterface */
181 | /**
182 | * @param string $code
183 | * @return mixed
184 | */
185 | public function getAuthorizationCode($code)
186 | {
187 | if ($code = Models\AuthorizationCode::where('authorization_code', $code)->first()) {
188 | // convert date string back to timestamp
189 | $code['expires'] = strtotime($code['expires']);
190 | }
191 |
192 | return $code;
193 | }
194 |
195 | /**
196 | * @param string $code
197 | * @param mixed $client_id
198 | * @param mixed $user_id
199 | * @param string $redirect_uri
200 | * @param int $expires
201 | * @param string $scope
202 | * @param string $id_token
203 | * @return bool|mixed
204 | */
205 | public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
206 | {
207 | /*if (func_num_args() > 6) {
208 | // we are calling with an id token
209 | return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args());
210 | }*/
211 |
212 | // convert expires to datestring
213 | $expires = date('Y-m-d H:i:s', $expires);
214 |
215 | // if it exists, update it.
216 | if ($this->getAuthorizationCode($code)) {
217 | return Models\AuthorizationCode::where('authorization_code', $code)->update([
218 | 'client_id' => $client_id,
219 | 'user_id' => $user_id,
220 | 'redirect_uri' => $redirect_uri,
221 | 'expires' => $expires,
222 | 'scope' => $scope
223 | ]);
224 |
225 | } else {
226 | return Models\AuthorizationCode::create([
227 | 'authorization_code' => $code,
228 | 'client_id' => $client_id,
229 | 'user_id' => $user_id,
230 | 'redirect_uri' => $redirect_uri,
231 | 'expires' => $expires,
232 | 'scope' => $scope
233 | ]);
234 | }
235 |
236 | }
237 |
238 | /**
239 | * @param string $code
240 | * @param mixed $client_id
241 | * @param mixed $user_id
242 | * @param string $redirect_uri
243 | * @param string $expires
244 | * @param string $scope
245 | * @param string $id_token
246 | * @return bool
247 | */
248 | private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
249 | {
250 | // convert expires to datestring
251 | $expires = date('Y-m-d H:i:s', $expires);
252 |
253 | // if it exists, update it.
254 | if ($this->getAuthorizationCode($code)) {
255 | return Models\AuthorizationCode::where('authorization_code', $code)->update([
256 | 'client_id' => $client_id,
257 | 'user_id' => $user_id,
258 | 'redirect_uri' => $redirect_uri,
259 | 'expires' => $expires,
260 | 'scope' => $scope,
261 | 'id_token' => $id_token,
262 | 'code_challenge' => $code_challenge,
263 | 'code_challenge_method' => $code_challenge_method
264 | ]);
265 | } else {
266 | return Models\AuthorizationCode::create([
267 | 'authorization_code' => $code,
268 | 'client_id' => $client_id,
269 | 'user_id' => $user_id,
270 | 'redirect_uri' => $redirect_uri,
271 | 'expires' => $expires,
272 | 'scope' => $scope,
273 | 'id_token' => $id_token,
274 | 'code_challenge' => $code_challenge,
275 | 'code_challenge_method' => $code_challenge_method
276 | ]);
277 | }
278 | }
279 |
280 | /**
281 | * @param string $code
282 | * @return bool
283 | */
284 | public function expireAuthorizationCode($code)
285 | {
286 | return Models\AuthorizationCode::where('authorization_code', $code)->delete();
287 | }
288 |
289 | /* OAuth2\Storage\RefreshTokenInterface */
290 |
291 | /**
292 | * @param string $refresh_token
293 | * @return bool|mixed
294 | */
295 | public function getRefreshToken($refresh_token)
296 | {
297 | $token = Models\RefreshToken::where('refresh_token', $refresh_token)->first();
298 |
299 | if ($token) {
300 | // convert expires to epoch time
301 | $token['expires'] = strtotime($token['expires']);
302 | }
303 |
304 | return $token;
305 | }
306 |
307 | /**
308 | * @param string $refresh_token
309 | * @param mixed $client_id
310 | * @param mixed $user_id
311 | * @param string $expires
312 | * @param string $scope
313 | * @return bool
314 | */
315 | public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
316 | {
317 | // convert expires to datestring
318 | $expires = date('Y-m-d H:i:s', $expires);
319 |
320 | return Models\RefreshToken::create([
321 | 'refresh_token' => $refresh_token,
322 | 'client_id' => $client_id,
323 | 'user_id' => $user_id,
324 | 'expires' => $expires,
325 | 'scope' => $scope
326 | ]);
327 | }
328 |
329 | /**
330 | * @param string $refresh_token
331 | * @return bool
332 | */
333 | public function unsetRefreshToken($refresh_token)
334 | {
335 | return Models\RefreshToken::where('refresh_token', $refresh_token)->delete();
336 | }
337 |
338 | /* OAuth2\Storage\UserCredentialsInterface */
339 | /**
340 | * @param string $username
341 | * @param string $password
342 | * @return bool
343 | */
344 | public function checkUserCredentials($username, $password)
345 | {
346 | if ($user = User::where('username', $username)->first()) {
347 | return $user->checkPassword($password);
348 | }
349 |
350 | return false;
351 | }
352 |
353 | /**
354 | * @param string $username
355 | * @return array|bool
356 | */
357 | public function getUserDetails($username)
358 | {
359 | $user = User::where('username', $username)->first();
360 | $user->user_id = $user->id;
361 |
362 | return $user;
363 | }
364 |
365 | /**
366 | * @param mixed $user_id
367 | * @param string $claims
368 | * @return array|bool
369 | */
370 | public function getUserClaims($user_id, $claims)
371 | {
372 | if (!$userDetails = $this->getUserDetails($user_id)) {
373 | return false;
374 | }
375 |
376 | $claims = explode(' ', trim($claims));
377 | $userClaims = array();
378 |
379 | // for each requested claim, if the user has the claim, set it in the response
380 | $validClaims = explode(' ', self::VALID_CLAIMS);
381 | foreach ($validClaims as $validClaim) {
382 | if (in_array($validClaim, $claims)) {
383 | if ($validClaim == 'address') {
384 | // address is an object with subfields
385 | $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
386 | } else {
387 | $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
388 | }
389 | }
390 | }
391 |
392 | return $userClaims;
393 | }
394 |
395 | /**
396 | * @param string $claim
397 | * @param array $userDetails
398 | * @return array
399 | */
400 | protected function getUserClaim($claim, $userDetails)
401 | {
402 | $userClaims = array();
403 | $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
404 | $claimValues = explode(' ', $claimValuesString);
405 |
406 | foreach ($claimValues as $value) {
407 | $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
408 | }
409 |
410 | return $userClaims;
411 | }
412 |
413 | /* OAuth2\Storage\ScopeInterface */
414 |
415 | /**
416 | * @param string $scope
417 | * @return bool
418 | */
419 | public function scopeExists($scope)
420 | {
421 | $scope = explode(' ', $scope);
422 |
423 | if ($count = Models\Scope::whereIn('scope', $scope)->count()) {
424 | return $count == count($scope);
425 | }
426 |
427 | return false;
428 | }
429 |
430 | /**
431 | * @param mixed $client_id
432 | * @return null|string
433 | */
434 | public function getDefaultScope($client_id = null)
435 | {
436 | if ($result = Models\Scope::where('is_default', true)->get()) {
437 | $defaultScope = array_map(function ($row) {
438 | return $row['scope'];
439 | }, $result->toArray());
440 |
441 | return implode(' ', $defaultScope);
442 | }
443 |
444 | return null;
445 | }
446 |
447 | /* OAuth2\Storage\JwtBearerInterface */
448 |
449 | /**
450 | * @param mixed $client_id
451 | * @param $subject
452 | * @return string
453 | */
454 | public function getClientKey($client_id, $subject)
455 | {
456 | $jwt = Models\Jwt::where('client_id', $client_id)->where('subject', $subject)->first();
457 |
458 | return $jwt->public_key;
459 | }
460 |
461 | /**
462 | * @param mixed $client_id
463 | * @return bool|null
464 | */
465 | public function getClientScope($client_id)
466 | {
467 | if (!$clientDetails = $this->getClientDetails($client_id)) {
468 | return false;
469 | }
470 |
471 | if (isset($clientDetails['scope'])) {
472 | return $clientDetails['scope'];
473 | }
474 |
475 | return null;
476 | }
477 |
478 | /**
479 | * @param mixed $client_id
480 | * @param $subject
481 | * @param $audience
482 | * @param $expires
483 | * @param $jti
484 | * @return array|null
485 | */
486 | public function getJti($client_id, $subject, $audience, $expires, $jti)
487 | {
488 | /*$jti = Models\Jti::where('issuer', $client_id)
489 | ->where('subject', $subject)
490 | ->where('audience', $audience)
491 | ->where('expires', $expires)
492 | ->where('jti', $jti)
493 | ->first();
494 |
495 | if ($jti) {
496 | return array(
497 | 'issuer' => $jti['issuer'],
498 | 'subject' => $jti['subject'],
499 | 'audience' => $jti['audience'],
500 | 'expires' => $jti['expires'],
501 | 'jti' => $jti['jti'],
502 | );
503 | }*/
504 |
505 | return null;
506 | }
507 |
508 | /**
509 | * @param mixed $client_id
510 | * @param $subject
511 | * @param $audience
512 | * @param $expires
513 | * @param $jti
514 | * @return bool
515 | */
516 | public function setJti($client_id, $subject, $audience, $expires, $jti)
517 | {
518 | /*return Models\Jti::create([
519 | 'issuer' => $client_id,
520 | 'subject' => $subject,
521 | 'audience' => $audience,
522 | 'expires' => $expires,
523 | 'jti' => $jti,
524 | ]);*/
525 | }
526 |
527 | /* OAuth2\Storage\PublicKeyInterface */
528 |
529 | /**
530 | * @param mixed $client_id
531 | * @return mixed
532 | */
533 | public function getPublicKey($client_id = null)
534 | {
535 | /*$result = Models\PublicKey::where('client_id', $client_id)->first();
536 |
537 | if ($result) {
538 | return $result['public_key'];
539 | }*/
540 | }
541 |
542 | /**
543 | * @param mixed $client_id
544 | * @return mixed
545 | */
546 | public function getPrivateKey($client_id = null)
547 | {
548 | /*$result = Models\PublicKey::where('client_id', $client_id)->first();
549 |
550 | if ($result) {
551 | return $result['private_key'];
552 | }*/
553 | }
554 |
555 | /**
556 | * @param mixed $client_id
557 | * @return string
558 | */
559 | public function getEncryptionAlgorithm($client_id = null)
560 | {
561 | /*$result = Models\PublicKey::where('client_id', $client_id)->first();
562 |
563 | if ($result) {
564 | return $result['encryption_algorithm'];
565 | }*/
566 |
567 | return 'RS256';
568 | }
569 |
570 | }
571 |
--------------------------------------------------------------------------------
/js/dist/forum.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"forum.js","mappings":"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","sources":["webpack://@foskym/flarum-oauth-center/webpack/bootstrap","webpack://@foskym/flarum-oauth-center/webpack/runtime/compat get default export","webpack://@foskym/flarum-oauth-center/webpack/runtime/define property getters","webpack://@foskym/flarum-oauth-center/webpack/runtime/hasOwnProperty shorthand","webpack://@foskym/flarum-oauth-center/webpack/runtime/make namespace object","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/extenders']\"","webpack://@foskym/flarum-oauth-center/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js","webpack://@foskym/flarum-oauth-center/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/Model']\"","webpack://@foskym/flarum-oauth-center/./src/common/models/Client.js","webpack://@foskym/flarum-oauth-center/./src/common/models/Scope.js","webpack://@foskym/flarum-oauth-center/./src/common/models/Record.js","webpack://@foskym/flarum-oauth-center/./src/common/models/Token.js","webpack://@foskym/flarum-oauth-center/./src/common/extend.js","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['forum/app']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/extend']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['forum/components/UserPage']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/LinkButton']\"","webpack://@foskym/flarum-oauth-center/./node_modules/@babel/runtime/helpers/esm/extends.js","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/Page']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['forum/components/IndexPage']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['forum/components/LogInModal']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/utils/extractText']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/Tooltip']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/Button']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/LoadingIndicator']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/helpers/avatar']\"","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/Component']\"","webpack://@foskym/flarum-oauth-center/./src/forum/components/ScopeComponent.js","webpack://@foskym/flarum-oauth-center/./src/forum/components/oauth/AuthorizePage.js","webpack://@foskym/flarum-oauth-center/external root \"flarum.core.compat['common/components/Placeholder']\"","webpack://@foskym/flarum-oauth-center/./src/forum/components/user/AuthorizedPage.js","webpack://@foskym/flarum-oauth-center/./src/forum/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extenders'];","export default function _setPrototypeOf(o, p) {\n _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n };\n return _setPrototypeOf(o, p);\n}","import setPrototypeOf from \"./setPrototypeOf.js\";\nexport default function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n setPrototypeOf(subClass, superClass);\n}","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Model'];","import Model from 'flarum/common/Model';\n\nexport default class Client extends Model {\n client_id = Model.attribute('client_id');\n client_secret = Model.attribute('client_secret');\n redirect_uri = Model.attribute('redirect_uri');\n grant_types = Model.attribute('grant_types');\n scope = Model.attribute('scope');\n user_id = Model.attribute('user_id');\n client_name = Model.attribute('client_name');\n client_icon = Model.attribute('client_icon');\n client_desc = Model.attribute('client_desc');\n client_home = Model.attribute('client_home');\n}\n","import Model from 'flarum/common/Model';\n\nexport default class Scope extends Model {\n scope = Model.attribute('scope');\n resource_path = Model.attribute('resource_path');\n method = Model.attribute('method');\n visible_fields = Model.attribute('visible_fields');\n is_default = Model.attribute('is_default');\n scope_name = Model.attribute('scope_name');\n scope_icon = Model.attribute('scope_icon');\n scope_desc = Model.attribute('scope_desc');\n}\n","import Model from 'flarum/common/Model';\n\nexport default class Record extends Model {\n client = Model.hasOne('client');\n user_id = Model.attribute('user_id');\n authorized_at = Model.attribute('authorized_at', Model.transformDate);\n}\n","import Model from 'flarum/common/Model';\n\nexport default class Token extends Model {\n access_token = Model.attribute('access_token');\n client_id = Model.attribute('client_id');\n user_id = Model.attribute('user_id');\n expires = Model.attribute('expires', Model.transformDate);\n scope = Model.attribute('scope');\n}\n","import Extend from 'flarum/common/extenders';\nimport Client from \"./models/Client\";\nimport Scope from \"./models/Scope\";\nimport Record from \"./models/Record\";\nimport Token from \"./models/Token\";\n\nexport default [\n new Extend.Store()\n .add('oauth-clients', Client)\n .add('oauth-scopes', Scope)\n .add('oauth-records', Record)\n .add('oauth-tokens', Token)\n];\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/UserPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LinkButton'];","export default function _extends() {\n _extends = Object.assign ? Object.assign.bind() : function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n return target;\n };\n return _extends.apply(this, arguments);\n}","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Page'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/IndexPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/LogInModal'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/extractText'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Tooltip'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Button'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LoadingIndicator'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/avatar'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Component'];","import app from 'flarum/forum/app';\nimport Component from 'flarum/common/Component';\nimport Tooltip from 'flarum/common/components/Tooltip';\n\nexport default class ScopeComponent extends Component {\n view() {\n const { scope, client } = this.attrs;\n return (\n \n
\n {\n (scope.scope_icon().indexOf('fa-') > -1) ?\n
:\n
})
\n }\n
\n
\n
\n {scope.scope_name()}\n
\n \n {\n scope.scope_desc()\n .replace('{client_name}', client.client_name())\n .replace('{user}', app.session.user.attribute('displayName'))\n }\n \n \n
\n );\n }\n}\n","import app from 'flarum/forum/app';\nimport {extend} from 'flarum/common/extend';\nimport Page from 'flarum/common/components/Page';\nimport IndexPage from 'flarum/forum/components/IndexPage';\nimport LogInModal from 'flarum/forum/components/LogInModal';\nimport extractText from 'flarum/common/utils/extractText';\nimport Tooltip from 'flarum/common/components/Tooltip';\nimport Button from 'flarum/common/components/Button';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\nimport avatar from 'flarum/common/helpers/avatar';\n\nimport ScopeComponent from '../ScopeComponent';\n\nexport default class AuthorizePage extends IndexPage {\n params = [];\n client = null;\n scopes = null;\n client_scope = [];\n loading = true;\n is_authorized = false;\n submit_loading = false;\n display_mode = 'box';\n\n oninit(vnode) {\n super.oninit(vnode);\n if (!app.session.user) {\n setTimeout(() => app.modal.show(LogInModal), 500);\n }\n\n this.display_mode = app.forum.attribute('foskym-oauth-center.display_mode') || 'box';\n\n const params = m.route.param();\n\n if (params.client_id == null || params.response_type == null || params.redirect_uri == null) {\n m.route.set('/');\n return;\n }\n\n this.params = params;\n\n Promise.all([\n app.store.find('oauth-clients', params.client_id),\n app.store.find('oauth-scopes')\n ]).then(([client, scopes]) => {\n if (!client) {\n m.route.set('/');\n return;\n }\n\n this.client = client;\n this.scopes = scopes;\n\n let uris = this.client.redirect_uri().split(' ');\n\n if (app.forum.attribute('foskym-oauth-center.require_exact_redirect_uri') && !uris.includes(params.redirect_uri)) {\n m.route.set('/');\n return;\n }\n\n if (!app.forum.attribute('foskym-oauth-center.allow_implicit') && params.response_type === 'token') {\n m.route.set('/');\n return;\n }\n\n if (app.forum.attribute('foskym-oauth-center.enforce_state') && params.state == null) {\n m.route.set('/');\n return;\n }\n\n let scopes_temp = params.scope ? params.scope.split(' ') : (this.client.scope() || '').split(' ');\n\n let default_scopes = this.scopes.filter(scope => scope.is_default() === 1).map(scope => scope.scope());\n\n this.client_scope = scopes_temp.filter((scope, index) => scopes_temp.indexOf(scope) === index);\n this.client_scope = this.client_scope.concat(default_scopes).filter(scope => scope !== '');\n\n this.loading = false;\n m.redraw();\n });\n }\n\n setTitle() {\n app.setTitle(extractText(app.translator.trans('foskym-oauth-center.forum.page.title.authorize')));\n app.setTitleCount(0);\n }\n\n view() {\n if (!this.client || this.loading) {\n return ;\n }\n return (\n \n
\n
\n
\n \n
\n\n
\n {avatar(app.session.user, {className: 'oauth-avatar'})}\n
\n {app.session.user.username()}\n {app.session.user.displayName()}\n
\n
\n\n
\n
\n
\n \n
\n
\n
\n \n
{this.client.client_name()}\n
\n
\n
{app.translator.trans('foskym-oauth-center.forum.authorize.require_these_scopes')}
\n {\n this.client_scope\n .filter(scope => scope)\n .map(scope => {\n let scope_info = this.scopes.find(s => s.scope() === scope);\n return scope_info && ;\n })\n }\n \n
\n
\n
\n
\n
\n
\n );\n }\n deny(e) {\n this.is_authorized = false;\n }\n agree(e) {\n this.is_authorized = true;\n }\n\n onsubmit(e) {\n e.preventDefault();\n this.submit_loading = true;\n if (app.forum.attribute('foskym-oauth-center.authorization_method_fetch')) {\n app.request({\n method: 'POST',\n url: '/oauth/authorize/fetch',\n body: {\n ...this.params,\n is_authorized: this.is_authorized,\n }\n }).then((params) => {\n window.location.href = params.location;\n });\n } else {\n e.target.submit();\n }\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Placeholder'];","import app from 'flarum/forum/app';\nimport UserPage from 'flarum/forum/components/UserPage';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\nimport Placeholder from 'flarum/common/components/Placeholder';\nimport Button from 'flarum/common/components/Button';\n\nexport default class AuthorizedPage extends UserPage {\n records = [];\n loading = true;\n nomore = false;\n page = 0;\n oninit(vnode) {\n super.oninit(vnode);\n this.loadUser(m.route.param('username'));\n this.loadRecords();\n }\n loadRecords() {\n app.store.find('oauth-records', { page: this.page }).then((records) => {\n this.records = this.records.concat(records);\n this.loading = false;\n if (records.length < 10) {\n this.nomore = true;\n }\n m.redraw();\n });\n }\n loadMore() {\n this.loadRecords((this.page += 1));\n }\n content() {\n if (this.records.length === 0) {\n return ;\n }\n\n let allow_delete = app.forum.attribute('foskym-oauth-center.allow_delete_records');\n console.log(allow_delete);\n\n return (\n \n {allow_delete && (\n
\n )}\n\n
\n {this.records.map((record) => (\n - \n
\n
\n
.client_icon})
\n
\n
\n
{record.attribute('client').client_desc}
\n
\n
\n
\n \n\n {allow_delete && (\n \n )}\n
\n
\n
\n \n ))}\n
\n\n {this.loading &&
}\n\n {!this.loading && !this.nomore && (\n
\n \n
\n )}\n\n {this.nomore &&
}\n
\n );\n }\n}\n","import app from 'flarum/forum/app';\nimport {extend} from 'flarum/common/extend';\nimport UserPage from 'flarum/forum/components/UserPage';\nimport LinkButton from 'flarum/common/components/LinkButton';\nimport AuthorizePage from \"./components/oauth/AuthorizePage\";\nimport AuthorizedPage from \"./components/user/AuthorizedPage\";\napp.initializers.add('foskym/flarum-oauth-center', () => {\n app.routes['oauth.authorize'] = {\n path: '/oauth/authorize',\n component: AuthorizePage\n };\n\n app.routes['user.authorized'] = {\n path: '/u/:username/authorized',\n component: AuthorizedPage\n };\n extend(UserPage.prototype, 'navItems', function (items) {\n if (app.session.user && app.session.user.id() === this.user.id()) {\n items.add(\n 'authorized',\n LinkButton.component(\n {\n href: app.route('user.authorized', { username: this.user.username() }),\n icon: 'fas fa-user-friends',\n },\n [\n app.translator.trans('foskym-oauth-center.forum.page.label.authorized'),\n ]\n ),\n -110\n );\n }\n });\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","_setPrototypeOf","p","setPrototypeOf","bind","__proto__","_inheritsLoose","subClass","superClass","create","constructor","Client","_Model","_this","_len","arguments","length","args","Array","_key","apply","concat","client_id","Model","client_secret","redirect_uri","grant_types","scope","user_id","client_name","client_icon","client_desc","client_home","Scope","resource_path","method","visible_fields","is_default","scope_name","scope_icon","scope_desc","Record","client","authorized_at","Token","access_token","expires","Extend","add","_extends","assign","target","i","source","this","ScopeComponent","_Component","view","_this$attrs","attrs","m","class","indexOf","style","src","replace","app","user","attribute","Component","AuthorizePage","_IndexPage","params","scopes","client_scope","loading","is_authorized","submit_loading","display_mode","_proto","oninit","vnode","_this2","setTimeout","show","LogInModal","route","param","response_type","Promise","all","find","then","_ref","uris","split","includes","state","set","scopes_temp","default_scopes","filter","map","index","redraw","setTitle","extractText","trans","_this3","LoadingIndicator","className","Tooltip","text","position","href","avatar","username","displayName","alt","scope_info","s","id","action","onsubmit","keys","type","name","Button","onclick","deny","agree","e","preventDefault","url","body","window","location","submit","IndexPage","AuthorizedPage","_UserPage","records","nomore","page","loadUser","loadRecords","loadMore","content","Placeholder","allow_delete","console","log","confirm","record","toLocaleString","r","display","disabled","UserPage","path","component","extend","items","LinkButton","icon"],"sourceRoot":""}
--------------------------------------------------------------------------------