Implemented partial UI for device authorisation.

This commit is contained in:
flash 2024-07-30 00:16:42 +00:00
parent 61dad487f6
commit ffb0cb96df
33 changed files with 1181 additions and 185 deletions

44
assets/hanyuu.js/csrfp.js Normal file
View file

@ -0,0 +1,44 @@
const HanyuuCSRFP = (() => {
let elem;
const getElement = () => {
if(elem === undefined)
elem = document.querySelector('meta[name="csrfp-token"]');
return elem;
};
const getToken = () => {
const elem = getElement();
return typeof elem.content === 'string' ? elem.content : '';
};
const setToken = token => {
if(typeof token !== 'string')
throw 'token must be a string';
let elem = getElement();
if(typeof elem.content === 'string')
elem.content = token;
else {
elem = document.createElement('meta');
elem.name = 'csrfp-token';
elem.content = token;
document.head.appendChild(elem);
}
};
return {
getToken: getToken,
setToken: setToken,
setFromHeaders: result => {
if(typeof result.headers !== 'function')
throw 'result.headers is not a function';
const headers = result.headers();
if(!(headers instanceof Map))
throw 'result of result.headers does not return a map';
if(headers.has('x-csrfp-token'))
setToken(headers.get('x-csrfp-token'));
},
};
})();

116
assets/hanyuu.js/html.js Normal file
View file

@ -0,0 +1,116 @@
const $e = function(info, attrs, child, created) {
info = info || {};
if(typeof info === 'string') {
info = {tag: info};
if(attrs)
info.attrs = attrs;
if(child)
info.child = child;
if(created)
info.created = created;
}
const elem = document.createElement(info.tag || 'div');
if(info.attrs) {
const attrs = info.attrs;
for(let key in attrs) {
const attr = attrs[key];
if(attr === undefined || attr === null)
continue;
switch(typeof attr) {
case 'function':
if(key.substring(0, 2) === 'on')
key = key.substring(2).toLowerCase();
elem.addEventListener(key, attr);
break;
case 'object':
if(attr instanceof Array) {
if(key === 'class')
key = 'classList';
const prop = elem[key];
let addFunc = null;
if(prop instanceof Array)
addFunc = prop.push.bind(prop);
else if(prop instanceof DOMTokenList)
addFunc = prop.add.bind(prop);
if(addFunc !== null) {
for(let j = 0; j < attr.length; ++j)
addFunc(attr[j]);
} else {
if(key === 'classList')
key = 'class';
elem.setAttribute(key, attr.toString());
}
} else {
for(const attrKey in attr)
elem[key][attrKey] = attr[attrKey];
}
break;
case 'boolean':
if(attr)
elem.setAttribute(key, '');
break;
default:
if(key === 'className')
key = 'class';
elem.setAttribute(key, attr.toString());
break;
}
}
}
if(info.child) {
let children = info.child;
if(!Array.isArray(children))
children = [children];
for(const child of children) {
switch(typeof child) {
case 'string':
elem.appendChild(document.createTextNode(child));
break;
case 'object':
if(child instanceof Element) {
elem.appendChild(child);
} else if('element' in child) {
const childElem = child.element;
if(childElem instanceof Element)
elem.appendChild(childElem);
else
elem.appendChild($e(child));
} else if('getElement' in child) {
const childElem = child.getElement();
if(childElem instanceof Element)
elem.appendChild(childElem);
else
elem.appendChild($e(child));
} else {
elem.appendChild($e(child));
}
break;
default:
elem.appendChild(document.createTextNode(child.toString()));
break;
}
}
}
if(info.created)
info.created(elem);
return elem;
};
const $er = (type, props, ...children) => $e({ tag: type, attrs: props, child: children });

3
assets/hanyuu.js/main.js Normal file
View file

@ -0,0 +1,3 @@
#include csrfp.js
#include html.js
#include xhr.js

114
assets/hanyuu.js/xhr.js Normal file
View file

@ -0,0 +1,114 @@
const $x = (function() {
const send = function(method, url, options, body) {
if(options === undefined)
options = {};
else if(typeof options !== 'object')
throw 'options must be undefined or an object';
const xhr = new XMLHttpRequest;
const requestHeaders = new Map;
if('headers' in options && typeof options.headers === 'object')
for(const name in options.headers)
if(options.headers.hasOwnProperty(name))
requestHeaders.set(name.toLowerCase(), options.headers[name]);
if(typeof options.download === 'function') {
xhr.onloadstart = ev => options.download(ev);
xhr.onprogress = ev => options.download(ev);
xhr.onloadend = ev => options.download(ev);
}
if(typeof options.upload === 'function') {
xhr.upload.onloadstart = ev => options.upload(ev);
xhr.upload.onprogress = ev => options.upload(ev);
xhr.upload.onloadend = ev => options.upload(ev);
}
if(options.authed)
xhr.withCredentials = true;
if(typeof options.timeout === 'number')
xhr.timeout = options.timeout;
if(typeof options.type === 'string')
xhr.responseType = options.type;
if(typeof options.abort === 'function')
options.abort(() => xhr.abort());
if(typeof options.xhr === 'function')
options.xhr(() => xhr);
if(typeof body === 'object') {
if(body instanceof URLSearchParams) {
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
} else if(body instanceof FormData) {
// content-type is implicitly set
} else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
if(!requestHeaders.has('content-type'))
requestHeaders.set('content-type', 'application/octet-stream');
} else if(!requestHeaders.has('content-type')) {
const bodyParts = [];
for(const name in body)
if(body.hasOwnProperty(name))
bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
body = bodyParts.join('&');
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
}
}
return new Promise((resolve, reject) => {
let responseHeaders = undefined;
xhr.onload = ev => resolve({
status: xhr.status,
body: () => xhr.response,
text: () => xhr.responseText,
headers: () => {
if(responseHeaders !== undefined)
return responseHeaders;
responseHeaders = new Map;
const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
for(const name in raw)
if(raw.hasOwnProperty(name)) {
const parts = raw[name].split(': ');
responseHeaders.set(parts.shift(), parts.join(': '));
}
return responseHeaders;
},
xhr: xhr,
ev: ev,
});
xhr.onabort = ev => reject({
abort: true,
xhr: xhr,
ev: ev,
});
xhr.onerror = ev => reject({
abort: false,
xhr: xhr,
ev: ev,
});
xhr.open(method, url);
for(const [name, value] of requestHeaders)
xhr.setRequestHeader(name, value);
xhr.send(body);
});
};
return {
send: send,
get: (url, options, body) => send('GET', url, options, body),
post: (url, options, body) => send('POST', url, options, body),
delete: (url, options, body) => send('DELETE', url, options, body),
patch: (url, options, body) => send('PATCH', url, options, body),
put: (url, options, body) => send('PUT', url, options, body),
};
})();

