diff --git a/composer.json b/composer.json index cbe48db..8467d87 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "name": "foskym/flarum-oauth-center", - "description": "Allow user to authorize the third clients", + "name": "rhodes-island/flarum-oauth-center", + "description": "Allow user to authorize the third-party clients", "keywords": [ "flarum", "user", @@ -19,6 +19,10 @@ "email": "i@fosky.top", "role": "Developer", "homepage": "https://fosky.top" + }, + { + "name": "Rain", + "role": "Developer" } ], "autoload": { diff --git a/extend.php b/extend.php index 5bae177..0379ef1 100644 --- a/extend.php +++ b/extend.php @@ -48,9 +48,9 @@ return [ ->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'), + ->serializeToForum('rhodes-island-oauth-center.allow_implicit', 'rhodes-island-oauth-center.allow_implicit', 'boolval') + ->serializeToForum('rhodes-island-oauth-center.enforce_state', 'rhodes-island-oauth-center.enforce_state', 'boolval') + ->serializeToForum('rhodes-island-oauth-center.require_exact_redirect_uri', 'rhodes-island-oauth-center.require_exact_redirect_uri', 'boolval'), (new Extend\Middleware('api')) ->insertAfter(AuthenticateWithHeader::class, ResourceScopeMiddleware::class), diff --git a/js/.gitignore b/js/.gitignore index adc90f3..d8d1816 100644 --- a/js/.gitignore +++ b/js/.gitignore @@ -7,3 +7,5 @@ !.yarn/versions node_modules + +dist \ No newline at end of file diff --git a/js/dist/admin.js b/js/dist/admin.js deleted file mode 100644 index 6c858cd..0000000 Binary files a/js/dist/admin.js and /dev/null differ diff --git a/js/dist/admin.js.map b/js/dist/admin.js.map deleted file mode 100644 index 4251e7d..0000000 Binary files a/js/dist/admin.js.map and /dev/null differ diff --git a/js/dist/forum.js b/js/dist/forum.js deleted file mode 100644 index 5515709..0000000 Binary files a/js/dist/forum.js and /dev/null differ diff --git a/js/dist/forum.js.map b/js/dist/forum.js.map deleted file mode 100644 index 36873d7..0000000 Binary files a/js/dist/forum.js.map and /dev/null differ diff --git a/js/package-lock.json b/js/package-lock.json deleted file mode 100644 index 8480f8f..0000000 Binary files a/js/package-lock.json and /dev/null differ diff --git a/js/package.json b/js/package.json index bda04ef..b85cb74 100644 --- a/js/package.json +++ b/js/package.json @@ -1,16 +1,18 @@ { - "name": "@foskym/flarum-oauth-center", + "name": "@rhodes-island/flarum-oauth-center", "private": true, "version": "0.0.0", "devDependencies": { - "flarum-webpack-config": "^2.0.0", - "webpack": "^5.65.0", - "webpack-cli": "^4.9.1", - "prettier": "^2.5.1", - "@flarum/prettier-config": "^1.0.0", + "@babel/core": "^7.14.6", + "@types/mithril": "^2.0.11", + "babel-loader": "^8.2.2", "flarum-tsconfig": "^1.0.2", + "flarum-webpack-config": "^2.0.2", + "moment": "^2.29.1", + "ts-loader": "8.1.0", "typescript": "^4.5.4", - "typescript-coverage-report": "^0.6.1" + "webpack": "^5.90.3", + "webpack-cli": "^5.1.4" }, "scripts": { "dev": "webpack --mode development --watch", @@ -23,6 +25,5 @@ "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'", "check-typings": "tsc --noEmit --emitDeclarationOnly false", "check-typings-coverage": "typescript-coverage-report" - }, - "prettier": "@flarum/prettier-config" + } } diff --git a/js/src/admin/components/EditApplicationModal.tsx b/js/src/admin/components/EditApplicationModal.tsx new file mode 100644 index 0000000..b68464a --- /dev/null +++ b/js/src/admin/components/EditApplicationModal.tsx @@ -0,0 +1,44 @@ +import app from "flarum/admin/app"; +import type Client from "src/common/models/Client"; + +import EditModelModal from "./EditModelModal"; +import { NestedStringArray } from "@askvortsov/rich-icu-message-formatter"; +import { randomString } from "../util"; + +export default class EditApplicationModal extends EditModelModal { + modelType(): string { + return "oauth-clients"; + } + + fields() { + return [ + 'client_id', + 'client_secret', + 'redirect_uri', + 'grant_types', + 'scope', + 'client_name', + 'client_desc', + 'client_icon', + 'client_home' + ]; + } + + defaultValues() { + return { + client_id: randomString(32), + client_secret: randomString(32) + }; + } + + translateFieldName(field: string): NestedStringArray { + return app.translator.trans("rhodes-island-oauth-center.admin.clients." + field); + } + + className(): string { + return "OAuth-EditApplication"; + } + title() { + return app.translator.trans("rhodes-island-oauth-center.admin.clients.edit_modal_title_" + (this.model ? "edit" : "add")); + } +} \ No newline at end of file diff --git a/js/src/admin/components/EditModelModal.tsx b/js/src/admin/components/EditModelModal.tsx new file mode 100644 index 0000000..8f930e0 --- /dev/null +++ b/js/src/admin/components/EditModelModal.tsx @@ -0,0 +1,87 @@ +import { type NestedStringArray } from "@askvortsov/rich-icu-message-formatter"; +import app from "flarum/admin/app"; +import type Model from "flarum/common/Model"; +import Button from "flarum/common/components/Button"; +import Modal from "flarum/common/components/Modal"; +import Stream from "flarum/common/utils/Stream"; + +export default abstract class EditModelModal extends Modal { + model: T | undefined; + + saving = false; + fieldValues: { [x: string]: Stream } = {}; + + oninit(vnode: any): void { + super.oninit(vnode); + + this.model = (this.attrs as any).model; + + for (const field of this.fields()) { + this.fieldValues[field] = Stream(this.model ? ((this.model as any)[field]() ?? "") : ""); + } + + if (!this.model) { + const defaults = this.defaultValues(); + for (const field in defaults) { + this.fieldValues[field](defaults[field]); + } + } + } + + abstract modelType(): string; + + abstract fields(): string[]; + + abstract defaultValues(): { [x: string]: string } + + abstract translateFieldName(field: string): NestedStringArray + + className(): string { + return "OAuth-EditModel"; + } + content() { + return ( +
+ {this.fields().map(field =>
+ + +
)} + +
+ ); + } + + save() { + this.saving = true; + if (!this.model) { + this.model = app.store.createRecord(this.modelType()) as T; + } + let modifiedCount = 0; + const attrs: { [x: string]: string } = {}; + Object.entries(this.fieldValues).forEach(val => { + const value = val[1](); + const modelValue = (this.model as any)[val[0]](); + if (value != modelValue && (value != "" || modelValue != null)) { + attrs[val[0]] = value; + modifiedCount++; + } + }); + if (modifiedCount == 0) { + this.saving = false; + return; + }; + this.model.save(attrs).finally(() => { + this.saving = false; + if (typeof (this.attrs as any).createCallback === "function") { + (this.attrs as any).createCallback(); + } + m.redraw(); + }); + } +} \ No newline at end of file diff --git a/js/src/admin/components/SettingsPage.js b/js/src/admin/components/SettingsPage.tsx similarity index 80% rename from js/src/admin/components/SettingsPage.js rename to js/src/admin/components/SettingsPage.tsx index d595d7f..e6102ba 100644 --- a/js/src/admin/components/SettingsPage.js +++ b/js/src/admin/components/SettingsPage.tsx @@ -4,6 +4,7 @@ import Button from 'flarum/common/components/Button'; import IndexPage from "../pages/IndexPage"; import ClientsPage from "../pages/ClientsPage"; import ScopesPage from "../pages/ScopesPage"; + export default class SettingsPage extends ExtensionPage { content() { const page = m.route.param().page || 'index'; @@ -24,42 +25,42 @@ export default class SettingsPage extends ExtensionPage { } // Return button menus - menuButtons(page) { + menuButtons(page: string) { return [ Button.component({ className: `Button ${page === 'index' ? 'item-selected' : ''}`, onclick: () => m.route.set( app.route('extension', { - id: 'foskym-oauth-center' + id: 'rhodes-island-oauth-center' }) ), icon: 'fas fa-home', - }, app.translator.trans('foskym-oauth-center.admin.page.index')), + }, app.translator.trans('rhodes-island-oauth-center.admin.page.index')), Button.component({ className: `Button ${page === 'clients' ? 'item-selected' : ''}`, onclick: () => m.route.set( app.route('extension', { - id: 'foskym-oauth-center', + id: 'rhodes-island-oauth-center', page: 'clients' }) ), icon: 'fas fa-network-wired', - }, app.translator.trans('foskym-oauth-center.admin.page.clients')), + }, app.translator.trans('rhodes-island-oauth-center.admin.page.clients')), Button.component({ className: `Button ${page === 'scopes' ? 'item-selected' : ''}`, onclick: () => m.route.set( app.route('extension', { - id: 'foskym-oauth-center', + id: 'rhodes-island-oauth-center', page: 'scopes' }) ), icon: 'fas fa-user-lock', - }, app.translator.trans('foskym-oauth-center.admin.page.scopes')), + }, app.translator.trans('rhodes-island-oauth-center.admin.page.scopes')), ]; } - pageContent(page) { + pageContent(page: string) { if (page == 'clients') { return } else if (page == 'scopes') { diff --git a/js/src/admin/index.js b/js/src/admin/index.js deleted file mode 100644 index 6f67bfe..0000000 --- a/js/src/admin/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import app from 'flarum/admin/app'; -import SettingsPage from './components/SettingsPage'; -app.initializers.add('foskym/flarum-oauth-center', () => { - app.extensionData - .for('foskym-oauth-center') - .registerPage(SettingsPage) - .registerPermission( - { - icon: 'fas fa-user-friends', - label: app.translator.trans('foskym-oauth-center.admin.permission.use_oauth'), - permission: 'foskym-oauth-center.use-oauth', - }, - 'view', - 95 - ); -}); diff --git a/js/src/admin/index.ts b/js/src/admin/index.ts new file mode 100644 index 0000000..6ac3256 --- /dev/null +++ b/js/src/admin/index.ts @@ -0,0 +1,36 @@ +import app from 'flarum/admin/app'; +import SettingsPage from './components/SettingsPage'; +app.initializers.add('foskym/flarum-oauth-center', () => { + app.extensionData + .for('rhodes-island-oauth-center') + .registerPage(SettingsPage) + .registerPermission( + { + icon: 'fas fa-user-friends', + label: app.translator.trans('rhodes-island-oauth-center.admin.permission.use_oauth'), + permission: 'rhodes-island-oauth-center.use-oauth', + }, + 'view', + 95 + ) + .registerSetting({ + label: app.translator.trans('rhodes-island-oauth-center.admin.settings.allow_implicit'), + setting: "rhodes-island-oauth-center.allow_implicit", + type: "boolean" + }) + .registerSetting({ + label: app.translator.trans('rhodes-island-oauth-center.admin.settings.enforce_state'), + setting: "rhodes-island-oauth-center.enforce_state", + type: "boolean" + }) + .registerSetting({ + label: app.translator.trans('rhodes-island-oauth-center.admin.settings.require_exact_redirect_uri'), + setting: "rhodes-island-oauth-center.require_exact_redirect_uri", + type: "boolean" + }) + .registerSetting({ + label: app.translator.trans('rhodes-island-oauth-center.admin.settings.access_lifetime'), + setting: "rhodes-island-oauth-center.access_lifetime", + type: "number" + }); +}); diff --git a/js/src/admin/pages/ClientsPage.js b/js/src/admin/pages/ClientsPage.js deleted file mode 100644 index b05e0af..0000000 --- a/js/src/admin/pages/ClientsPage.js +++ /dev/null @@ -1,100 +0,0 @@ -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 { - translationPrefix = 'foskym-oauth-center.admin.clients.'; - clients = []; - oninit(vnode) { - super.oninit(vnode); - - 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 ( -
- { - 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/ClientsPage.tsx b/js/src/admin/pages/ClientsPage.tsx new file mode 100644 index 0000000..630f386 --- /dev/null +++ b/js/src/admin/pages/ClientsPage.tsx @@ -0,0 +1,79 @@ +import app from 'flarum/admin/app'; +import Page from 'flarum/common/components/Page'; +import Button from 'flarum/common/components/Button'; +import LoadingIndicator from "flarum/common/components/LoadingIndicator"; +import type Client from 'src/common/models/Client'; +import EditApplicationModal from '../components/EditApplicationModal'; + +export default class ClientsPage extends Page { + translationPrefix = 'rhodes-island-oauth-center.admin.clients.'; + loading = false; + clients: Client[] = []; + tableFields = [ + "client_id", + "client_name", + "client_desc" + ]; + loadingClients: Client[] = []; + + oninit(vnode: any) { + super.oninit(vnode); + + this.refresh(); + } + + refresh() { + this.loading = true; + app.store.find('oauth-clients').then(r => { + this.clients = r as unknown as Client[]; + this.loading = false; + m.redraw(); + }); + } + + view() { + return ( +
+ {(this.loading ? :
+ + + + {this.tableFields.map(key => )} + + + + + {this.clients.map((client: any) => { + const clientSaving = this.loadingClients.indexOf(client) != -1; + return + {this.tableFields.map(key => )} + + + } + )} + +
{app.translator.trans(this.translationPrefix + key)}{app.translator.trans(this.translationPrefix + "actions")}
{client[key]()} + +   + +
+ +
)} +
+ ); + } +} diff --git a/js/src/admin/pages/IndexPage.js b/js/src/admin/pages/IndexPage.js deleted file mode 100644 index 241bc2d..0000000 --- a/js/src/admin/pages/IndexPage.js +++ /dev/null @@ -1,132 +0,0 @@ -import app from 'flarum/admin/app'; -import Page from 'flarum/common/components/Page'; -import FieldSet from 'flarum/common/components/FieldSet'; -import Button from 'flarum/common/components/Button'; -import saveSettings from 'flarum/admin/utils/saveSettings'; -import Stream from 'flarum/common/utils/Stream'; -import Select from 'flarum/common/components/Select'; -import Switch from 'flarum/common/components/Switch'; - -export default class IndexPage extends Page { - oninit(vnode) { - super.oninit(vnode); - - this.saving = false; - - this.fields = [ - 'foskym-oauth-center.access_lifetime', - 'foskym-oauth-center.allow_implicit', - 'foskym-oauth-center.enforce_state', - 'foskym-oauth-center.require_exact_redirect_uri' - ]; - this.values = {}; - - const settings = app.data.settings; - this.fields.forEach(key => this.values[key] = Stream(settings[key] || "")); - - for (let i = 1; i < this.fields.length; i++) { - this.values[this.fields[i]] = settings[this.fields[i]] === '1'; - } - } - - view() { - return ( -
-
- {FieldSet.component({}, [ -
, - Switch.component({ - state: this.values['foskym-oauth-center.allow_implicit'], - onchange: (value) => this.saveSingleSetting('foskym-oauth-center.allow_implicit', value), - loading: this.saving, - }, app.translator.trans('foskym-oauth-center.admin.settings.allow_implicit')), - ])} - - {FieldSet.component({}, [ -
, - Switch.component({ - state: this.values['foskym-oauth-center.enforce_state'], - onchange: (value) => this.saveSingleSetting('foskym-oauth-center.enforce_state', value), - loading: this.saving, - }, app.translator.trans('foskym-oauth-center.admin.settings.enforce_state')), - ])} - - {FieldSet.component({}, [ -
, - Switch.component({ - state: this.values['foskym-oauth-center.require_exact_redirect_uri'], - onchange: (value) => this.saveSingleSetting('foskym-oauth-center.require_exact_redirect_uri', value), - loading: this.saving, - }, app.translator.trans('foskym-oauth-center.admin.settings.require_exact_redirect_uri')), - ])} -
- {FieldSet.component({}, [ - , -
- {app.translator.trans('foskym-oauth-center.admin.settings.access_lifetime')} -
, - Button.component({ - type: 'submit', - className: 'Button Button--primary', - loading: this.saving - }, app.translator.trans('core.admin.settings.submit_button')) - ])} - -
- -
- ); - } - - saveSingleSetting(setting, value) { - if (this.saving) return; - - this.saving = true; - - this.values[setting] = value; - - let data = {}; - data[setting] = value; - - saveSettings(data) - .then(() => app.alerts.show({type: 'success'}, app.translator.trans('core.admin.settings.saved_message'))) - .catch(() => { - }) - .then(() => { - this.saving = false; - m.redraw(); - }); - } - - onsubmit(e) { - e.preventDefault(); - - if (this.saving) return; - - this.saving = true; - app.alerts.dismiss(this.successAlert); - - const settings = {}; - - settings['foskym-oauth-center.access_lifetime'] = this.values['foskym-oauth-center.access_lifetime'](); - - // this.fields.forEach(key => { - // settings[key] = this.values[key]() - // - // }); - - if (settings['foskym-oauth-center.access_lifetime'] === "") { - settings['foskym-oauth-center.access_lifetime'] = 3600; - } - - saveSettings(settings) - .then(() => app.alerts.show({type: 'success'}, app.translator.trans('core.admin.settings.saved_message'))) - .catch(() => { - }) - .then(() => { - this.saving = false; - m.redraw(); - }); - } -} diff --git a/js/src/admin/pages/IndexPage.tsx b/js/src/admin/pages/IndexPage.tsx new file mode 100644 index 0000000..fff19ad --- /dev/null +++ b/js/src/admin/pages/IndexPage.tsx @@ -0,0 +1,23 @@ +import app from 'flarum/admin/app'; +import AdminPage from 'flarum/admin/components/AdminPage'; + +export default class IndexPage extends AdminPage { + header() { + + } + + content() { + const settings = app.extensionData.getSettings("rhodes-island-oauth-center"); + + return ( +
+
+
+ {settings!!.map(this.buildSettingComponent.bind(this))} +
{this.submitButton()}
+
+
+
+ ); + } +} diff --git a/js/src/admin/pages/ScopesPage.js b/js/src/admin/pages/ScopesPage.js deleted file mode 100644 index 2fb36d0..0000000 --- a/js/src/admin/pages/ScopesPage.js +++ /dev/null @@ -1,118 +0,0 @@ -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 { - 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/admin/pages/ScopesPage.tsx b/js/src/admin/pages/ScopesPage.tsx new file mode 100644 index 0000000..bf7f4ff --- /dev/null +++ b/js/src/admin/pages/ScopesPage.tsx @@ -0,0 +1,166 @@ +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'; +import type Scope from 'src/common/models/Scope'; +import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; +import Stream from 'flarum/common/utils/Stream'; +import { randomString } from '../util'; + +interface EditingScope { + model?: Scope, + markDeletion?: boolean, + modifiedFields: string[], + scope?: Stream, + resource_path?: Stream, + method?: Stream, + is_default?: Stream, + scope_name?: Stream, + scope_icon?: Stream, + scope_desc?: Stream +} + +export default class ScopesPage extends Page { + translationPrefix = 'rhodes-island-oauth-center.admin.scopes.'; + scopes: Scope[] = []; + fields = [ + 'scope', + 'resource_path', + 'method', + 'is_default', + 'scope_name', + 'scope_icon', + 'scope_desc' + ]; + + bufferedValues: EditingScope[] = []; + + components: { [x: string]: (stream: Stream, disabled: boolean) => any } = { + "method": (stream, disabled) => , + "is_default": (stream, disabled) => stream(val ? 1 : 0)}> + } + + loading = false; + saving = false; + + oninit(vnode: any) { + super.oninit(vnode); + + this.refresh(); + } + + streamMapper(stream: Stream, scope: EditingScope, field: string) { + stream.map((val: any) => { + if (stream.notInitial) { + scope.modifiedFields.push(field); + } else { + stream.notInitial = true; + } + return val; + }); + } + + refresh() { + this.loading = true; + this.bufferedValues = []; + app.store.find('oauth-scopes').then(r => { + this.scopes = r as unknown as Scope[]; + for (const scope of this.scopes) { + const eScope: EditingScope = {model: scope, modifiedFields: []}; + for (const field of this.fields) { + const value = (scope as any)[field](); + const stream = Stream(value ? value : (field == "method" ? "GET" : null)); + (eScope as any)[field] = stream; + this.streamMapper(stream, eScope, field); + } + this.bufferedValues.push(eScope); + } + this.loading = false; + m.redraw(); + }); + } + + view() { + return ( +
+ {(this.loading ? :
+ + + + {this.fields.map(key => )} + + + + {this.bufferedValues.map((scope) => + {this.fields.map((key) => )} + + )} + +
{app.translator.trans(this.translationPrefix + key)}
+ {(this.components[key] && this.components[key]((scope as any)[key], this.saving)) ?? } + + +
+ +
+ +
)} +
+ ); + } + + save() { + const promises = []; + for (const eScope of this.bufferedValues) { + const scope = eScope.model; + if (scope) { + if (eScope.markDeletion) { + promises.push(scope.delete()); + } else { + if (eScope.modifiedFields.length > 0) { + const attrs: any = {}; + for (const field of eScope.modifiedFields) { + attrs[field] = (eScope as any)[field](); + } + promises.push(scope.save(attrs)); + } + } + } else { + const scope = app.store.createRecord("oauth-scopes"); + const attrs: any = {}; + for (const field of this.fields) { + const value = (eScope as any)[field](); + if (value) { + attrs[field] = value; + } + } + promises.push(scope.save(attrs)); + } + } + if (promises.length > 0) { + this.saving = true; + Promise.allSettled(promises).finally(() => { + this.saving = false; + this.refresh(); + }); + } + } +} diff --git a/js/src/admin/util.ts b/js/src/admin/util.ts new file mode 100644 index 0000000..07f2753 --- /dev/null +++ b/js/src/admin/util.ts @@ -0,0 +1,10 @@ +export function randomString(len: number) { + len = len || 32; + let $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let maxPos = $chars.length; + let pwd = ''; + for (let i = 0; i < len; i++) { + pwd += $chars.charAt(Math.floor(Math.random() * (maxPos + 1))); + } + return pwd; +} \ No newline at end of file diff --git a/js/src/common/extend.js b/js/src/common/extend.ts similarity index 100% rename from js/src/common/extend.js rename to js/src/common/extend.ts diff --git a/js/src/common/models/Client.js b/js/src/common/models/Client.js deleted file mode 100644 index 4372b95..0000000 --- a/js/src/common/models/Client.js +++ /dev/null @@ -1,14 +0,0 @@ -import Model from 'flarum/common/Model'; - -export default class Client extends Model { - client_id = Model.attribute('client_id'); - client_secret = Model.attribute('client_secret'); - redirect_uri = Model.attribute('redirect_uri'); - grant_types = Model.attribute('grant_types'); - scope = Model.attribute('scope'); - user_id = Model.attribute('user_id'); - client_name = Model.attribute('client_name'); - client_icon = Model.attribute('client_icon'); - client_desc = Model.attribute('client_desc'); - client_home = Model.attribute('client_home'); -} diff --git a/js/src/common/models/Client.ts b/js/src/common/models/Client.ts new file mode 100644 index 0000000..5c79ca7 --- /dev/null +++ b/js/src/common/models/Client.ts @@ -0,0 +1,14 @@ +import Model from 'flarum/common/Model'; + +export default class Client extends Model { + client_id = Model.attribute('client_id'); + client_secret = Model.attribute('client_secret'); + redirect_uri = Model.attribute('redirect_uri'); + grant_types = Model.attribute('grant_types'); + scope = Model.attribute('scope'); + user_id = Model.attribute('user_id'); + client_name = Model.attribute('client_name'); + client_icon = Model.attribute('client_icon'); + client_desc = Model.attribute('client_desc'); + client_home = Model.attribute('client_home'); +} diff --git a/js/src/common/models/Scope.js b/js/src/common/models/Scope.js deleted file mode 100644 index 2b89440..0000000 --- a/js/src/common/models/Scope.js +++ /dev/null @@ -1,11 +0,0 @@ -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/common/models/Scope.ts b/js/src/common/models/Scope.ts new file mode 100644 index 0000000..3a82654 --- /dev/null +++ b/js/src/common/models/Scope.ts @@ -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 deleted file mode 100644 index 522ba40..0000000 --- a/js/src/forum/components/oauth/AuthorizePage.js +++ /dev/null @@ -1,222 +0,0 @@ -import app from 'flarum/forum/app'; -import {extend} from 'flarum/common/extend'; -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) { - setTimeout(() => app.modal.show(LogInModal), 500); - } - - 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 &&
-
-
-
-
-

