rewrite: frontend

This commit is contained in:
2024-02-25 04:55:37 +08:00
parent bd4402f98c
commit 64d36a111b
Signed by: LuoRain
GPG key ID: 16B4D3D5372966A6
44 changed files with 950 additions and 926 deletions

View file

@ -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": {

View file

@ -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),

2
js/.gitignore vendored
View file

@ -7,3 +7,5 @@
!.yarn/versions
node_modules
dist

BIN
js/dist/admin.js generated vendored

Binary file not shown.

BIN
js/dist/admin.js.map generated vendored

Binary file not shown.

BIN
js/dist/forum.js generated vendored

Binary file not shown.

BIN
js/dist/forum.js.map generated vendored

Binary file not shown.

BIN
js/package-lock.json generated

Binary file not shown.

View file

@ -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"
}
}

View file

@ -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<Client> {
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"));
}
}

View file

@ -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<T extends Model> 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 (
<div className="Modal-body">
{this.fields().map(field => <div className="Form-group">
<label for={field}>{this.translateFieldName(field)}</label>
<input
className="FormControl"
name={field}
type="text"
bidi={this.fieldValues[field]}
disabled={this.saving}
/>
</div>)}
<Button className="Button Button--primary Button--block" onclick={this.save.bind(this)} disabled={this.saving} loading={this.saving}>{app.translator.trans("rhodes-island-oauth-center.admin.clients.edit_modal_save")}</Button>
</div>
);
}
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();
});
}
}

View file

@ -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 <ClientsPage />
} else if (page == 'scopes') {

View file

@ -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
);
});

36
js/src/admin/index.ts Normal file
View file

@ -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"
});
});

View file

@ -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 (
<div class={"OAuthCenter-clientsPage"}>
{
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')))),
]),
]),
])
}
</div>
);
}
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,
});
}
}

View file

@ -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 (
<div class="OAuthCenter-clientsPage">
{(this.loading ? <LoadingIndicator></LoadingIndicator> : <div className="Form-group">
<table>
<thead>
<tr>
{this.tableFields.map(key => <th>{app.translator.trans(this.translationPrefix + key)}</th>)}
<th>{app.translator.trans(this.translationPrefix + "actions")}</th>
</tr>
</thead>
<tbody>
{this.clients.map((client: any) => {
const clientSaving = this.loadingClients.indexOf(client) != -1;
return <tr>
{this.tableFields.map(key => <td>{client[key]()}</td>)}
<td>
<Button className="Button Button--icon" icon="fas fa-pen" disabled={clientSaving} onclick={() => {
app.modal.show(EditApplicationModal, {
model: client
});
}}></Button>
&nbsp;
<Button className="Button Button--icon" icon="fas fa-times" disabled={clientSaving} onclick={() => {
this.loadingClients.push(client);
(client as Client).delete()
.then(() => this.clients.splice(this.clients.indexOf(client), 1))
.finally(() => this.loadingClients.splice(this.loadingClients.indexOf(client), 1))
.finally(m.redraw);
}}></Button>
</td>
</tr>
}
)}
</tbody>
</table>
<Button className="Button Button--block" onclick={() => {
app.modal.show(EditApplicationModal, {
createCallback: this.refresh.bind(this)
});
}}>{app.translator.trans(this.translationPrefix + "add_button")}</Button>
</div>)}
</div>
);
}
}

View file

@ -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 (
<div>
<form onsubmit={this.onsubmit.bind(this)} className="BasicsPage">
{FieldSet.component({}, [
<div style="height: 5px;"></div>,
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({}, [
<div style="height: 5px;"></div>,
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({}, [
<div style="height: 5px;"></div>,
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')),
])}
<hr/>
{FieldSet.component({}, [
<input className="FormControl" bidi={this.values['foskym-oauth-center.access_lifetime']}
placeholder={app.translator.trans('foskym-oauth-center.admin.settings.access_lifetime')} required/>,
<div className="helpText">
{app.translator.trans('foskym-oauth-center.admin.settings.access_lifetime')}
</div>,
Button.component({
type: 'submit',
className: 'Button Button--primary',
loading: this.saving
}, app.translator.trans('core.admin.settings.submit_button'))
])}
</form>
</div>
);
}
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();
});
}
}

View file