View file

@ -0,0 +1,35 @@
.oauth2-approval {
display: flex;
flex-direction: column;
align-items: center;
}
.oauth2-approval-icon {
flex: 0 0 auto;
background-color: #fff;
width: 40px;
height: 40px;
margin: 10px;
}
.oauth2-approval-icon-approved {
mask: url('/images/circle-check-solid.svg') no-repeat center;
}
.oauth2-approval-icon-denied {
mask: url('/images/circle-xmark-solid.svg') no-repeat center;
}
.oauth2-approval-header {
text-align: center;
font-size: 1.2em;
line-height: 1.5em;
}
.oauth2-approval-text {
width: 100%;
}
.oauth2-approval-text p {
margin: .5em 0;
font-size: .8em;
line-height: 1.5em;
}

View file

@ -7,6 +7,14 @@
margin: .5em 0;
}
.oauth2-authorise-device {
font-size: .8em;
line-height: 1.4em;
}
.oauth2-authorise-device p {
margin: .5em 0;
}
.oauth2-authorise-buttons {
margin-top: 10px;
display: flex;
@ -30,6 +38,13 @@
align-items: center;
justify-content: center;
text-decoration: none;
color: #8559a5;
border-color: #8559a5;
}
.oauth2-authorise-button:hover,
.oauth2-authorise-button:focus {
color: #191919;
background-color: #8559a5;
}
.oauth2-authorise-button[disabled] {

View file

@ -0,0 +1,43 @@
.oauth2-devicehead {
display: flex;
align-items: center;
}
.oauth2-devicehead-icon {
flex: 0 0 auto;
background-color: #fff;
mask: url('/images/mobile-screen-solid.svg') no-repeat center;
width: 40px;
height: 40px;
margin: 10px;
}
.oauth2-devicehead-text {
font-size: 1.8em;
line-height: 1.4em;
}
.oauth2-device-form {
display: flex;
justify-content: center;
margin: 10px;
}
.oauth2-device-code {
font-size: 1.4em;
border: 1px solid #222;
padding: 5px 10px;
background: #222;
color: #fff;
border-radius: 2px;
box-shadow: inset 0 0 4px #111;
transition: border-color .2s;
text-align: center;
font-family: var(--font-monospace);
min-width: 0;
max-width: 200px;
width: 100%;
}
.oauth2-device-code:focus {
border-color: #8559a5;
}

View file

@ -0,0 +1,36 @@
.oauth2-loading {
display: flex;
justify-content: center;
flex-direction: column;
min-height: 200px;
}
.oauth2-loading-frame {
display: flex;
justify-content: center;
flex: 0 0 auto;
}
.oauth2-loading-icon {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 2px;
margin: 20px;
}
.oauth2-loading-icon-block {
background: #fff;
width: 20px;
height: 20px;
}
.oauth2-loading-icon-block-hidden {
opacity: 0;
}
.oauth2-loading-text {
text-align: center;
font-size: 1.2em;
line-height: 1.5em;
}

View file

@ -35,10 +35,13 @@
margin: 10px;
}
@include loading.css;
@include banner.css;
@include error.css;
@include login.css;
@include device.css;
@include userhead.css;
@include appinfo.css;
@include scope.css;
@include authorise.css;
@include approval.css;

View file

@ -0,0 +1,30 @@
const HanyuuOAuth2AppInfoLink = function(info) {
const element = <a href={info.uri} target="_blank" rel="noopener noreferrer" class="oauth2-appinfo-link" title={info.title}>
<div class="oauth2-appinfo-link-icon oauth2-appinfo-link-icon-globe"></div>
<div class="oauth2-appinfo-link-text">{info.display}</div>
</a>;
return {
get element() { return element; },
};
};
const HanyuuOAuth2AppInfo = function(info) {
const linksElem = <div class="oauth2-appinfo-links"/>;
if(Array.isArray(info.links))
for(const link of info.links)
linksElem.appendChild((new HanyuuOAuth2AppInfoLink(link)).element);
// TODO: author should be listed
const element = <div class="oauth2-appinfo">
<div class="oauth2-appinfo-name">{info.name}</div>
{linksElem}
<div class="oauth2-appinfo-summary">
<p>{info.summary}</p>
</div>
</div>;
return {
get element() { return element; },
};
};

View file

@ -0,0 +1,29 @@
const HanyuuOAuth2AppScopeEntry = function(text) {
const element = <div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">{text}</div>
</div>;
return {
get element() { return element; },
};
};
const HanyuuOAuth2AppScopeList = function() {
// TODO: actual scope listing
const permsElem = <div class="oauth2-scope-perms">
{new HanyuuOAuth2AppScopeEntry('Do anything because I have not made up scopes yet.')}
{new HanyuuOAuth2AppScopeEntry('Eat soup.')}
{new HanyuuOAuth2AppScopeEntry('These are placeholders.')}
{new HanyuuOAuth2AppScopeEntry('This one is really long because I want to test wrapping and how the chevron icon thing will handle it so there will be a lot of text here, the app will not be gaining anything from it but yeah sometimes you just need to explode seventy times.')}
</div>;
const element = <div class="oauth2-scope">
<div class="oauth2-scope-header">This application will be able to:</div>
{permsElem}
</div>;
return {
get element() { return element; },
};
};