{app.forum.attribute('title')}

-

- {app.translator.trans('foskym-oauth-center.forum.authorize.access')} {this.client.client_name()} -

- -
-
- -
- - - - - -
-
- { - 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) => { - window.location.href = params.location; - }); - - // Some form handling logic here - } -} diff --git a/js/src/forum/components/oauth/AuthorizePage.tsx b/js/src/forum/components/oauth/AuthorizePage.tsx new file mode 100644 index 0000000..a0158e3 --- /dev/null +++ b/js/src/forum/components/oauth/AuthorizePage.tsx @@ -0,0 +1,212 @@ +import app from 'flarum/forum/app'; +import IndexPage from 'flarum/forum/components/IndexPage'; +import LogInModal from 'flarum/forum/components/LogInModal'; +import Placeholder from "flarum/common/components/Placeholder"; +import extractText from 'flarum/common/utils/extractText'; +import Tooltip from 'flarum/common/components/Tooltip'; +import Button from 'flarum/common/components/Button'; +import type Client from 'src/common/models/Client'; +import type Scope from 'src/common/models/Scope'; +import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; + +interface AuthorizeRequestParams { + response_type: string, + client_id: string, + redirect_uri: string, + state: string, + scope: string, +} + +export default class AuthorizePage extends IndexPage { + params: AuthorizeRequestParams | null = null; + client: Client | null = null; + clientScopes: Scope[] = []; + loading = true; + submitting = false; + + oninit(vnode: any) { + super.oninit(vnode); + + if (!app.session.user) return; + + const rawParams = m.route.param(); + + if (rawParams.client_id == null || rawParams.response_type == null || rawParams.redirect_uri == null) { + m.route.set('/'); + return; + } + + const params = { + client_id: rawParams.client_id, + response_type: rawParams.response_type, + redirect_uri: rawParams.redirect_uri, + state: rawParams.state, + scope: rawParams.scope + }; + this.params = params; + + const scopesPromise = app.store.find('oauth-scopes'); + app.store.find('oauth-clients', params.client_id).then(response => { + const clients = response as unknown as Client[]; + if (clients.length === 0) { + m.route.set('/'); + return; + } + + this.client = clients[0]; + let uris = [this.client.redirect_uri()]; + if (uris[0].indexOf(' ') > -1) { + uris = uris[0].split(' '); + } + + if ((app.forum.attribute('rhodes-island-oauth-center.require_exact_redirect_uri') && uris.indexOf(params.redirect_uri) == -1) && + (!app.forum.attribute('rhodes-island-oauth-center.allow_implicit') && params.response_type == 'token') && + (app.forum.attribute('rhodes-island-oauth-center.enforce_state') && rawParams.enforce_state == null)) { + m.route.set('/'); + } + + scopesPromise.then(scopes => { + const wantedScopes = []; + + const paramScopes = params.scope as string; + if (paramScopes) { + wantedScopes.push(...paramScopes.split(" ")); + } + + const clientScopes = this.client!!.scope(); + if (clientScopes) { + wantedScopes.push(...clientScopes.split(" ")); + } + + const finalScopes = []; + + const definedScopes = (scopes as unknown as Scope[]); + for (const definedScope of definedScopes) { + if (definedScope.is_default() || wantedScopes.indexOf(definedScope.scope()) != -1) { + finalScopes.push(definedScope); + } + } + console.log(wantedScopes, definedScopes, finalScopes); + + this.clientScopes = finalScopes; + this.loading = false; + }); + }); + } + + setTitle() { + app.setTitle(extractText(app.translator.trans('rhodes-island-oauth-center.forum.page.title.authorize'))); + app.setTitleCount(0); + } + + oncreate(vnode: any): void { + super.oncreate(vnode); + + if (!app.session.user) { + app.modal.show(LogInModal); + } + } + + view() { + if (!app.session.user) { + return {app.translator.trans("rhodes-island-oauth-center.forum.authorize.not_logged_in")}; + } + if (this.loading) { + return ; + } + + const user = app.session.user; + const clientHome = this.client!!.client_home(); + const clientIcon = this.client!!.client_icon(); + return ( +
+
+
+

