diff --git a/extend.php b/extend.php
index c8f1edd..b4680d5 100644
--- a/extend.php
+++ b/extend.php
@@ -12,7 +12,10 @@
namespace FoskyM\OAuthCenter;
use Flarum\Extend;
+use Flarum\Http\Middleware\AuthenticateWithHeader;
+use Flarum\Http\Middleware\CheckCsrfToken;
use FoskyM\OAuthCenter\Middlewares\ResourceScopeMiddleware;
+use FoskyM\OAuthCenter\Middlewares\UnsetCsrfMiddleware;
return [
(new Extend\Frontend('forum'))
@@ -26,9 +29,30 @@ return [
new Extend\Locales(__DIR__.'/locale'),
(new Extend\Routes('forum'))
- ->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class),
- (new Extend\Routes('api'))
- ->get('/oauth/clients', 'oauth.clients.list', Api\Controller\ListClientController::class),
+ ->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class)
+ ->post('/oauth/token', 'oauth.token', Controllers\TokenController::class),
- (new Extend\Middleware('api'))->add(ResourceScopeMiddleware::class),
+ (new Extend\Routes('api'))
+ ->get('/oauth-clients', 'oauth.clients.list', Api\Controller\ListClientController::class)
+ ->post('/oauth-clients', 'oauth.clients.create', Api\Controller\CreateClientController::class)
+ ->get('/oauth-clients/{client_id}', 'oauth.clients.show', Api\Controller\ShowClientController::class)
+ ->patch('/oauth-clients/{id}', 'oauth.clients.update', Api\Controller\UpdateClientController::class)
+ ->delete('/oauth-clients/{id}', 'oauth.clients.delete', Api\Controller\DeleteClientController::class)
+
+ ->get('/oauth-scopes', 'oauth.scopes.list', Api\Controller\ListScopeController::class)
+ ->post('/oauth-scopes', 'oauth.scopes.create', Api\Controller\CreateScopeController::class)
+ ->patch('/oauth-scopes/{id}', 'oauth.scopes.update', Api\Controller\UpdateScopeController::class)
+ ->delete('/oauth-scopes/{id}', 'oauth.scopes.delete', Api\Controller\DeleteScopeController::class)
+
+ ->get('/user', 'user.show', Controllers\ApiUserController::class),
+
+ (new Extend\Settings)
+ ->serializeToForum('foskym-oauth-center.allow_implicit', 'foskym-oauth-center.allow_implicit', 'boolval')
+ ->serializeToForum('foskym-oauth-center.enforce_state', 'foskym-oauth-center.enforce_state', 'boolval')
+ ->serializeToForum('foskym-oauth-center.require_exact_redirect_uri', 'foskym-oauth-center.require_exact_redirect_uri', 'boolval'),
+
+ (new Extend\Middleware('api'))
+ ->insertAfter(AuthenticateWithHeader::class, ResourceScopeMiddleware::class),
+ (new Extend\Middleware('forum'))
+ ->insertBefore(CheckCsrfToken::class, UnsetCsrfMiddleware::class),
];
diff --git a/js/dist/admin.js b/js/dist/admin.js
index 47a216f..d45ab95 100644
Binary files a/js/dist/admin.js and b/js/dist/admin.js differ
diff --git a/js/dist/admin.js.map b/js/dist/admin.js.map
index 5f1a3e8..21de8ca 100644
Binary files a/js/dist/admin.js.map and b/js/dist/admin.js.map differ
diff --git a/js/dist/forum.js b/js/dist/forum.js
index 3003770..4c8eaf1 100644
Binary files a/js/dist/forum.js and b/js/dist/forum.js differ
diff --git a/js/dist/forum.js.map b/js/dist/forum.js.map
index 8eac3b2..c314cbb 100644
Binary files a/js/dist/forum.js.map and b/js/dist/forum.js.map differ
diff --git a/js/src/admin/pages/ClientsPage.js b/js/src/admin/pages/ClientsPage.js
index fd464c3..b05e0af 100644
--- a/js/src/admin/pages/ClientsPage.js
+++ b/js/src/admin/pages/ClientsPage.js
@@ -1,22 +1,100 @@
import app from 'flarum/admin/app';
import Page from 'flarum/common/components/Page';
-
+import Button from 'flarum/common/components/Button';
export default class ClientsPage extends Page {
- settingName = 'collapsible-posts.reasons';
translationPrefix = 'foskym-oauth-center.admin.clients.';
+ clients = [];
oninit(vnode) {
super.oninit(vnode);
- app.store.find('oauth/clients').then(() => {
+ this.fields = [
+ 'client_id',
+ 'client_secret',
+ 'redirect_uri',
+ 'grant_types',
+ 'scope',
+ 'client_name',
+ 'client_desc',
+ 'client_icon',
+ 'client_home'
+ ];
+
+ app.store.find('oauth-clients').then(r => {
+ this.clients = r;
+ this.fields.map(key => console.log(this.clients[0][key]))
m.redraw();
});
}
view() {
return (
-
-
Clients Page
+
+ {
+ m('.Form-group', [
+ m('table', [
+ m('thead', m('tr', [
+ this.fields.map(key => m('th', app.translator.trans(this.translationPrefix + key))),
+ m('th'),
+ ])),
+ m('tbody', [
+ this.clients.map((client, index) => m('tr', [
+ this.fields.map(key =>
+ m('td', m('input.FormControl', {
+ type: 'text',
+ value: client[key]() || '',
+ onchange: (event) => {
+ this.saveClientInfo(index, key, event.target.value);
+ },
+ }))
+ ),
+ m('td', Button.component({
+ className: 'Button Button--icon',
+ icon: 'fas fa-times',
+ onclick: () => {
+ this.clients[index].delete();
+ this.clients.splice(index, 1);
+
+ },
+ })),
+ ])),
+ m('tr', m('td', {
+ colspan: 9,
+ }, Button.component({
+ className: 'Button Button--block',
+ onclick: () => {
+ const client = app.store.createRecord('oauth-clients');
+ const client_id = this.randomString(32);
+ const client_secret = this.randomString(32);
+ client.save({
+ client_id: client_id,
+ client_secret: client_secret,
+ }).then(this.clients.push(client));
+ },
+ }, app.translator.trans(this.translationPrefix + 'add_button')))),
+ ]),
+ ]),
+ ])
+ }
);
}
+
+ randomString(len) {
+ len = len || 32;
+ let $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let maxPos = $chars.length;
+ let pwd = '';
+ for (let i = 0; i < len; i++) {
+ //0~32的整数
+ pwd += $chars.charAt(Math.floor(Math.random() * (maxPos + 1)));
+ }
+ return pwd;
+ }
+
+ saveClientInfo(index, key, value) {
+ console.log(index, key, value);
+ this.clients[index].save({
+ [key]: value,
+ });
+ }
}
diff --git a/js/src/admin/pages/ScopesPage.js b/js/src/admin/pages/ScopesPage.js
index e830f7d..2fb36d0 100644
--- a/js/src/admin/pages/ScopesPage.js
+++ b/js/src/admin/pages/ScopesPage.js
@@ -1,11 +1,118 @@
+import app from 'flarum/admin/app';
import Page from 'flarum/common/components/Page';
+import Button from 'flarum/common/components/Button';
+import Select from 'flarum/common/components/Select';
+import Checkbox from 'flarum/common/components/Checkbox';
export default class ScopesPage extends Page {
- view() {
- return (
-
-
Scopes Page
-
- );
+ translationPrefix = 'foskym-oauth-center.admin.scopes.';
+ scopes = [];
+
+ oninit(vnode) {
+ super.oninit(vnode);
+
+ this.fields = [
+ 'scope',
+ 'resource_path',
+ 'method',
+ 'is_default',
+ 'scope_name',
+ 'scope_icon',
+ 'scope_desc'
+ ];
+
+ app.store.find('oauth-scopes').then(r => {
+ this.scopes = r;
+ this.fields.map(key => console.log(this.scopes[0][key]))
+ m.redraw();
+ });
+ }
+
+ view() {
+ return (
+
+ {
+ m('.Form-group', [
+ m('table', [
+ m('thead', m('tr', [
+ this.fields.map(key => m('th', app.translator.trans(this.translationPrefix + key))),
+ m('th'),
+ ])),
+ m('tbody', [
+ this.scopes.map((scope, index) => m('tr', [
+ this.fields.map(key =>
+ m('td', key === 'method' ? Select.component({
+ options: {
+ 'GET': 'GET',
+ 'POST': 'POST',
+ 'PUT': 'PUT',
+ 'DELETE': 'DELETE',
+ 'PATCH': 'PATCH',
+ },
+ value: scope[key]() || 'GET',
+ disabled: scope.resource_path() === '/api/user' && key === 'method',
+ onchange: (value) => {
+ this.saveScopeInfo(index, key, value);
+ },
+ }) : key === 'is_default' ? Checkbox.component({
+ state: scope[key]() === 1 || false,
+ disabled: scope.resource_path() === '/api/user' && key === 'is_default',
+ onchange: (checked) => {
+ this.scopes[index].is_default((this.scopes[index].is_default() + 1) % 2)
+ this.saveScopeInfo(index, key, checked ? 1 : 0);
+ },
+ }) : m('input.FormControl', {
+ type: 'text',
+ value: scope[key]() || '',
+ disabled: scope.resource_path() === '/api/user' && key === 'resource_path',
+ onchange: (event) => {
+ this.saveScopeInfo(index, key, event.target.value);
+ },
+ }))
+ ),
+ (scope.resource_path() !== '/api/user' && m('td', Button.component({
+ className: 'Button Button--icon',
+ icon: 'fas fa-times',
+ onclick: () => {
+ this.scopes[index].delete();
+ this.scopes.splice(index, 1);
+ },
+ }))),
+ ])),
+ m('tr', m('td', {
+ colspan: 7,
+ }, Button.component({
+ className: 'Button Button--block',
+ onclick: () => {
+ const scope = app.store.createRecord('oauth-scopes');
+ scope.save({
+ 'scope': 'Scope.' + this.randomString(8),
+ 'resource_path': '/api/' + this.randomString(4),
+ 'method': 'GET',
+ }).then(this.scopes.push(scope));
+ },
+ }, app.translator.trans(this.translationPrefix + 'add_button')))),
+ ]),
+ ]),
+ ])
+ }
+
+ );
+ }
+ randomString(len) {
+ len = len || 8;
+ let $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let maxPos = $chars.length;
+ let str = '';
+ for (let i = 0; i < len; i++) {
+ //0~32的整数
+ str += $chars.charAt(Math.floor(Math.random() * (maxPos + 1)));
}
+ return str;
+ }
+ saveScopeInfo(index, key, value) {
+ this.scopes[index].save({
+ [key]: value,
+ });
+ }
}
diff --git a/js/src/common/extend.js b/js/src/common/extend.js
index 54f41cd..8e49d35 100644
--- a/js/src/common/extend.js
+++ b/js/src/common/extend.js
@@ -1,7 +1,9 @@
import Extend from 'flarum/common/extenders';
import Client from "./models/Client";
+import Scope from "./models/Scope";
export default [
new Extend.Store()
- .add('oauth-clients', Client),
+ .add('oauth-clients', Client)
+ .add('oauth-scopes', Scope),
];
diff --git a/js/src/common/models/Scope.js b/js/src/common/models/Scope.js
new file mode 100644
index 0000000..2b89440
--- /dev/null
+++ b/js/src/common/models/Scope.js
@@ -0,0 +1,11 @@
+import Model from 'flarum/common/Model';
+
+export default class Scope extends Model {
+ scope = Model.attribute('scope');
+ resource_path = Model.attribute('resource_path');
+ method = Model.attribute('method');
+ is_default = Model.attribute('is_default');
+ scope_name = Model.attribute('scope_name');
+ scope_icon = Model.attribute('scope_icon');
+ scope_desc = Model.attribute('scope_desc');
+}
diff --git a/js/src/forum/components/oauth/AuthorizePage.js b/js/src/forum/components/oauth/AuthorizePage.js
index e853bd6..6d31f91 100644
--- a/js/src/forum/components/oauth/AuthorizePage.js
+++ b/js/src/forum/components/oauth/AuthorizePage.js
@@ -4,8 +4,18 @@ import Page from 'flarum/common/components/Page';
import IndexPage from 'flarum/forum/components/IndexPage';
import LogInModal from 'flarum/forum/components/LogInModal';
import extractText from 'flarum/common/utils/extractText';
+import Tooltip from 'flarum/common/components/Tooltip';
+import Button from 'flarum/common/components/Button';
export default class AuthorizePage extends IndexPage {
+ params = [];
+ client = null;
+ scopes = null;
+ client_scope = [];
+ loading = true;
+ is_authorized = false;
+ submit_loading = false;
+
oninit(vnode) {
super.oninit(vnode);
if (!app.session.user) {
@@ -13,18 +23,205 @@ export default class AuthorizePage extends IndexPage {
}
const params = m.route.param();
+
+ if (params.client_id == null || params.response_type == null || params.redirect_uri == null) {
+ m.route.set('/');
+ } else {
+ this.params = params;
+ app.store.find('oauth-clients', params.client_id).then(client => {
+ if (client.length === 0) {
+ m.route.set('/');
+ } else {
+ this.client = client[0];
+ let uris = null;
+ if (this.client.redirect_uri().indexOf(' ') > -1) {
+ uris = this.client.redirect_uri().split(' ');
+ } else {
+ uris = [this.client.redirect_uri()];
+ }
+
+ if (app.forum.attribute('foskym-oauth-center.require_exact_redirect_uri') && uris.indexOf(params.redirect_uri) == -1) {
+ m.route.set('/');
+ }
+ if (app.forum.attribute('foskym-oauth-center.allow_implicit') && params.response_type == 'token') {
+ m.route.set('/');
+ }
+ if (app.forum.attribute('foskym-oauth-center.enforce_state') && params.enforce_state == null) {
+ m.route.set('/');
+ }
+
+ app.store.find('oauth-scopes').then((scopes) => {
+ this.scopes = scopes
+ let scope = params.scope;
+
+ if (params.scope == null) {
+ scope = this.client.scope();
+ }
+
+ let scopes_temp = [];
+ if (scope == null) {
+ scopes_temp = [];
+ } else if (scope.indexOf(' ') > -1) {
+ scopes_temp = scope.split(' ');
+ } else {
+ scopes_temp = [scope];
+ }
+
+ let default_scopes = [];
+ this.scopes.map(scope => {
+ let index = scopes_temp.indexOf(scope.scope());
+ if (index > -1) {
+ scopes_temp[index] = scope;
+ } else {
+ scopes_temp.slice(index, 1);
+ }
+ if (scope.is_default() === 1) {
+ default_scopes.push(scope);
+ }
+ });
+
+ scopes_temp = scopes_temp.concat(default_scopes);
+
+ this.client_scope = scopes_temp.filter((scope, index) => scopes_temp.indexOf(scope) === index);
+ console.log(this.client_scope);
+ this.loading = false;
+ m.redraw();
+ });
+
+
+ }
+ });
+ }
}
setTitle() {
app.setTitle(extractText(app.translator.trans('foskym-oauth-center.forum.page.title.authorize')));
app.setTitleCount(0);
}
+
view() {
+ if (!this.client) {
+ return '';
+ }
return (
-
+ !this.loading &&
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ this.client_scope.length > 0 && this.client_scope.map(scope => {
+ let scope_info = null;
+ this.scopes.map(s => {
+ if (s.scope() === scope.scope()) {
+ scope_info = s;
+ }
+ });
+ if (scope_info == null) {
+ return '';
+ }
+ return (
+
+
+ {
+ (scope_info.scope_icon().indexOf('fa-') > -1) ?
+
:
+
+ }
+
+
+
+ {scope_info.scope_name()}
+
+
+ {
+ scope_info.scope_desc()
+ .replace('{client_name}', this.client.client_name())
+ .replace('{user}', app.session.user.attribute('displayName'))
+ }
+
+
+
+ );
+ })
+ }
+
+
+
+
+
+
+
);
}
+ deny(e) {
+ this.is_authorized = false;
+ }
+ agree(e) {
+ this.is_authorized = true;
+ }
+
+ onsubmit(e) {
+ e.preventDefault();
+ this.submit_loading = true;
+ app.request({
+ method: 'POST',
+ url: '/oauth/authorize',
+ body: {
+ response_type: this.params.response_type,
+ client_id: this.params.client_id,
+ redirect_uri: this.params.redirect_uri,
+ state: this.params.state,
+ scope: this.params.scope,
+ is_authorized: this.is_authorized,
+ }
+ }).then((params) => {
+ let arr = []
+ for (let k in params) {
+ arr.push(`${k}=${params[k]}`)
+ }
+ let url = `${this.params.redirect_uri }?${arr.join('&')}`;
+ window.location.href = url;
+ });
+
+ // Some form handling logic here
+ }
}
diff --git a/js/src/forum/index.js b/js/src/forum/index.js
index 3ba3f21..c6ee2f4 100644
--- a/js/src/forum/index.js
+++ b/js/src/forum/index.js
@@ -16,20 +16,19 @@ app.initializers.add('foskym/flarum-oauth-center', () => {
};
extend(UserPage.prototype, 'navItems', function (items) {
if (app.session.user && app.session.user.id() === this.user.id()) {
- items.add(
- 'authorized',
- LinkButton.component(
- {
- href: app.route('user.authorized', { username: this.user.username() }),
- icon: 'fas fa-user-friends',
- },
- [
- app.translator.trans('foskym-oauth-center.forum.page.label.authorized'),
- // this.user.moderatorNoteCount() > 0 ?
{this.user.moderatorNoteCount()} : '',
- ]
- ),
- -110
- );
+ // items.add(
+ // 'authorized',
+ // LinkButton.component(
+ // {
+ // href: app.route('user.authorized', { username: this.user.username() }),
+ // icon: 'fas fa-user-friends',
+ // },
+ // [
+ // app.translator.trans('foskym-oauth-center.forum.page.label.authorized'),
+ // ]
+ // ),
+ // -110
+ // );
}
});
});
diff --git a/less/admin.less b/less/admin.less
index 9795d1f..8add5d6 100644
--- a/less/admin.less
+++ b/less/admin.less
@@ -24,6 +24,39 @@
}
}
}
+
+ .OAuthCenterPage-container {
+ max-width: 100% !important;
+ }
+
+ .OAuthCenter-clientsPage, .OAuthCenter-scopesPage {
+ table {
+ width: 100%;
+
+ td, th {
+ padding: 3px 5px;
+ }
+
+ th {
+ text-align: left;
+ }
+ }
+
+ .Checkbox {
+ padding: 0 10px;
+ }
+
+ .FormControl {
+ background: @body-bg;
+ border-color: @control-bg;
+
+ // We set the same as Flarum default, but with more specificity
+ &:focus,
+ &.focus {
+ border-color: @primary-color;
+ }
+ }
+ }
}
@media (min-width: 992px) {
.OAuthCenter {
diff --git a/less/forum.less b/less/forum.less
index e69de29..8a23319 100644
--- a/less/forum.less
+++ b/less/forum.less
@@ -0,0 +1 @@
+@import url('./forum/oauth');
diff --git a/less/forum/oauth.less b/less/forum/oauth.less
new file mode 100644
index 0000000..c85eea5
--- /dev/null
+++ b/less/forum/oauth.less
@@ -0,0 +1,226 @@
+.oauth-area {
+ display: block !important;
+ position: relative;
+ left: 0;
+ top: 0;
+ padding: 110px 0;
+ min-height: 100%;
+ box-sizing: border-box;
+}
+
+.oauth-main {
+ position: relative;
+ width: 376px;
+ margin: 0 auto;
+ box-sizing: border-box;
+ box-shadow: 0px 0px 15px 0px #bdbdbd;
+ border-radius: 12px;
+}
+
+.oauth-main::before {
+ backdrop-filter: blur(20px);
+ content: '';
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ background: hsla(0, 0%, 100%, .3);
+ border-radius: 12px;
+}
+
+.oauth-box {
+ padding: 20px;
+ background-color: #f3f3f3;
+}
+
+.oauth-header {
+ backdrop-filter: blur(0);
+ text-align: center;
+ box-shadow: 0 5px 10px -5px #d2d2d2;
+ border-radius: 12px 12px 0 0;
+}
+
+.oauth-header h2 {
+ margin-bottom: 8px;
+ font-weight: 600;
+ font-size: 40px;
+ color: #000;
+}
+
+.oauth-header p {
+ font-weight: 400;
+ font-size: 20px;
+ color: #333;
+}
+
+.oauth-body {
+ border-radius: 0 0 12px 12px;
+}
+
+.oauth-body .oauth-form-item {
+ position: relative;
+ margin-bottom: 15px;
+ clear: both;
+ *zoom: 1;
+}
+
+.oauth-body .oauth-form-item:after {
+ content: '\20';
+ clear: both;
+ *zoom: 1;
+ display: block;
+ height: 0;
+}
+
+.oauth-icon {
+ position: absolute;
+ left: 4px;
+ top: 1px;
+ width: auto;
+ line-height: 35px;
+ text-align: center;
+ color: #999;
+ padding: 0 8px;
+ font-size: 14px;
+}
+
+label:before {
+ color: #999;
+}
+
+@media screen and (max-width: 768px) {
+ .oauth-area {
+ padding-top: 60px
+ }
+
+ .oauth-main {
+ width: 300px
+ }
+
+ .oauth-box {
+ padding: 10px
+ }
+
+ .oauth-main::before {
+ backdrop-filter: none;
+ }
+
+ .oauth-header {
+ // background-color: #fff;
+ }
+
+ body {
+ margin: 0
+ }
+}
+
+@media screen and (max-width: 600px) {
+ .oauth-area {
+ padding-top: 0
+ }
+
+ body {
+ background: #f3f3f3 !important;
+ }
+
+ .oauth-main {
+ width: 100%;
+ }
+
+ .oauth-main::before {
+ box-shadow: none !important;
+ }
+
+ .oauth-header {
+ box-shadow: none;
+ }
+
+ .oauth-box:last-child {
+ box-shadow: 0 5px 10px -5px #d2d2d2;
+ }
+}
+
+.oauth-top {
+ text-align: center;
+ padding-bottom: 20px;
+ position: relative;
+}
+
+.oauth-top img {
+ width: 64px;
+ border-radius: 50%;
+ border: #4950578c solid 1px;
+ 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;
+ transition: all .3s;
+}
+
+.oauth-top img:hover {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
+}
+
+.oauth-top i {
+ top: -24px;
+ position: relative;
+ padding-left: 10px;
+ padding-right: 10px;
+ color: #111;
+}
+
+.oauth-scope-area {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ overflow: auto;
+ max-height: 350px;
+ position: relative;
+}
+
+.oauth-scope {
+ margin-top: 15px;
+}
+
+.oauth-scope:first-child {
+ margin-top: 0;
+}
+
+.oauth-scope, .oauth-scope-body {
+ overflow: hidden;
+ zoom: 1;
+}
+
+.oauth-scope-body, .oauth-scope-left, .oauth-scope-right {
+ display: table-cell;
+ vertical-align: top;
+}
+
+.oauth-scope-left, .oauth-scope > .pull-left {
+ padding-right: 10px;
+ min-width: 42px;
+ text-align: center;
+}
+
+img.oauth-scope-object {
+ display: block;
+ vertical-align: middle;
+ border: 0;
+ width: 32px;
+ height: 32px;
+}
+
+.oauth-scope-body {
+ width: 10000px;
+ padding-left: 8px;
+}
+
+.oauth-scope-heading {
+ margin-top: 0;
+ font-weight: 800;
+ color: #382e2e;
+ margin-block-end: 0;
+}
+
+.oauth-scope-body small {
+ font-weight: 500;
+ font-size: 12px;
+ color: #aaa;
+}
diff --git a/locale/zh-Hans.yml b/locale/zh-Hans.yml
index 7bcb0f0..556fb1b 100644
--- a/locale/zh-Hans.yml
+++ b/locale/zh-Hans.yml
@@ -12,13 +12,23 @@ foskym-oauth-center:
clients:
client_id: 应用 ID
client_secret: 应用密钥
- redirect_uri: 回调地址(多地址请用空格分割)
- grant_types: 授权类型(可空)
- scope: 权限(可空)
- name: 应用名称(可空)
- description: 应用描述(可空)
- icon: 应用图标地址(可空 可使用fontawesome图标)
- home: 主页地址(可空)
+ redirect_uri: 回调地址
+ grant_types: 授权类型
+ scope: 权限
+ client_name: 名称
+ client_desc: 描述
+ client_icon: 图标
+ client_home: 主页
+ add_button: 添加应用
+ scopes:
+ scope: 权限标识
+ resource_path: 资源路径
+ method: 请求方法
+ is_default: 默认
+ scope_name: 名称
+ scope_icon: 图标
+ scope_desc: 描述
+ add_button: 添加权限
forum:
page:
@@ -26,3 +36,7 @@ foskym-oauth-center:
authorize: 授权
label:
authorized: 授权记录
+ authorize:
+ access: 授权访问
+ agree: 授权
+ deny: 拒绝
diff --git a/migrations/2023_10_02_add_default_record_to_oauth_scopes_table.php b/migrations/2023_10_02_add_default_record_to_oauth_scopes_table.php
new file mode 100644
index 0000000..bda0bc2
--- /dev/null
+++ b/migrations/2023_10_02_add_default_record_to_oauth_scopes_table.php
@@ -0,0 +1,32 @@
+ function (Builder $schema) {
+ if (!$schema->hasTable('oauth_scopes')) {
+ return;
+ }
+ $schema->getConnection()->table('oauth_scopes')->insert([
+ 'scope' => 'user.read',
+ 'resource_path' => '/api/user',
+ 'method' => 'GET',
+ 'is_default' => 1,
+ 'scope_name' => '获取用户信息',
+ 'scope_icon' => 'fas fa-user',
+ 'scope_desc' => '访问该用户({user})的个人信息等',
+ ]);
+ },
+ 'down' => function (Builder $schema) {
+
+ },
+];
diff --git a/src/Api/Controller/CreateClientController.php b/src/Api/Controller/CreateClientController.php
new file mode 100644
index 0000000..866b73b
--- /dev/null
+++ b/src/Api/Controller/CreateClientController.php
@@ -0,0 +1,29 @@
+assertAdmin();
+
+ $attributes = Arr::get($request->getParsedBody(), 'data.attributes');
+
+ return Client::create([
+ 'client_id' => Arr::get($attributes, 'client_id'),
+ 'client_secret' => Arr::get($attributes, 'client_secret'),
+ 'user_id' => $actor->id,
+ ]);
+ }
+}
diff --git a/src/Api/Controller/CreateScopeController.php b/src/Api/Controller/CreateScopeController.php
new file mode 100644
index 0000000..014d45d
--- /dev/null
+++ b/src/Api/Controller/CreateScopeController.php
@@ -0,0 +1,27 @@
+assertAdmin();
+
+ $attributes = Arr::get($request->getParsedBody(), 'data.attributes');
+
+ return Scope::create([
+ 'scope' => Arr::get($attributes, 'scope'),
+ ]);
+ }
+}
diff --git a/src/Api/Controller/DeleteClientController.php b/src/Api/Controller/DeleteClientController.php
new file mode 100644
index 0000000..e585dfa
--- /dev/null
+++ b/src/Api/Controller/DeleteClientController.php
@@ -0,0 +1,26 @@
+getQueryParams(), 'id');
+ RequestUtil::getActor($request)
+ ->assertAdmin();
+
+ $client = Client::find($id);
+
+ $client->delete();
+ }
+}
diff --git a/src/Api/Controller/DeleteScopeController.php b/src/Api/Controller/DeleteScopeController.php
new file mode 100644
index 0000000..52729d8
--- /dev/null
+++ b/src/Api/Controller/DeleteScopeController.php
@@ -0,0 +1,25 @@
+getQueryParams(), 'id');
+ RequestUtil::getActor($request)
+ ->assertAdmin();
+
+ $scope = Scope::find($id);
+
+ $scope->delete();
+ }
+}
diff --git a/src/Api/Controller/ListClientController.php b/src/Api/Controller/ListClientController.php
index caa3307..566fba0 100644
--- a/src/Api/Controller/ListClientController.php
+++ b/src/Api/Controller/ListClientController.php
@@ -16,10 +16,8 @@ class ListClientController extends AbstractListController
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
- if (!$actor->isAdmin()) {
- return [];
- }
+ $actor->assertAdmin();
- return Client::get();
+ return Client::all();
}
}
diff --git a/src/Api/Controller/ListScopeController.php b/src/Api/Controller/ListScopeController.php
new file mode 100644
index 0000000..44344ef
--- /dev/null
+++ b/src/Api/Controller/ListScopeController.php
@@ -0,0 +1,23 @@
+assertAdmin();
+
+ return Scope::all();
+ }
+}
diff --git a/src/Api/Controller/ShowClientController.php b/src/Api/Controller/ShowClientController.php
new file mode 100644
index 0000000..5a64375
--- /dev/null
+++ b/src/Api/Controller/ShowClientController.php
@@ -0,0 +1,26 @@
+getQueryParams(), 'client_id');
+ RequestUtil::getActor($request)->assertRegistered();
+
+ $client = Client::where('client_id', $client_id)->get();
+
+ return $client;
+
+ }
+}
diff --git a/src/Api/Controller/UpdateClientController.php b/src/Api/Controller/UpdateClientController.php
new file mode 100644
index 0000000..e2cfee1
--- /dev/null
+++ b/src/Api/Controller/UpdateClientController.php
@@ -0,0 +1,37 @@
+assertAdmin();
+
+ $id = Arr::get($request->getQueryParams(), 'id');
+ $client = Client::find($id);
+
+ $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []);
+
+ collect(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'client_name', 'client_desc', 'client_icon', 'client_home'])
+ ->each(function (string $attribute) use ($client, $attributes) {
+ if (($val = Arr::get($attributes, $attribute)) !== null) {
+ $client->$attribute = $val;
+ }
+ });
+
+ $client->save();
+
+ return $client;
+ }
+}
diff --git a/src/Api/Controller/UpdateScopeController.php b/src/Api/Controller/UpdateScopeController.php
new file mode 100644
index 0000000..5f9aeb7
--- /dev/null
+++ b/src/Api/Controller/UpdateScopeController.php
@@ -0,0 +1,37 @@
+assertAdmin();
+
+ $id = Arr::get($request->getQueryParams(), 'id');
+ $scope = Scope::find($id);
+
+ $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []);
+
+ collect(['scope', 'resource_path', 'method', 'is_default', 'scope_name', 'scope_icon', 'scope_desc'])
+ ->each(function (string $attribute) use ($scope, $attributes) {
+ if (($val = Arr::get($attributes, $attribute)) !== null) {
+ $scope->$attribute = $val;
+ }
+ });
+
+ $scope->save();
+
+ return $scope;
+ }
+}
diff --git a/src/Api/Serializer/ClientPublicSerializer.php b/src/Api/Serializer/ClientPublicSerializer.php
new file mode 100644
index 0000000..660fd63
--- /dev/null
+++ b/src/Api/Serializer/ClientPublicSerializer.php
@@ -0,0 +1,35 @@
+ $model->id,
+ "client_id" => $model->client_id,
+ "redirect_uri" => $model->redirect_uri,
+ "grant_types" => $model->grant_types,
+ "scope" => $model->scope,
+ "client_name" => $model->client_name,
+ "client_icon" => $model->client_icon,
+ "client_desc" => $model->client_desc,
+ "client_home" => $model->client_home
+ ];
+ }
+}
diff --git a/src/Api/Serializer/ScopeSerializer.php b/src/Api/Serializer/ScopeSerializer.php
new file mode 100644
index 0000000..fabdbc7
--- /dev/null
+++ b/src/Api/Serializer/ScopeSerializer.php
@@ -0,0 +1,34 @@
+ $model->id,
+ "scope" => $model->scope,
+ "resource_path" => $model->resource_path,
+ "method" => $model->method,
+ "is_default" => $model->is_default,
+ "scope_name" => $model->scope_name,
+ "scope_icon" => $model->scope_icon,
+ "scope_desc" => $model->scope_desc,
+ ];
+ }
+}
diff --git a/src/Controllers/ApiUserController.php b/src/Controllers/ApiUserController.php
new file mode 100644
index 0000000..d20fdd2
--- /dev/null
+++ b/src/Controllers/ApiUserController.php
@@ -0,0 +1,49 @@
+settings = $settings;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $actor = RequestUtil::getActor($request);
+ $actor = $actor->toArray();
+ $data = [
+ 'id' => $actor['id'],
+ 'username' => $actor['username'],
+ 'nickname' => $actor['nickname'],
+ 'avatar_url' => $actor['avatar_url'],
+ 'email' => $actor['email'],
+ 'is_email_confirmed' => $actor['is_email_confirmed'],
+ 'joined_at' => $actor['joined_at'],
+ 'last_seen_at' => $actor['last_seen_at'],
+ 'discussion_count' => $actor['discussion_count'],
+ 'comment_count' => $actor['comment_count'],
+ ];
+ return new JsonResponse($data);
+ }
+}
diff --git a/src/Controllers/AuthorizeController.php b/src/Controllers/AuthorizeController.php
index e210b87..b004f71 100644
--- a/src/Controllers/AuthorizeController.php
+++ b/src/Controllers/AuthorizeController.php
@@ -35,23 +35,24 @@ class AuthorizeController implements RequestHandlerInterface
$params = $request->getParsedBody();
- $oauth = new OAuth();
+ $oauth = new OAuth($this->settings);
$server = $oauth->server();
$request = $oauth->request()::createFromGlobals();
$response = $oauth->response();
if (!$server->validateAuthorizeRequest($request, $response)) {
- $response->send();
- die;
+ return new JsonResponse(json_decode($response->getResponseBody(), true));
}
- $is_authorized = (Arr::get($params, 'authorized', 'no') === 'yes');
+ $is_authorized = Arr::get($params, 'is_authorized', 0);
$server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id);
if ($is_authorized) {
- // this is only here so that you get to see your code in the cURL request. Otherwise, we'd redirect back to the client
- /* $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=')+5, 40);
- exit("SUCCESS! Authorization Code: $code");*/
+ $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=') + 5, 40);
+ return new JsonResponse([
+ 'code' => $code
+ ]);
}
- $response->send();
+
+ return new JsonResponse(json_decode($response->getResponseBody(), true));
}
}
diff --git a/src/Controllers/TokenController.php b/src/Controllers/TokenController.php
new file mode 100644
index 0000000..63be7ed
--- /dev/null
+++ b/src/Controllers/TokenController.php
@@ -0,0 +1,40 @@
+settings = $settings;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $oauth = new OAuth($this->settings);
+ $server = $oauth->server();
+
+ $body = $server->handleTokenRequest($oauth->request()::createFromGlobals())
+ ->getResponseBody();
+ return new JsonResponse(json_decode($body, true));
+ }
+}
diff --git a/src/Middlewares/ResourceScopeMiddleware.php b/src/Middlewares/ResourceScopeMiddleware.php
index 288acbd..951cbd4 100644
--- a/src/Middlewares/ResourceScopeMiddleware.php
+++ b/src/Middlewares/ResourceScopeMiddleware.php
@@ -4,50 +4,63 @@ namespace FoskyM\OAuthCenter\Middlewares;
use Flarum\Foundation\ErrorHandling\ExceptionHandler\IlluminateValidationExceptionHandler;
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
+use Flarum\Settings\SettingsRepositoryInterface;
+use Flarum\User\User;
use FoskyM\OAuthCenter\OAuth;
use FoskyM\OAuthCenter\Storage;
use Illuminate\Support\Arr;
+use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
+use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Flarum\Http\RequestUtil;
-use Flarum\Api\JsonApiResponse;
-use Tobscure\JsonApi\Document;
-use Tobscure\JsonApi\Exception\Handler\ResponseBag;
-
use FoskyM\OAuthCenter\Models\Scope;
+
class ResourceScopeMiddleware implements MiddlewareInterface
{
+ const TOKEN_PREFIX = 'Bearer ';
+ protected $settings;
+ public function __construct(SettingsRepositoryInterface $settings)
+ {
+ $this->settings = $settings;
+ }
public function process(Request $request, RequestHandlerInterface $handler): Response
{
- $path = $request->getUri()->getPath();
- $token = Arr::get($request->getQueryParams(), 'access_token', '');
+ if (!$request->getAttribute('originalUri')) {
+ return $handler->handle($request);
+ }
+
+ $headerLine = $request->getHeaderLine('authorization');
+
+ $parts = explode(';', $headerLine);
+
+ if (isset($parts[0]) && Str::startsWith($parts[0], self::TOKEN_PREFIX)) {
+ $token = substr($parts[0], strlen(self::TOKEN_PREFIX));
+ } else {
+ $token = Arr::get($request->getQueryParams(), 'access_token', '');
+ }
+ $path = $request->getAttribute('originalUri')->getPath();
+
if ($token !== '' && $scope = Scope::get_path_scope($path)) {
if (strtolower($request->getMethod()) === strtolower($scope->method)) {
try {
- $oauth = new OAuth();
+ $oauth = new OAuth($this->settings);
$server = $oauth->server();
- $request = $oauth->request();
- if (!$server->verifyResourceRequest($request::createFromGlobals(), null, $scope->scope)) {
- $server->getResponse()->send('json');
- die;
- }
- /*$error = new ResponseBag('422', [
- [
- 'status' => '422',
- 'code' => 'validation_error',
- 'source' => [
- 'pointer' => $path,
- ],
- 'detail' => 'Yikes! The access token don\'t has the scope.',
- ],
- ]);
- $document = new Document();
- $document->setErrors($error->getErrors());
+ $oauth_request = $oauth->request()::createFromGlobals();
- return new JsonApiResponse($document, $error->getStatus());*/
+ if (!$server->verifyResourceRequest($oauth_request, null, $scope->scope)) {
+ return new JsonResponse(json_decode($server->getResponse()->getResponseBody(), true));
+ }
+
+ $token = $server->getAccessTokenData($oauth_request);
+ $actor = User::find($token['user_id']);
+
+ $request = RequestUtil::withActor($request, $actor);
+ $request = $request->withAttribute('bypassCsrfToken', true);
+ $request = $request->withoutAttribute('session');
} catch (ValidationException $exception) {
$handler = resolve(IlluminateValidationExceptionHandler::class);
diff --git a/src/Middlewares/UnsetCsrfMiddleware.php b/src/Middlewares/UnsetCsrfMiddleware.php
new file mode 100644
index 0000000..f1a2d72
--- /dev/null
+++ b/src/Middlewares/UnsetCsrfMiddleware.php
@@ -0,0 +1,35 @@
+getUri()->getPath();
+ if (in_array($path, $uri)) {
+ $request = $request->withAttribute('bypassCsrfToken', true);
+ }
+
+ return $handler->handle($request);
+ }
+}
diff --git a/src/Models/AccessToken.php b/src/Models/AccessToken.php
index 502d559..4204e88 100644
--- a/src/Models/AccessToken.php
+++ b/src/Models/AccessToken.php
@@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel;
class AccessToken extends AbstractModel
{
protected $table = 'oauth_access_tokens';
+ protected $guarded = [];
}
diff --git a/src/Models/AuthorizationCode.php b/src/Models/AuthorizationCode.php
index 8b34f73..f144175 100644
--- a/src/Models/AuthorizationCode.php
+++ b/src/Models/AuthorizationCode.php
@@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel;
class AuthorizationCode extends AbstractModel
{
protected $table = 'oauth_authorization_codes';
+ protected $guarded = [];
}
diff --git a/src/Models/Client.php b/src/Models/Client.php
index 840b713..d6dc3d2 100644
--- a/src/Models/Client.php
+++ b/src/Models/Client.php
@@ -15,4 +15,16 @@ use Flarum\Database\AbstractModel;
class Client extends AbstractModel
{
protected $table = 'oauth_clients';
+ protected $guarded = [];
+
+ public static function build(string $client_id, string $client_secret, int $user_id)
+ {
+ $client = new static();
+
+ $client->client_id = $client_id;
+ $client->client_secret = $client_secret;
+ $client->user_id = $user_id;
+
+ return $client;
+ }
}
diff --git a/src/Models/Jwt.php b/src/Models/Jwt.php
index d9ee703..652a3c4 100644
--- a/src/Models/Jwt.php
+++ b/src/Models/Jwt.php
@@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel;
class Jwt extends AbstractModel
{
protected $table = 'oauth_jwt';
+ protected $guarded = [];
}
diff --git a/src/Models/RefreshToken.php b/src/Models/RefreshToken.php
index 7f6bea0..92919f4 100644
--- a/src/Models/RefreshToken.php
+++ b/src/Models/RefreshToken.php
@@ -15,4 +15,5 @@ use Flarum\Database\AbstractModel;
class RefreshToken extends AbstractModel
{
protected $table = 'oauth_refresh_tokens';
+ protected $guarded = [];
}
diff --git a/src/Models/Scope.php b/src/Models/Scope.php
index 125bf94..b191afc 100644
--- a/src/Models/Scope.php
+++ b/src/Models/Scope.php
@@ -15,7 +15,7 @@ use Flarum\Database\AbstractModel;
class Scope extends AbstractModel
{
protected $table = 'oauth_scopes';
-
+ protected $guarded = [];
static public function get_path_scope($path = '')
{
return self::where('resource_path', 'like', $path . '%')->first();
diff --git a/src/OAuth.php b/src/OAuth.php
index 8718df0..66c67a8 100644
--- a/src/OAuth.php
+++ b/src/OAuth.php
@@ -36,9 +36,13 @@ class OAuth
{
return new Request;
}
+
+ public function storage(): Storage
+ {
+ return new Storage;
+ }
public function server(): Server
{
-
$storage = new Storage;
$server = new Server($storage, array(
'allow_implicit' => $this->settings->get('foskym-oauth-center.allow_implicit') == "1",
diff --git a/src/Storage.php b/src/Storage.php
index e99fea1..c5c26c9 100644
--- a/src/Storage.php
+++ b/src/Storage.php
@@ -204,10 +204,10 @@ class Storage implements
*/
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{
- if (func_num_args() > 6) {
+ /*if (func_num_args() > 6) {
// we are calling with an id token
return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args());
- }
+ }*/
// convert expires to datestring
$expires = date('Y-m-d H:i:s', $expires);
@@ -433,7 +433,7 @@ class Storage implements
if ($result = Models\Scope::where('is_default', true)->get()) {
$defaultScope = array_map(function ($row) {
return $row['scope'];
- }, $result);
+ }, $result->toArray());
return implode(' ', $defaultScope);
}