View file

@ -0,0 +1,150 @@
#include loading.jsx
#include app/info.jsx
#include app/scope.jsx
#include header/header.js
#include header/user.jsx
const HanyuuOAuth2DeviceVerify = () => {
const queryParams = new URLSearchParams(window.location.search);
const loading = new HanyuuOAuth2Loading('.js-loading');
const header = new HanyuuOAuth2Header;
const fAuths = document.querySelector('.js-verify-authorise');
const eAuthsInfo = document.querySelector('.js-verify-authorise-info');
const eAuthsScope = document.querySelector('.js-verify-authorise-scope');
const rApproved = document.querySelector('.js-verify-approved');
const rDenied = document.querySelector('.js-verify-denied');
let userCode = '';
let userHeader;
const verifyDeviceRequest = async approve => {
return await $x.post('/oauth2/device/verify', { type: 'json' }, {
_csrfp: HanyuuCSRFP.getToken(),
code: userCode,
approve: approve === true ? 'yes' : 'no',
});
};
const handleVerifyDeviceResponse = result => {
const response = result.body();
if(!response || typeof response.error === 'string') {
// TODO: nicer errors
if(response.error === 'auth')
alert('You are not logged in.');
else if(response.error === 'csrf')
alert('Request verification failed, please refresh and try again.');
else if(response.error === 'code')
alert('This code is not associated with any device authorisation request.');
else if(response.error === 'approval')
alert('The device authorisation request associated with this code is not pending approval.');
else if(response.error === 'invalid')
alert('Invalid approval state specified.');
else
alert(`An unknown error occurred: ${response.error}`);
loading.visible = false;
fAuths.classList.remove('hidden');
return;
}
loading.visible = false;
if(response.approval === 'approved')
rApproved.classList.remove('hidden');
else
rDenied.classList.remove('hidden');
};
fAuths.onsubmit = ev => {
ev.preventDefault();
loading.visible = true;
fAuths.classList.add('hidden');
if(userHeader)
userHeader.guiseVisible = false;
verifyDeviceRequest(ev.submitter.value === 'yes')
.then(handleVerifyDeviceResponse);
};
const fCode = document.querySelector('.js-verify-code');
const eUserCode = fCode.elements.namedItem('code');
fCode.onsubmit = ev => {
ev.preventDefault();
loading.visible = true;
fCode.classList.add('hidden');
$x.get(`/oauth2/device/resolve?csrfp=${encodeURIComponent(HanyuuCSRFP.getToken())}&code=${encodeURIComponent(eUserCode.value)}`, { type: 'json' })
.then(result => {
const response = result.body();
if(!response || typeof response.error === 'string') {
// TODO: nicer errors
if(response.error === 'auth')
alert('You are not logged in.');
else if(response.error === 'csrf')
alert('Request verification failed, please refresh and try again.');
else if(response.error === 'code')
alert('This code is not associated with any device authorisation request.');
else if(response.error === 'approval')
alert('The device authorisation request associated with this code is not pending approval.');
else
alert(`An unknown error occurred: ${response.error}`);
loading.visible = false;
fCode.classList.remove('hidden');
return;
}
userCode = response.device.code;
userHeader = new HanyuuOAuth2UserHeader(response.user);
header.setElement(userHeader);
if(response.app.trusted && response.user.guise === undefined) {
if(userHeader)
userHeader.guiseVisible = false;
verifyDeviceRequest(true).then(handleVerifyDeviceResponse);
return;
}
const appElem = new HanyuuOAuth2AppInfo(response.app);
eAuthsInfo.replaceWith(appElem.element);
const scopeElem = new HanyuuOAuth2AppScopeList();
eAuthsScope.replaceWith(scopeElem.element);
loading.visible = false;
fAuths.classList.remove('hidden');
});
};
const validateCodeInput = () => {
// [A-Za-z0-8]{3}\-[A-Za-z0-8]{3}\-[A-Za-z0-8]{3}
// 0 -> O, 1 -> I, 8 -> B
const eCode = eUserCode.value;
return true;
};
eUserCode.oninput = () => {
validateCodeInput();
console.warn(eUserCode.value);
};
if(queryParams.has('code') && eUserCode.value === '')
eUserCode.value = queryParams.get('code');
if(validateCodeInput()) {
fCode.requestSubmit();
} else {
loading.visible = false;
fCode.classList.remove('hidden');
}
};

View file

@ -0,0 +1,55 @@
const HanyuuOAuth2Header = function(element = '.js-oauth2-header', simpleElement = '.js-oauth2-header-simple') {
if(typeof element === 'string')
element = document.querySelector(element);
if(!(element instanceof HTMLElement))
throw 'element must be a valid query selector or an instance of HTMLElement';
if(typeof simpleElement === 'string')
simpleElement = element.querySelector(simpleElement);
const simpleElementIcon = simpleElement?.querySelector('.js-oauth2-header-simple-icon');
const simpleElementText = simpleElement?.querySelector('.js-oauth2-header-simple-text');
const hasSimpleElement = simpleElement instanceof HTMLElement;
const setSimpleVisible = state => {
if(hasSimpleElement)
simpleElement.classList.toggle('hidden', !state);
};
const setSimpleData = (name, text) => {
if(hasSimpleElement) {
simpleElement.className = `oauth2-${name}`;
simpleElementIcon.className = `oauth2-${name}-icon`;
simpleElementText.className = `oauth2-${name}-text`;
simpleElementText.textContent = text;
}
};
const removeElement = (forceSimple = true) => {
while(element.childElementCount > 1)
element.lastElementChild.remove();
if(typeof forceSimple === 'boolean')
setSimpleVisible(forceSimple);
};
return {
get element() { return element; },
get simpleVisible() { return hasSimpleElement && !simpleElement.classList.contains('hidden'); },
set simpleVisible(state) { setSimpleVisible(state); },
setSimpleData: setSimpleData,
setElement: elementInfo => {
removeElement(false);
if(elementInfo instanceof Element)
element.appendChild(elementInfo);
else if('element' in elementInfo)
element.appendChild(elementInfo.element);
else
throw 'elementInfo must be an instance of Element or contain an object with an element property';
},
removeElement: removeElement,
};
};