@ -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 (
<div className="ExtensionPage-settings">
<div className="container">
<div className="Form">
{settings!!.map(this.buildSettingComponent.bind(this))}
<div className="Form-group">{this.submitButton()}</div>
</div>
</div>
</div>
);
}
}

View file

@ -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 (
<div class={"OAuthCenter-scopesPage"}>
{
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')))),
]),
]),
])
}
</div>
);
}
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,
});
}
}

View file

@ -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) => <Select options={{
"GET": "GET",
"POST": "POST",
"PUT": "PUT",
"DELETE": "DELETE",
"PATCH": "PATCH"
}} disabled={disabled} value={stream()} onchange={(val: boolean) => stream(val)}></Select>,
"is_default": (stream, disabled) => <Checkbox disabled={disabled} state={stream() === 1} onchange={(val: boolean) => stream(val ? 1 : 0)}></Checkbox>
}
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 (
<div class="OAuthCenter-scopesPage">
{(this.loading ? <LoadingIndicator></LoadingIndicator> : <div className="Form-group">
<table>
<thead>
<tr>
{this.fields.map(key => <th>{app.translator.trans(this.translationPrefix + key)}</th>)}
</tr>
</thead>
<tbody>
{this.bufferedValues.map((scope) => <tr style={scope.markDeletion ? "opacity: .5;" : ""}>
{this.fields.map((key) => <td>
{(this.components[key] && this.components[key]((scope as any)[key], this.saving)) ?? <input className="FormControl" type="text" bidi={(scope as any)[key]} disabled={this.saving}></input>}
</td>)}
<td>
<Button className="Button Button--icon" disabled={this.saving} icon={"fas fa-" + (scope.markDeletion ? "undo" : "times")} onclick={() => {
scope.markDeletion = !scope.markDeletion;
}}></Button>
</td>
</tr>)}
</tbody>
</table>
<Button className="Button Button--block" disabled={this.saving} onclick={() => {
const eScope: EditingScope = {modifiedFields: []};
for (const field of this.fields) {
const stream = Stream();
(eScope as any)[field] = stream;
}
eScope.scope("scope." + randomString(4));
eScope.method("GET");
this.bufferedValues.push(eScope);
}}>{app.translator.trans(this.translationPrefix + 'add_button')}</Button>
<br/>
<Button className="Button Button--primary" onclick={this.save.bind(this)} loading={this.saving}>{app.translator.trans(this.translationPrefix + "save")}</Button>
</div>)}
</div>
);
}
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();
});
}
}
}

10
js/src/admin/util.ts Normal file
View file

@ -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;
}

View file

@ -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');
}

View file

@ -0,0 +1,14 @@
import Model from 'flarum/common/Model';
export default class Client extends Model {
client_id = Model.attribute<string>('client_id');
client_secret = Model.attribute<string>('client_secret');
redirect_uri = Model.attribute<string>('redirect_uri');
grant_types = Model.attribute<string>('grant_types');
scope = Model.attribute<string>('scope');
user_id = Model.attribute<number>('user_id');
client_name = Model.attribute<string>('client_name');
client_icon = Model.attribute<string>('client_icon');
client_desc = Model.attribute<string>('client_desc');
client_home = Model.attribute<string>('client_home');
}

View file

@ -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');
}

View file

@ -0,0 +1,11 @@
import Model from 'flarum/common/Model';
export default class Scope extends Model {
scope = Model.attribute<string>('scope');
resource_path = Model.attribute<string>('resource_path');
method = Model.attribute<string>('method');
is_default = Model.attribute<boolean>('is_default');
scope_name = Model.attribute<string>('scope_name');
scope_icon = Model.attribute<string>('scope_icon');
scope_desc = Model.attribute<string>('scope_desc');
}

View file