{app.translator.trans("rhodes-island-oauth-center.forum.authorize.title")}

+

{app.translator.trans('rhodes-island-oauth-center.forum.authorize.description')}

+
+
+
+ +
+

{user.attribute("displayName")}

+ {user.attribute("email")} +
+
+
+
+

{app.translator.trans('rhodes-island-oauth-center.forum.authorize.app_text')}

+ {(clientHome && clientHome != "" ? + + {clientIcon && clientIcon != "" ? : } +

{this.client!!.client_name()}

+ {this.client!!.client_desc()} +
+
:
+ {clientIcon && clientIcon != "" ? : } +

{this.client!!.client_name()}

+ {this.client!!.client_desc()} +
)} +
+
+

{app.translator.trans('rhodes-island-oauth-center.forum.authorize.scope_text')}

+ { + this.clientScopes.length > 0 && this.clientScopes.map(scope => { + return
+ { + (scope.scope_icon().indexOf('fa-') > -1) ? + : + + } +
+

+ {scope.scope_name()} +

+ + { + scope.scope_desc() + .replace('{client_name}', this.client!!.client_name()) + .replace('{user}', user.attribute('displayName')) + } + +
+
+ }) + } +
+
+
+ + +
+
+
+
+ ); + } + + submit(authorized: boolean) { + this.submitting = 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: authorized, + } + }).then((params: any) => { + window.location.href = params.location; + }); + } +} diff --git a/js/src/forum/components/user/AuthorizedPage.js b/js/src/forum/components/user/AuthorizedPage.tsx similarity index 91% rename from js/src/forum/components/user/AuthorizedPage.js rename to js/src/forum/components/user/AuthorizedPage.tsx index e55e596..daeacc8 100644 --- a/js/src/forum/components/user/AuthorizedPage.js +++ b/js/src/forum/components/user/AuthorizedPage.tsx @@ -1,7 +1,9 @@ import UserPage from 'flarum/forum/components/UserPage'; + export default class AuthorizedPage extends UserPage { - oninit(vnode) { + oninit(vnode: any) { super.oninit(vnode); + this.loadUser(m.route.param('username')); } content() { diff --git a/js/src/forum/index.js b/js/src/forum/index.js deleted file mode 100644 index c6ee2f4..0000000 --- a/js/src/forum/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import app from 'flarum/forum/app'; -import {extend} from 'flarum/common/extend'; -import UserPage from 'flarum/forum/components/UserPage'; -import LinkButton from 'flarum/common/components/LinkButton'; -import AuthorizePage from "./components/oauth/AuthorizePage"; -import AuthorizedPage from "./components/user/AuthorizedPage"; -app.initializers.add('foskym/flarum-oauth-center', () => { - app.routes['oauth.authorize'] = { - path: '/oauth/authorize', - component: AuthorizePage - }; - - app.routes['user.authorized'] = { - path: '/u/:username/authorized', - component: AuthorizedPage - }; - 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'), - // ] - // ), - // -110 - // ); - } - }); -}); diff --git a/js/src/forum/index.ts b/js/src/forum/index.ts new file mode 100644 index 0000000..7260327 --- /dev/null +++ b/js/src/forum/index.ts @@ -0,0 +1,14 @@ +import app from 'flarum/forum/app'; +import AuthorizePage from "./components/oauth/AuthorizePage"; +import AuthorizedPage from "./components/user/AuthorizedPage"; +app.initializers.add('foskym/flarum-oauth-center', () => { + app.routes['oauth.authorize'] = { + path: '/oauth/authorize', + component: AuthorizePage + }; + + app.routes['user.authorized'] = { + path: '/u/:username/authorized', + component: AuthorizedPage + }; +}); diff --git a/js/tsconfig.json b/js/tsconfig.json index 504f800..7e26bc5 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -1,24 +1,24 @@ { - // Use Flarum's tsconfig as a starting point "extends": "flarum-tsconfig", - // This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder - // and also tells your Typescript server to read core's global typings for - // access to `dayjs` and `$` in the global namespace. - "include": [ - "src/**/*", - "../vendor/*/*/js/dist-typings/@types/**/*", -// -// - "@types/**/*" -], "compilerOptions": { - // This will output typings to `dist-typings` - "declarationDir": "./dist-typings", - "baseUrl": ".", - "paths": { - "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"], -// -// - } - } -} + "module": "ES2020", + "moduleResolution": "Node", + "target": "ES6", + "sourceMap": true, + "allowJs": true, + "jsx": "react", + "jsxFactory": "m", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "declarationDir": "./dist-typings", + "baseUrl": ".", + "paths": { + "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"] + } + }, + "exclude": [ + "node_modules", + "**/node_modules" + ], + "include": ["src/**/*", "../vendor/flarum/core/js/dist-typings/@types/**/*"] +} \ No newline at end of file diff --git a/js/webpack.config.js b/js/webpack.config.js index ef35ea0..ec205a7 100644 --- a/js/webpack.config.js +++ b/js/webpack.config.js @@ -1 +1,34 @@ -module.exports = require('flarum-webpack-config')(); +const config = require("flarum-webpack-config"); + +module.exports = env => { + // Determine build mode + let mode = "development"; + if (env.production) {mode = "production";} + console.log("Compilation Mode: " + mode); + let flarumConfig = config({ + resolve: { + extenstions: [".wasm", ".mjs", ".ts", ".tsx", ".js", ".json"] + }, + module: { + rules: [ + { + resource: { + and: [/\.(js|ts)$/], + not: [/node_modules/] + }, + use: [ + "babel-loader", + { + loader: "ts-loader", + options: { + transpileOnly: true + } + } + ] + } + ] + } + }); + flarumConfig.mode = mode; + return flarumConfig; +}; \ No newline at end of file diff --git a/js/yarn.lock b/js/yarn.lock new file mode 100644 index 0000000..5e9f01c Binary files /dev/null and b/js/yarn.lock differ diff --git a/less/forum/oauth.less b/less/forum/oauth.less index 55c3d0e..8b41d05 100644 --- a/less/forum/oauth.less +++ b/less/forum/oauth.less @@ -1,227 +1,132 @@ -.oauth-area { - display: block !important; - position: relative; - left: 0; - top: 0; - padding: 110px 0; - min-height: 100%; - box-sizing: border-box; -} +.AuthorizePage { + .OAuth-Header { + text-align: center; + margin-bottom: 30px; -.oauth-main { - position: relative; - width: 376px; - margin: 0 auto; - box-sizing: border-box; - box-shadow: 0px 0px 15px 0px #bdbdbd; - border-radius: 12px; -} + h2 { + font-size: 3em; + margin-bottom: .1em; + } -.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 + p { + font-size: 1.2em; + margin-top: .1em; + } } - .oauth-main { - width: 300px - } + .OAuth-Body { + max-width: 60%; + margin: 0 auto; - .oauth-box { - padding: 10px - } + .OAuth-User { + display: flex; + align-items: center; + justify-content: center; - .oauth-main::before { - backdrop-filter: none; - } + .OAuth-User-Avatar { + width: 48px; + height: 48px; + border-radius: 50%; + } - .oauth-header { - // background-color: #fff; - } + .OAuth-User-Info { + margin-left: 10px; + white-space: nowrap; - body { - margin: 0 + h4 { + margin: 0; + } + + small { + opacity: .5; + } + } + } + + .OAuth-Details { + display: flex; + + .OAuth-App { + min-width: 300px; + } + + .OAuth-Scopes { + flex: 1; + } + } + + .OAuth-App, .OAuth-Scopes { + margin: 20px 0; + + > p { + font-weight: bold; + font-size: 1.2em; + } + } + + .OAuth-App { + text-align: center; + + .OAuth-App-Info { + color: @text-color; + display: inline-block; + text-decoration: none; + + .OAuth-App-Icon { + width: 64px; + height: 64px; + } + + p { + font-weight: bolder; + font-size: 2em; + margin-bottom: 0; + } + small { + font-size: 1.2em; + opacity: .5; + } + } + } + + .OAuth-Scopes { + text-align: center; + + .OAuth-Scope { + display: flex; + text-align: left; + + .OAuth-ScopeIcon { + width: 40px; + height: 40px; + } + + .OAuth-ScopeDetail { + margin-left: 10px; + + h4 { + margin: 0; + } + } + } + } + + .OAuth-Action { + margin: 30px 0; + } } } -@media screen and (max-width: 600px) { - .oauth-area { - padding-top: 0 +@media screen and (max-width: @screen-phone-max) { + .AuthorizePage .OAuth-Body { + max-width: 100%; + + .OAuth-Details { + flex-direction: column; + + .OAuth-Scopes { + padding: 0 30px; + } + } } - - 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; - font-size: 12px; -} - -.oauth-scope-body small { - font-weight: 500; - font-size: 12px; - color: #aaa; -} +} \ No newline at end of file diff --git a/locale/en.yml b/locale/en.yml index 064e788..e08603c 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -1,4 +1,4 @@ -foskym-oauth-center: +rhodes-island-oauth-center: admin: permission: use_oauth: Use OAuth @@ -20,8 +20,12 @@ foskym-oauth-center: client_name: Name client_desc: Description client_icon: Icon - client_home: HomePage + client_home: Homepage + actions: Actions add_button: Add Client + edit_modal_title_add: New application + edit_modal_title_edit: Edit application + edit_modal_save: Save scopes: scope: Scope resource_path: Resource Path @@ -31,6 +35,7 @@ foskym-oauth-center: scope_icon: Icon scope_desc: Description add_button: Add Scope + save: Save scopes forum: page: @@ -39,6 +44,11 @@ foskym-oauth-center: label: authorized: Authorized Logs authorize: - access: Access to + title: Third party application authorization + description: Authorize third party application to use your identity + app_text: You are about to authorize + to_app_homepage: Go to {name}'s homepage + scope_text: To use the following privileges agree: Agree deny: Deny + not_logged_in: Please continue after log in diff --git a/locale/zh-Hans.yml b/locale/zh-Hans.yml index de3d7b8..01b2926 100644 --- a/locale/zh-Hans.yml +++ b/locale/zh-Hans.yml @@ -1,4 +1,4 @@ -foskym-oauth-center: +rhodes-island-oauth-center: admin: permission: use_oauth: 使用 OAuth 授权 @@ -21,7 +21,11 @@ foskym-oauth-center: client_desc: 描述 client_icon: 图标 client_home: 主页 + actions: 操作 add_button: 添加应用 + edit_modal_title_add: 新建应用 + edit_modal_title_edit: 编辑应用 + edit_modal_save: 保存应用 scopes: scope: 权限标识 resource_path: 资源路径 @@ -31,6 +35,7 @@ foskym-oauth-center: scope_icon: 图标 scope_desc: 描述 add_button: 添加权限 + save: 保存权限 forum: page: @@ -39,6 +44,11 @@ foskym-oauth-center: label: authorized: 授权记录 authorize: - access: 授权访问 + title: 第三方应用授权 + description: 授权第三方应用以你的身份使用部分权限 + app_text: 你即将授权 + to_app_homepage: 前往 {name} 的主页 + scope_text: 获得以下权限 agree: 授权 deny: 拒绝 + not_logged_in: 请登录后再继续操作 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 index bda0bc2..205a982 100644 --- 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 @@ -20,7 +20,7 @@ return [ 'scope' => 'user.read', 'resource_path' => '/api/user', 'method' => 'GET', - 'is_default' => 1, + 'is_default' => true, 'scope_name' => '获取用户信息', 'scope_icon' => 'fas fa-user', 'scope_desc' => '访问该用户({user})的个人信息等', diff --git a/src/Api/Controller/CreateClientController.php b/src/Api/Controller/CreateClientController.php index 866b73b..14863c2 100644 --- a/src/Api/Controller/CreateClientController.php +++ b/src/Api/Controller/CreateClientController.php @@ -20,10 +20,17 @@ class CreateClientController extends AbstractCreateController $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, - ]); + $validAttrs = [ + 'user_id' => $actor->id + ]; + + collect(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'client_name', 'client_desc', 'client_icon', 'client_home']) + ->each(function (string $attribute) use (&$validAttrs, $attributes) { + if (($val = Arr::get($attributes, $attribute)) !== null) { + $validAttrs[$attribute] = $val; + } + }); + + return Client::create($validAttrs); } } diff --git a/src/Api/Controller/ListScopeController.php b/src/Api/Controller/ListScopeController.php index d9f38e3..1597d16 100644 --- a/src/Api/Controller/ListScopeController.php +++ b/src/Api/Controller/ListScopeController.php @@ -21,7 +21,7 @@ class ListScopeController extends AbstractListController $actor->assertAdmin(); } catch (\Exception $e) { $actor->assertRegistered(); - if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) { + if (!$actor->hasPermission('rhodes-island-oauth-center.use-oauth')) { return []; } $this->serializer = ScopeUserSerializer::class; diff --git a/src/Api/Controller/ShowClientController.php b/src/Api/Controller/ShowClientController.php index 9b4ebe5..c904992 100644 --- a/src/Api/Controller/ShowClientController.php +++ b/src/Api/Controller/ShowClientController.php @@ -20,7 +20,7 @@ class ShowClientController extends AbstractListController $actor = RequestUtil::getActor($request); $actor->assertRegistered(); - if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) { + if (!$actor->hasPermission('rhodes-island-oauth-center.use-oauth')) { return []; } diff --git a/src/Controllers/AuthorizeController.php b/src/Controllers/AuthorizeController.php index 2a57c5e..28d5eb4 100644 --- a/src/Controllers/AuthorizeController.php +++ b/src/Controllers/AuthorizeController.php @@ -33,7 +33,7 @@ class AuthorizeController implements RequestHandlerInterface $actor = RequestUtil::getActor($request); $actor->assertRegistered(); - if (!$actor->hasPermission('foskym-oauth-center.use-oauth')) { + if (!$actor->hasPermission('rhodes-island-oauth-center.use-oauth')) { return new JsonResponse([ 'error' => 'no_permission', 'error_description' => 'Don\'t have the permissions of oauth' ]); } diff --git a/src/Middlewares/UserCredentialsMiddleware.php b/src/Middlewares/UserCredentialsMiddleware.php index 5273a5d..c0428ab 100644 --- a/src/Middlewares/UserCredentialsMiddleware.php +++ b/src/Middlewares/UserCredentialsMiddleware.php @@ -22,7 +22,7 @@ class UserCredentialsMiddleware implements MiddlewareInterface $path = $request->getUri()->getPath(); if (in_array($path, $uri) && Arr::get($request->getParsedBody(), 'grant_type', '') === 'password') { if ($user = User::where('username', Arr::get($request->getParsedBody(), 'username', ''))->first()) { - if (!$user->hasPermission('foskym-oauth-center.use-oauth')) { + if (!$user->hasPermission('rhodes-island-oauth-center.use-oauth')) { return new JsonResponse([ 'error' => 'no_permission', 'error_description' => 'Don\'t have the permissions of oauth' ]); } } diff --git a/src/OAuth.php b/src/OAuth.php index 66c67a8..60da284 100644 --- a/src/OAuth.php +++ b/src/OAuth.php @@ -45,10 +45,10 @@ class OAuth { $storage = new Storage; $server = new Server($storage, array( - 'allow_implicit' => $this->settings->get('foskym-oauth-center.allow_implicit') == "1", - 'enforce_state' => $this->settings->get('foskym-oauth-center.enforce_state') == "1", - 'require_exact_redirect_uri' => $this->settings->get('foskym-oauth-center.require_exact_redirect_uri') == "1", - 'access_lifetime' => $this->settings->get('foskym-oauth-center.access_lifetime') == "" ? 3600 : $this->settings->get('foskym-oauth-center.access_lifetime'), + 'allow_implicit' => $this->settings->get('rhodes-island-oauth-center.allow_implicit') == "1", + 'enforce_state' => $this->settings->get('rhodes-island-oauth-center.enforce_state') == "1", + 'require_exact_redirect_uri' => $this->settings->get('rhodes-island-oauth-center.require_exact_redirect_uri') == "1", + 'access_lifetime' => $this->settings->get('rhodes-island-oauth-center.access_lifetime') == "" ? 3600 : $this->settings->get('rhodes-island-oauth-center.access_lifetime'), )); $server->addGrantType(new AuthorizationCode($storage)); $server->addGrantType(new ClientCredentials($storage));