View file

@ -0,0 +1,53 @@
const HanyuuOAuth2UserGuiseHeader = function(guise) {
const element = <div class="oauth2-userhead-guise">
<div class="oauth2-userhead-guise-avatar">
<div class="oauth2-userhead-guise-avatar-image">
<img src={guise.avatar_uri} alt=""/>
</div>
</div>
<div class="oauth2-userhead-guise-text">
<p>Are you <a href={guise.profile_uri} target="_blank" style={`color: ${guise.colour}`}>{guise.name}</a> and did you mean to use your own account?</p>
<p><a href={guise.revert_uri} target="_blank">Click here</a> and reload this page.</p>
</div>
</div>;
return {
get element() { return element; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) {
element.classList.toggle('hidden', !state);
},
};
};
const HanyuuOAuth2UserHeader = function(user) {
const element = <div class="oauth2-userhead">
<div class="oauth2-userhead-main">
<div class="oauth2-userhead-main-avatar">
<div class="oauth2-userhead-main-avatar-image">
<img src={user.avatar_uri} alt=""/>
</div>
</div>
<div class="oauth2-userhead-main-name">
<a href={user.profile_uri} target="_blank">{user.name}</a>
</div>
</div>
</div>;
let guiseInfo;
if(user.guise) {
guiseInfo = new HanyuuOAuth2UserGuiseHeader(user.guise);
element.appendChild(guiseInfo.element);
}
return {
get element() { return element; },
get guiseVisible() { return guiseInfo?.visible === true; },
set guiseVisible(state) {
if(guiseInfo !== undefined)
guiseInfo.visible = state;
},
};
};

View file

@ -0,0 +1,84 @@
const HanyuuOAuth2LoadingIcon = function() {
const element = <div class="oauth2-loading-icon"/>;
for(let i = 0; i < 9; ++i)
element.appendChild(<div class="oauth2-loading-icon-block"/>);
// this is moderately cursed but it'll do
const blocks = [
element.children[3],
element.children[0],
element.children[1],
element.children[2],
element.children[5],
element.children[8],
element.children[7],
element.children[6],
];
let tsLastUpdate;
let counter = 0;
let playing = false;
const update = tsCurrent => {
try {
if(tsLastUpdate !== undefined && (tsCurrent - tsLastUpdate) < 50)
return;
tsLastUpdate = tsCurrent;
for(let i = 0; i < blocks.length; ++i)
blocks[(counter + i) % blocks.length].classList.toggle('oauth2-loading-icon-block-hidden', i < 3);
++counter;
} finally {
if(playing)
requestAnimationFrame(update);
}
};
const play = () => {
if(playing)
return;
playing = true;
requestAnimationFrame(update);
};
const pause = () => { playing = false; };
const stop = () => { pause(); counter = 0; };
const restart = () => { stop(); play(); };
return {
get element() { return element; },
get playing() { return playing; },
play: play,
pause: pause,
stop: stop,
restart: restart,
};
};
const HanyuuOAuth2Loading = function(element) {
if(typeof element === 'string')
element = document.querySelector(element);
if(!(element instanceof HTMLElement))
element = <div class="oauth2-loading"/>;
if(!element.classList.contains('oauth2-loading'))
element.classList.add('oauth2-loading');
let icon;
if(element.childElementCount < 1) {
icon = new HanyuuOAuth2LoadingIcon;
icon.play();
element.appendChild(<div class="oauth2-loading-frame">{icon}</div>);
}
return {
get element() { return element; },
get hasIcon() { return icon !== undefined; },
get icon() { return icon; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { element.classList.toggle('hidden', !state); },
};
};

View file

@ -1,3 +1,5 @@
#include device/verify.js
(() => {
const authoriseButtons = document.querySelectorAll('.js-authorise-action');
@ -29,4 +31,7 @@
xhr.send(body.join('&'));
};
}
if(location.pathname === '/oauth2/device/verify')
HanyuuOAuth2DeviceVerify();
})();

View file

@ -17,6 +17,7 @@ const fs = require('fs');
const tasks = {
js: [
{ source: 'hanyuu.js', target: '/assets', name: 'hanyuu.{hash}.js', },
{ source: 'oauth2.js', target: '/assets', name: 'oauth2.{hash}.js', },
],
css: [

View file

@ -0,0 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class NineCharacterDeviceUserCode_20240727_201438 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
$conn->execute('ALTER TABLE hau_oauth2_device CHANGE COLUMN dev_user_code dev_user_code CHAR(9) NOT NULL COLLATE ascii_general_ci AFTER dev_code');
}
}

View file

@ -0,0 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class RemovedDeviceAttemptsField_20240730_001441 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
$conn->execute('ALTER TABLE hau_oauth2_device DROP COLUMN dev_attempts');
}
}

118
package-lock.json generated
View file