@ -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 && <div className="AuthorizePage">
<div className="container">
<div class="oauth-area">
<div class="oauth-main">
<div class="oauth-box oauth-header">
<h2>{app.forum.attribute('title')}</h2>
<p>
{app.translator.trans('foskym-oauth-center.forum.authorize.access')} <a
href={this.client.client_home()} target="_blank">{this.client.client_name()}</a>
</p>
</div>
<div class="oauth-box oauth-body">
<div class="oauth-top">
<img src={app.forum.attribute('faviconUrl')}/>
<i class="fas fa-exchange-alt fa-2x"></i>
<Tooltip text={this.client.client_desc()}>
<img src={this.client.client_icon()}/>
</Tooltip>
</div>
<div class="oauth-scope-area">
{
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 (
<div class="oauth-scope">
<div class="oauth-scope-left">
{
(scope_info.scope_icon().indexOf('fa-') > -1) ?
<i class={"oauth-scope-object fa-2x " + scope_info.scope_icon()}
style="margin-left:2px;color:#000"></i> :
<img class="oauth-scope-object" src={scope_info.scope_icon()} style="width:32px"/>
}
</div>
<div class="oauth-scope-body">
<h6 class="oauth-scope-heading">
{scope_info.scope_name()}
</h6>
<small>
{
scope_info.scope_desc()
.replace('{client_name}', this.client.client_name())
.replace('{user}', app.session.user.attribute('displayName'))
}
</small>
</div>
</div>
);
})
}
</div>
<form class="oauth-form" method="post" id="form" onsubmit={this.onsubmit.bind(this)}>
{/* <input type="hidden" name="response_type" value={this.params.response_type}/>
<input type="hidden" name="client_id" value={this.params.client_id}/>
<input type="hidden" name="redirect_uri"
value={this.params.redirect_uri}/>
<input type="hidden" name="state" value={this.params.state}/>
<input type="hidden" name="scope" value={this.params.scope}/>*/}
<input type="hidden" name="is_authorized" value={this.is_authorized}/>
<div style="display: flex; margin-top: 15px" class="oauth-form-item">
<Button className="Button" type="submit" style="width: 50%;" onclick={this.deny.bind(this)}
loading={this.submit_loading}>
{app.translator.trans('foskym-oauth-center.forum.authorize.deny')}
</Button>
<Button className="Button Button--primary" type="submit" style="width: 50%;"
onclick={this.agree.bind(this)} loading={this.submit_loading}>
{app.translator.trans('foskym-oauth-center.forum.authorize.agree')}
</Button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
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
}
}

View file

@ -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 <Placeholder>{app.translator.trans("rhodes-island-oauth-center.forum.authorize.not_logged_in")}</Placeholder>;
}
if (this.loading) {
return <LoadingIndicator></LoadingIndicator>;
}
const user = app.session.user;
const clientHome = this.client!!.client_home();
const clientIcon = this.client!!.client_icon();
return (
<div className="AuthorizePage">
<div className="container">
<div class="OAuth-Header">
<h2>{app.translator.trans("rhodes-island-oauth-center.forum.authorize.title")}</h2>
<p>{app.translator.trans('rhodes-island-oauth-center.forum.authorize.description')}</p>
</div>
<div class="OAuth-Body">
<div className="OAuth-User">
<img className="OAuth-User-Avatar" src={user.attribute("avatarUrl")}/>
<div className="OAuth-User-Info">
<h4>{user.attribute("displayName")}</h4>
<small>{user.attribute("email")}</small>
</div>
</div>
<div className="OAuth-Details">
<div className="OAuth-App">
<p>{app.translator.trans('rhodes-island-oauth-center.forum.authorize.app_text')}</p>
{(clientHome && clientHome != "" ? <Tooltip text={app.translator.trans('rhodes-island-oauth-center.forum.authorize.to_app_homepage', { name: this.client!!.client_name() })}>
<a href={clientHome} target="_blank" className="OAuth-App-Info">
{clientIcon && clientIcon != "" ? <img src={clientIcon} class="OAuth-App-Icon"/> : <i class="OAuth-App-Icon fas fa-5x fa-cube"></i>}
<p>{this.client!!.client_name()}</p>
<small>{this.client!!.client_desc()}</small>
</a>
</Tooltip> : <div className="OAuth-App-Info">
{clientIcon && clientIcon != "" ? <img src={clientIcon} class="OAuth-App-Icon"/> : <i class="OAuth-App-Icon fas fa-5x fa-cube"></i>}
<p>{this.client!!.client_name()}</p>
<small>{this.client!!.client_desc()}</small>
</div>)}
</div>
<div class="OAuth-Scopes">
<p>{app.translator.trans('rhodes-island-oauth-center.forum.authorize.scope_text')}</p>
{
this.clientScopes.length > 0 && this.clientScopes.map(scope => {
return <div class="OAuth-Scope">
{
(scope.scope_icon().indexOf('fa-') > -1) ?
<i class={"OAuth-ScopeIcon fa-3x " + scope.scope_icon()}
style="margin-left:2px;color:#000"></i> :
<img class="OAuth-ScopeIcon" src={scope.scope_icon()} />
}
<div class="OAuth-ScopeDetail">
<h4>
{scope.scope_name()}
</h4>
<small>
{
scope.scope_desc()
.replace('{client_name}', this.client!!.client_name())
.replace('{user}', user.attribute('displayName'))
}
</small>
</div>
</div>
})
}
</div>
</div>
<div class="OAuth-Action">
<Button className="Button Button--primary Button--block" onclick={() => this.submit(true)}
loading={this.submitting}>
{app.translator.trans('rhodes-island-oauth-center.forum.authorize.agree')}
</Button>
<Button className="Button Button--link Button--block" onclick={() => this.submit(false)}
loading={this.submitting}>
{app.translator.trans('rhodes-island-oauth-center.forum.authorize.deny')}
</Button>
</div>
</div>
</div>
</div>
);
}
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;
});
}
}

View file

@ -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() {

View file

@ -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
// );
}
});
});

14
js/src/forum/index.ts Normal file
View file

@ -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
};
});

View file

@ -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/**/*",
// <CUSTOM-1>
// </CUSTOM-1>
"@types/**/*"
],
"compilerOptions": {
// This will output typings to `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/*"],
// <CUSTOM-2>
// </CUSTOM-2>
}
"flarum/*": ["../vendor/flarum/core/js/dist-typings/*"]
}
},
"exclude": [
"node_modules",
"**/node_modules"
],
"include": ["src/**/*", "../vendor/flarum/core/js/dist-typings/@types/**/*"]
}

View file

@ -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;
};

BIN
js/yarn.lock Normal file

Binary file not shown.

View file

@ -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;
h2 {
font-size: 3em;
margin-bottom: .1em;
}
.oauth-main {
position: relative;
width: 376px;
p {
font-size: 1.2em;
margin-top: .1em;
}
}
.OAuth-Body {
max-width: 60%;
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-User {
display: flex;
align-items: center;
justify-content: center;
.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;
.OAuth-User-Avatar {
width: 48px;
height: 48px;
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-User-Info {
margin-left: 10px;
white-space: nowrap;
h4 {
margin: 0;
}
.oauth-top i {
top: -24px;
position: relative;
padding-left: 10px;
padding-right: 10px;
color: #111;
small {
opacity: .5;
}
}
}
.oauth-scope-area {
padding-top: 10px;
padding-bottom: 10px;
overflow: auto;
max-height: 350px;
position: relative;
.OAuth-Details {
display: flex;
.OAuth-App {
min-width: 300px;
}
.oauth-scope {
margin-top: 15px;
.OAuth-Scopes {
flex: 1;
}
}
.oauth-scope:first-child {
margin-top: 0;
.OAuth-App, .OAuth-Scopes {
margin: 20px 0;
> p {
font-weight: bold;
font-size: 1.2em;
}
}
.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;
.OAuth-App {
text-align: center;
.OAuth-App-Info {
color: @text-color;
display: inline-block;
text-decoration: none;
.OAuth-App-Icon {
width: 64px;
height: 64px;
}
img.oauth-scope-object {
display: block;
vertical-align: middle;
border: 0;
width: 32px;
height: 32px;
p {
font-weight: bolder;
font-size: 2em;
margin-bottom: 0;
}
small {
font-size: 1.2em;
opacity: .5;
}
}
}
.oauth-scope-body {
width: 10000px;
padding-left: 8px;
.OAuth-Scopes {
text-align: center;
.OAuth-Scope {
display: flex;
text-align: left;
.OAuth-ScopeIcon {
width: 40px;
height: 40px;
}
.oauth-scope-heading {
margin-top: 0;
font-weight: 800;
color: #382e2e;
margin-block-end: 0;
font-size: 12px;
.OAuth-ScopeDetail {
margin-left: 10px;
h4 {
margin: 0;
}
}
}
}
.oauth-scope-body small {
font-weight: 500;
font-size: 12px;
color: #aaa;
.OAuth-Action {
margin: 30px 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;
}
}
}
}

View file

@ -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

View file

@ -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: 请登录后再继续操作

View file

@ -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})的个人信息等',

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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 [];
}

View file

@ -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' ]);
}

View file

@ -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' ]);
}
}

View file

@ -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));