@ -80,14 +80,14 @@
}
},
"node_modules/@swc/core": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz",
"integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.3.tgz",
"integrity": "sha512-HHAlbXjWI6Kl9JmmUW1LSygT1YbblXgj2UvvDzMkTBPRzYMhW6xchxdO8HbtMPtFYRt/EQq9u1z7j4ttRSrFsA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.9"
"@swc/types": "^0.1.12"
},
"engines": {
"node": ">=10"
@ -97,16 +97,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.6.13",
"@swc/core-darwin-x64": "1.6.13",
"@swc/core-linux-arm-gnueabihf": "1.6.13",
"@swc/core-linux-arm64-gnu": "1.6.13",
"@swc/core-linux-arm64-musl": "1.6.13",
"@swc/core-linux-x64-gnu": "1.6.13",
"@swc/core-linux-x64-musl": "1.6.13",
"@swc/core-win32-arm64-msvc": "1.6.13",
"@swc/core-win32-ia32-msvc": "1.6.13",
"@swc/core-win32-x64-msvc": "1.6.13"
"@swc/core-darwin-arm64": "1.7.3",
"@swc/core-darwin-x64": "1.7.3",
"@swc/core-linux-arm-gnueabihf": "1.7.3",
"@swc/core-linux-arm64-gnu": "1.7.3",
"@swc/core-linux-arm64-musl": "1.7.3",
"@swc/core-linux-x64-gnu": "1.7.3",
"@swc/core-linux-x64-musl": "1.7.3",
"@swc/core-win32-arm64-msvc": "1.7.3",
"@swc/core-win32-ia32-msvc": "1.7.3",
"@swc/core-win32-x64-msvc": "1.7.3"
},
"peerDependencies": {
"@swc/helpers": "*"
@ -118,9 +118,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz",
"integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.3.tgz",
"integrity": "sha512-CTkHa6MJdov9t41vuV2kmQIMu+Q19LrEHGIR/UiJYH06SC/sOu35ZZH8DyfLp9ZoaCn21gwgWd61ixOGQlwzTw==",
"cpu": [
"arm64"
],
@ -134,9 +134,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz",
"integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.3.tgz",
"integrity": "sha512-mun623y6rCoZ2EFIYfIRqXYRFufJOopoYSJcxYhZUrfTpAvQ1zLngjQpWCUU1krggXR2U0PQj+ls0DfXUTraNg==",
"cpu": [
"x64"
],
@ -150,9 +150,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz",
"integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.3.tgz",
"integrity": "sha512-4Jz4UcIcvZNMp9qoHbBx35bo3rjt8hpYLPqnR4FFq6gkAsJIMFC56UhRZwdEQoDuYiOFMBnnrsg31Fyo6YQypA==",
"cpu": [
"arm"
],
@ -166,9 +166,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz",
"integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.3.tgz",
"integrity": "sha512-p+U/M/oqV7HC4erQ5TVWHhJU1984QD+wQBPxslAYq751bOQGm0R/mXK42GjugqjnR6yYrAiwKKbpq4iWVXNePA==",
"cpu": [
"arm64"
],
@ -182,9 +182,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz",
"integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.3.tgz",
"integrity": "sha512-s6VzyaJwaRGTi2mz2h6Ywxfmgpkc69IxhuMzl+sl34plH0V0RgnZDm14HoCGIKIzRk4+a2EcBV1ZLAfWmPACQg==",
"cpu": [
"arm64"
],
@ -198,9 +198,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz",
"integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.3.tgz",
"integrity": "sha512-IrFY48C356Z2dU2pjYg080yvMXzmSV3Lmm/Wna4cfcB1nkVLjWsuYwwRAk9CY7E19c+q8N1sMNggubAUDYoX2g==",
"cpu": [
"x64"
],
@ -214,9 +214,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz",
"integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.3.tgz",
"integrity": "sha512-qoLgxBlBnnyUEDu5vmRQqX90h9jldU1JXI96e6eh2d1gJyKRA0oSK7xXmTzorv1fGHiHulv9qiJOUG+g6uzJWg==",
"cpu": [
"x64"
],
@ -230,9 +230,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz",
"integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.3.tgz",
"integrity": "sha512-OAd7jVVJ7nb0Ev80VAa1aeK+FldPeC4eZ35H4Qn6EICzIz0iqJo2T33qLKkSZiZEBKSoF4KcwrqYfkjLOp5qWg==",
"cpu": [
"arm64"
],
@ -246,9 +246,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz",
"integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.3.tgz",
"integrity": "sha512-31+Le1NyfSnILFV9+AhxfFOG0DK0272MNhbIlbcv4w/iqpjkhaOnNQnLsYJD1Ow7lTX1MtIZzTjOhRlzSviRWg==",
"cpu": [
"ia32"
],
@ -262,9 +262,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz",
"integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.3.tgz",
"integrity": "sha512-jVQPbYrwcuueI4QB0fHC29SVrkFOBcfIspYDlgSoHnEz6tmLMqUy+txZUypY/ZH/KaK0HEY74JkzgbRC1S6LFQ==",
"cpu": [
"x64"
],
@ -284,9 +284,9 @@
"license": "Apache-2.0"
},
"node_modules/@swc/types": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.9.tgz",
"integrity": "sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz",
"integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
@ -417,9 +417,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001642",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz",
"integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==",
"version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"funding": [
{
"type": "opencollective",
@ -703,9 +703,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.829",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.829.tgz",
"integrity": "sha512-5qp1N2POAfW0u1qGAxXEtz6P7bO1m6gpZr5hdf5ve6lxpLM7MpiM4jIPz7xcrNlClQMafbyUDDWjlIQZ1Mw0Rw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz",
"integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==",
"license": "ISC"
},
"node_modules/entities": {
@ -831,9 +831,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz",
"integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"license": "MIT"
},
"node_modules/normalize-range": {
@ -884,9 +884,9 @@
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
"funding": [
{
"type": "opencollective",

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>

After

Width:  |  Height:  |  Size: 424 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/></svg>

After

Width:  |  Height:  |  Size: 511 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M16 64C16 28.7 44.7 0 80 0L304 0c35.3 0 64 28.7 64 64l0 384c0 35.3-28.7 64-64 64L80 512c-35.3 0-64-28.7-64-64L16 64zM144 448c0 8.8 7.2 16 16 16l64 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-64 0c-8.8 0-16 7.2-16 16zM304 64L80 64l0 320 224 0 0-320z"/></svg>

After

Width:  |  Height:  |  Size: 471 B

View file

@ -89,7 +89,7 @@ class HanyuuContext {
'Misuzu',
(string)$request->getCookie('msz_auth'),
$_SERVER['REMOTE_ADDR'],
[60]
[60, 120]
);
});

View file

@ -13,7 +13,6 @@ class OAuth2DeviceInfo {
private ?string $userId,
private string $code,
private string $userCode,
private int $attempts,
private int $interval,
private int $polled,
private string $scope,
@ -29,13 +28,12 @@ class OAuth2DeviceInfo {
userId: $result->getStringOrNull(2),
code: $result->getString(3),
userCode: $result->getString(4),
attempts: $result->getInteger(5),
interval: $result->getInteger(6),
polled: $result->getInteger(7),
scope: $result->getString(8),
approval: $result->getString(9),
created: $result->getInteger(10),
expires: $result->getInteger(11),
interval: $result->getInteger(5),
polled: $result->getInteger(6),
scope: $result->getString(7),
approval: $result->getString(8),
created: $result->getInteger(9),
expires: $result->getInteger(10),
);
}
@ -61,7 +59,7 @@ class OAuth2DeviceInfo {
public function getUserCode(): string {
return $this->userCode;
}
public function getUserCodeDashed(int $interval = 4): string {
public function getUserCodeDashed(int $interval = 3): string {
// how is this a stdlib function????? i mean thanks but ?????
// update: it was too good to be true, i need to trim anyway...
return trim(chunk_split($this->userCode, $interval, '-'), '-');

View file

@ -6,10 +6,11 @@ use RuntimeException;
use Index\XString;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Serialisation\Base32;
use Hanyuu\Apps\AppInfo;
class OAuth2DevicesData {
private const USER_CODE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
private IDbConnection $dbConn;
private DbStatementCache $cache;
@ -49,14 +50,14 @@ class OAuth2DevicesData {
}
if($userCode !== null) {
$selectors[] = 'dev_user_code = ?';
$values[] = $userCode;
$values[] = preg_replace('#[^A-Za-z2-7]#', '', strtr($userCode, ['0' => 'O', '1' => 'I', '8' => 'B']));
}
if(empty($selectors))
throw new RuntimeException('Insufficient data to do device authorisation request lookup.');
$args = 0;
$stmt = $this->cache->get('SELECT dev_id, app_id, user_id, dev_code, dev_user_code, dev_attempts, dev_interval, UNIX_TIMESTAMP(dev_polled), dev_scope, dev_approval, UNIX_TIMESTAMP(dev_created), UNIX_TIMESTAMP(dev_expires) FROM hau_oauth2_device WHERE ' . implode(' AND ', $selectors));
$stmt = $this->cache->get('SELECT dev_id, app_id, user_id, dev_code, dev_user_code, dev_interval, UNIX_TIMESTAMP(dev_polled), dev_scope, dev_approval, UNIX_TIMESTAMP(dev_created), UNIX_TIMESTAMP(dev_expires) FROM hau_oauth2_device WHERE ' . implode(' AND ', $selectors));
foreach($values as $value)
$stmt->addParameter(++$args, $value);
$stmt->execute();
@ -76,7 +77,7 @@ class OAuth2DevicesData {
?string $userCode = null
): OAuth2DeviceInfo {
$code ??= XString::random(60);
$userCode ??= Base32::encode(random_bytes(5));
$userCode ??= XString::random(9, self::USER_CODE_CHARS);
$stmt = $this->cache->get('INSERT INTO hau_oauth2_device (app_id, user_id, dev_code, dev_user_code, dev_scope) VALUES (?, ?, ?, ?, ?)');
$stmt->addParameter(1, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo);
@ -120,17 +121,11 @@ class OAuth2DevicesData {
$stmt = $this->cache->get('UPDATE hau_oauth2_device SET dev_approval = ?, user_id = COALESCE(user_id, ?) WHERE dev_id = ? AND dev_approval = "pending" AND user_id IS NULL');
$stmt->addParameter(1, $approval ? 'approved' : 'denied');
$stmt->addParameter(2, $userId);
$stmt->addParameter(2, $approval ? $userId : null);
$stmt->addParameter(3, $deviceInfo);
$stmt->execute();
}
public function decrementDeviceUserAttempts(OAuth2DeviceInfo|string $deviceInfo): void {
$stmt = $this->cache->get('UPDATE hau_oauth2_device SET dev_attempts = MAX(0, dev_attempts - 1) WHERE dev_id = ?');
$stmt->addParameter(1, $deviceInfo instanceof OAuth2DeviceInfo ? $deviceInfo->getId() : $deviceInfo);
$stmt->execute();
}
public function bumpDevicePollTime(OAuth2DeviceInfo|string $deviceInfo): void {
$stmt = $this->cache->get('UPDATE hau_oauth2_device SET dev_polled = NOW() WHERE dev_id = ?');
$stmt->addParameter(1, $deviceInfo instanceof OAuth2DeviceInfo ? $deviceInfo->getId() : $deviceInfo);

View file

View file

@ -23,6 +23,16 @@ final class OAuth2Routes extends RouteHandler {
throw new InvalidArgumentException('$getAuthInfo must be callable');
}
private static function error(string $code, string $message = '', string $url = ''): array {
$info = ['error' => $code];
if($message !== '')
$info['error_description'] = $message;
if($url !== '')
$info['error_uri'] = $url;
return $info;
}
private static function buildCallbackUri(string $target, array $params): string {
if($target === '')
$target = '/oauth2/error';
@ -368,28 +378,121 @@ final class OAuth2Routes extends RouteHandler {
];
}
private static function error(string $code, string $message = '', string $url = ''): array {
$info = ['error' => $code];
if($message !== '')
$info['error_description'] = $message;
if($url !== '')
$info['error_uri'] = $url;
return $info;
}
#[HttpGet('/oauth2/device/verify')]
public function getDeviceVerify() {
return 'TODO: make this page';
public function getDeviceVerify($response, $request) {
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
return $this->templating->render('oauth2/login', [
'auth' => $authInfo,
]);
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
return $this->templating->render('oauth2/device/verify', [
'csrfp_token' => $csrfp->createToken(),
]);
}
#[HttpPost('/oauth2/device/verify')]
public function postDeviceVerify() {
public function postDeviceVerify($response, $request) {
if(!$request->isFormContent())
return 400;
// TODO: RATE LIMITING
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
return ['error' => 'auth'];
$content = $request->getContent();
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
if(!$csrfp->verifyToken((string)$content->getParam('_csrfp')))
return ['error' => 'csrf'];
$devicesData = $this->oauth2Ctx->getDevicesData();
try {
$deviceInfo = $devicesData->getDeviceInfo(userCode: (string)$content->getParam('code'));
} catch(RuntimeException $ex) {
return ['error' => 'code'];
}
if(!$deviceInfo->isPending())
return ['error' => 'approval'];
$approve = (string)$content->getParam('approve');
if(!in_array($approve, ['yes', 'no']))
return ['error' => 'invalid'];
$approved = $approve === 'yes';
$devicesData->setDeviceApproval($deviceInfo, $approved, $authInfo->user->id);
return [
'TODO' => 'make this endpoint',
'approval' => $approved ? 'approved' : 'denied',
];
}
#[HttpGet('/oauth2/device/resolve')]
public function getDeviceResolve($response, $request) {
// TODO: RATE LIMITING
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
return ['error' => 'auth'];
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
if(!$csrfp->verifyToken((string)$request->getParam('csrfp')))
return ['error' => 'csrf'];
$devicesData = $this->oauth2Ctx->getDevicesData();
try {
$deviceInfo = $devicesData->getDeviceInfo(userCode: (string)$request->getParam('code'));
} catch(RuntimeException $ex) {
return ['error' => 'code'];
}
if(!$deviceInfo->isPending())
return ['error' => 'approval'];
$appsData = $this->appsCtx->getData();
try {
$appInfo = $appsData->getAppInfo(appId: $deviceInfo->getAppId(), deleted: false);
} catch(RuntimeException $ex) {
return ['error' => 'code'];
}
$result = [
'device' => [
'code' => $deviceInfo->getUserCode(),
],
'app' => [
'name' => $appInfo->getName(),
'summary' => $appInfo->getSummary(),
'trusted' => $appInfo->isTrusted(),
'links' => [
['title' => 'Website', 'display' => $appInfo->getWebsiteDisplay(), 'uri' => $appInfo->getWebsite()],
],
],
'user' => [
'name' => $authInfo->user->name,
'colour' => $authInfo->user->colour,
'profile_uri' => $authInfo->user->profile_url,
'avatar_uri' => $authInfo->user->avatars->x120,
],
];
if(isset($authInfo->guise))
$result['user']['guise'] = [
'name' => $authInfo->guise->name,
'colour' => $authInfo->guise->colour,
'profile_uri' => $authInfo->guise->profile_url,
'revert_uri' => $authInfo->guise->revert_url,
'avatar_uri' => $authInfo->guise->avatars->x60,
];
return $result;
}
#[HttpPost('/oauth2/device/authorise')]
public function postDeviceAuthorise($response, $request) {
$response->setHeader('Cache-Control', 'no-store');

View file

@ -1,12 +1,14 @@
{% extends 'oauth2/master.twig' %}
{% block body %}
{% set body_title = 'Authorisation Request' %}
{% block body_header %}
<header class="oauth2-header"{% if auth.user.colour != 'inherit' %} style="background-color: {{ auth.user.colour }}"{% endif %}>
<div class="oauth2-userhead">
<div class="oauth2-userhead-main">
<div class="oauth2-userhead-main-avatar">
<div class="oauth2-userhead-main-avatar-image">
<img src="{{ auth.user.avatars.original }}" alt="">
<img src="{{ auth.user.avatars.x120 }}" alt="">
</div>
</div>
<div class="oauth2-userhead-main-name">
@ -28,16 +30,9 @@
{% endif %}
</div>
</header>
<div class="oauth2-body">
<div class="oauth2-banner">
<div class="oauth2-banner-text">
Authorisation Request
</div>
<div class="oauth2-banner-logo">
Flashii
</div>
</div>
{% endblock %}
{% block body_content %}
<div class="oauth2-authorise-requesting">
<p>A third-party application is requesting permission to access your account.</p>
</div>
@ -83,5 +78,4 @@
<button type="button" class="oauth2-authorise-button oauth2-authorise-button-accept js-authorise-action" data-redirect="{{ redirect_uri }}" data-csrfp="{{ csrfp_token }}" data-approve="yes" data-code="{{ req.code }}" disabled>Authorise</button>
<button type="button" class="oauth2-authorise-button oauth2-authorise-button-deny js-authorise-action" data-redirect="{{ redirect_uri }}" data-csrfp="{{ csrfp_token }}" data-approve="no" data-code="{{ req.code }}" disabled>Cancel</button>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,58 @@
{% extends 'oauth2/master.twig' %}
{% set body_header_class = 'devicehead' %}
{% set body_header_text = 'Device authorisation' %}
{% set body_title = 'Device Authorisation Request' %}
{% block body_content %}
<div class="js-loading"></div>
<form class="js-verify-code hidden">
<div class="oauth2-authorise-requesting">
<p>A third-party application that cannot display websites on its own is requesting permission to access your account.</p>
<p>Enter the code displayed on the device or application in the box below to continue authorisation.</p>
</div>
<div class="oauth2-device-form">
<input type="text" class="oauth2-device-code js-device-code" name="code" placeholder="XXX-XXX-XXX" autofocus>
</div>
<div class="oauth2-authorise-buttons">
<button class="oauth2-authorise-button">Next</button>
</div>
</form>
<form class="js-verify-authorise hidden">
<div class="oauth2-authorise-requesting">
<p>A third-party application is requesting permission to access your account.</p>
</div>
<div class="js-verify-authorise-info"></div>
<div class="js-verify-authorise-scope"></div>
<div class="oauth2-authorise-buttons">
<button name="approve" value="yes" class="oauth2-authorise-button oauth2-authorise-button-accept">Authorise</button>
<button name="approve" value="no" class="oauth2-authorise-button oauth2-authorise-button-deny">Cancel</button>
</div>
</form>
<div class="js-verify-approved hidden">
<div class="oauth2-approval">
<div class="oauth2-approval-icon oauth2-approval-icon-approved"></div>
<div class="oauth2-approval-header">Approved!</div>
<div class="oauth2-approval-text">
<p>You have approved the device authorisation request. You should now be signed in on the target device or application.</p>
</div>
</div>
</div>
<div class="js-verify-denied hidden">
<div class="oauth2-approval">
<div class="oauth2-approval-icon oauth2-approval-icon-denied"></div>
<div class="oauth2-approval-header">Denied!</div>
<div class="oauth2-approval-text">
<p>You have denied the device authorisation request. Please return to the target device or application and follow displayed instructions.</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,14 +1,10 @@
{% extends 'oauth2/master.twig' %}
{% block body %}
<header class="oauth2-header">
<div class="oauth2-errorhead">
<div class="oauth2-errorhead-icon"></div>
<div class="oauth2-errorhead-text">
An error occurred!
</div>
</div>
</header>
{% set body_header_class = 'errorhead' %}
{% set body_header_text = 'Error' %}
{% set body_title = 'An error occurred' %}
{% block body_content %}
<div class="oauth2-errorbody">
<p>{{ error_description }}</p>
</div>

View file

@ -1,36 +1,23 @@
{% extends 'oauth2/master.twig' %}
{% block body %}
<header class="oauth2-header">
<div class="oauth2-loginhead">
<div class="oauth2-loginhead-icon"></div>
<div class="oauth2-loginhead-text">
Not logged in!
</div>
</div>
</header>
<div class="oauth2-body">
<div class="oauth2-banner">
<div class="oauth2-banner-text">
Authorisation Request
</div>
<div class="oauth2-banner-logo">
Flashii
</div>
</div>
{% set body_header_class = 'loginhead' %}
{% set body_header_text = 'Not logged in' %}
{% set body_title = 'Authorisation Request' %}
{% block body_content %}
<div class="oauth2-authorise-requesting">
{% if not app.isTrusted %}
{% if app is defined and not app.isTrusted %}
<p>A third-party application is requesting permission to access your account.</p>
{% endif %}
<p>You must be logged in to authorise applications. <a href="{{ auth.login_url }}" target="_blank">Log in</a> or <a href="{{ auth.register_url }}" target="_blank">create an account</a> and reload this page to try again.</p>
{% if app.isTrusted %}
{% if app is defined and app.isTrusted %}
<p>You will be redirected to the following application after logging in.</p>
{% endif %}
</div>
{% if app is defined %}
<div class="oauth2-appinfo">
<div class="oauth2-appinfo-name">
{{ app.name }}
@ -67,5 +54,9 @@
</div>
</div>
</div>
{% else %}
<div class="oauth2-authorise-device">
<p>More details about the application will be displayed once you're logged in.</p>
</div>
{% endif %}
{% endblock %}

View file

@ -6,15 +6,39 @@
<title>{% if title is defined %}{{ title }} :: {% endif %}{{ globals.siteInfo.name }}</title>
<link href="{{ asset('hanyuu.css') }}" rel="stylesheet">
<link href="{{ asset('oauth2.css') }}" rel="stylesheet">
{% if csrfp_token is defined %}
<meta name="csrfp-token" content="{{ csrfp_token }}">
{% endif %}
</head>
<body>
<div class="oauth2-wrapper">
<div class="oauth2-dialog">
<div class="oauth2-dialog-body">
{% block body %}{% endblock %}
{% block body %}
{% block body_header %}
<header class="oauth2-header js-oauth2-header">
<div class="oauth2-{{ body_header_class|default('') }} js-oauth2-header-simple">
<div class="oauth2-{{ body_header_class|default('') }}-icon js-oauth2-header-simple-icon"></div>
<div class="oauth2-{{ body_header_class|default('') }}-text js-oauth2-header-simple-text">
{{ body_header_text|default('') }}
</div>
</div>
</header>
{% endblock %}
<div class="oauth2-body">
<div class="oauth2-banner">
<div class="oauth2-banner-text">{{ body_title|default('') }}</div>
<div class="oauth2-banner-logo">{{ globals.siteInfo.name }}</div>
</div>
{% block body_content %}{% endblock %}
</div>
{% endblock %}
</div>
</div>
</div>
<script src="{{ asset('hanyuu.js') }}"></script>
<script src="{{ asset('oauth2.js') }}"></script>
</body>
</html>