Implemented partial UI for device authorisation.
This commit is contained in:
parent
61dad487f6
commit
ffb0cb96df
33 changed files with 1181 additions and 185 deletions
44
assets/hanyuu.js/csrfp.js
Normal file
44
assets/hanyuu.js/csrfp.js
Normal 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
116
assets/hanyuu.js/html.js
Normal 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
3
assets/hanyuu.js/main.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#include csrfp.js
|
||||||
|
#include html.js
|
||||||
|
#include xhr.js
|
114
assets/hanyuu.js/xhr.js
Normal file
114
assets/hanyuu.js/xhr.js
Normal 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),
|
||||||
|
};
|
||||||
|
})();
|
35
assets/oauth2.css/approval.css
Normal file
35
assets/oauth2.css/approval.css
Normal 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;
|
||||||
|
}
|
|
@ -7,6 +7,14 @@
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oauth2-authorise-device {
|
||||||
|
font-size: .8em;
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
.oauth2-authorise-device p {
|
||||||
|
margin: .5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
.oauth2-authorise-buttons {
|
.oauth2-authorise-buttons {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -30,6 +38,13 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-decoration: none;
|
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] {
|
.oauth2-authorise-button[disabled] {
|
||||||
|
|
43
assets/oauth2.css/device.css
Normal file
43
assets/oauth2.css/device.css
Normal 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;
|
||||||
|
}
|
36
assets/oauth2.css/loading.css
Normal file
36
assets/oauth2.css/loading.css
Normal 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;
|
||||||
|
}
|
|
@ -35,10 +35,13 @@
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include loading.css;
|
||||||
@include banner.css;
|
@include banner.css;
|
||||||
@include error.css;
|
@include error.css;
|
||||||
@include login.css;
|
@include login.css;
|
||||||
|
@include device.css;
|
||||||
@include userhead.css;
|
@include userhead.css;
|
||||||
@include appinfo.css;
|
@include appinfo.css;
|
||||||
@include scope.css;
|
@include scope.css;
|
||||||
@include authorise.css;
|
@include authorise.css;
|
||||||
|
@include approval.css;
|
||||||
|
|
30
assets/oauth2.js/app/info.jsx
Normal file
30
assets/oauth2.js/app/info.jsx
Normal 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; },
|
||||||
|
};
|
||||||
|
};
|
29
assets/oauth2.js/app/scope.jsx
Normal file
29
assets/oauth2.js/app/scope.jsx
Normal 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; },
|
||||||
|
};
|
||||||
|
};
|
150
assets/oauth2.js/device/verify.js
Normal file
150
assets/oauth2.js/device/verify.js
Normal 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');
|
||||||
|
}
|
||||||
|
};
|
55
assets/oauth2.js/header/header.js
Normal file
55
assets/oauth2.js/header/header.js
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
53
assets/oauth2.js/header/user.jsx
Normal file
53
assets/oauth2.js/header/user.jsx
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
84
assets/oauth2.js/loading.jsx
Normal file
84
assets/oauth2.js/loading.jsx
Normal 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); },
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,3 +1,5 @@
|
||||||
|
#include device/verify.js
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const authoriseButtons = document.querySelectorAll('.js-authorise-action');
|
const authoriseButtons = document.querySelectorAll('.js-authorise-action');
|
||||||
|
|
||||||
|
@ -29,4 +31,7 @@
|
||||||
xhr.send(body.join('&'));
|
xhr.send(body.join('&'));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(location.pathname === '/oauth2/device/verify')
|
||||||
|
HanyuuOAuth2DeviceVerify();
|
||||||
})();
|
})();
|
||||||
|
|
1
build.js
1
build.js
|
@ -17,6 +17,7 @@ const fs = require('fs');
|
||||||
|
|
||||||
const tasks = {
|
const tasks = {
|
||||||
js: [
|
js: [
|
||||||
|
{ source: 'hanyuu.js', target: '/assets', name: 'hanyuu.{hash}.js', },
|
||||||
{ source: 'oauth2.js', target: '/assets', name: 'oauth2.{hash}.js', },
|
{ source: 'oauth2.js', target: '/assets', name: 'oauth2.{hash}.js', },
|
||||||
],
|
],
|
||||||
css: [
|
css: [
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
118
package-lock.json
generated
|
@ -80,14 +80,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core": {
|
"node_modules/@swc/core": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.3.tgz",
|
||||||
"integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==",
|
"integrity": "sha512-HHAlbXjWI6Kl9JmmUW1LSygT1YbblXgj2UvvDzMkTBPRzYMhW6xchxdO8HbtMPtFYRt/EQq9u1z7j4ttRSrFsA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/counter": "^0.1.3",
|
"@swc/counter": "^0.1.3",
|
||||||
"@swc/types": "^0.1.9"
|
"@swc/types": "^0.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
|
@ -97,16 +97,16 @@
|
||||||
"url": "https://opencollective.com/swc"
|
"url": "https://opencollective.com/swc"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-darwin-arm64": "1.6.13",
|
"@swc/core-darwin-arm64": "1.7.3",
|
||||||
"@swc/core-darwin-x64": "1.6.13",
|
"@swc/core-darwin-x64": "1.7.3",
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.6.13",
|
"@swc/core-linux-arm-gnueabihf": "1.7.3",
|
||||||
"@swc/core-linux-arm64-gnu": "1.6.13",
|
"@swc/core-linux-arm64-gnu": "1.7.3",
|
||||||
"@swc/core-linux-arm64-musl": "1.6.13",
|
"@swc/core-linux-arm64-musl": "1.7.3",
|
||||||
"@swc/core-linux-x64-gnu": "1.6.13",
|
"@swc/core-linux-x64-gnu": "1.7.3",
|
||||||
"@swc/core-linux-x64-musl": "1.6.13",
|
"@swc/core-linux-x64-musl": "1.7.3",
|
||||||
"@swc/core-win32-arm64-msvc": "1.6.13",
|
"@swc/core-win32-arm64-msvc": "1.7.3",
|
||||||
"@swc/core-win32-ia32-msvc": "1.6.13",
|
"@swc/core-win32-ia32-msvc": "1.7.3",
|
||||||
"@swc/core-win32-x64-msvc": "1.6.13"
|
"@swc/core-win32-x64-msvc": "1.7.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@swc/helpers": "*"
|
"@swc/helpers": "*"
|
||||||
|
@ -118,9 +118,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-darwin-arm64": {
|
"node_modules/@swc/core-darwin-arm64": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.3.tgz",
|
||||||
"integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==",
|
"integrity": "sha512-CTkHa6MJdov9t41vuV2kmQIMu+Q19LrEHGIR/UiJYH06SC/sOu35ZZH8DyfLp9ZoaCn21gwgWd61ixOGQlwzTw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -134,9 +134,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-darwin-x64": {
|
"node_modules/@swc/core-darwin-x64": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.3.tgz",
|
||||||
"integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==",
|
"integrity": "sha512-mun623y6rCoZ2EFIYfIRqXYRFufJOopoYSJcxYhZUrfTpAvQ1zLngjQpWCUU1krggXR2U0PQj+ls0DfXUTraNg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -150,9 +150,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.3.tgz",
|
||||||
"integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==",
|
"integrity": "sha512-4Jz4UcIcvZNMp9qoHbBx35bo3rjt8hpYLPqnR4FFq6gkAsJIMFC56UhRZwdEQoDuYiOFMBnnrsg31Fyo6YQypA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -166,9 +166,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.3.tgz",
|
||||||
"integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==",
|
"integrity": "sha512-p+U/M/oqV7HC4erQ5TVWHhJU1984QD+wQBPxslAYq751bOQGm0R/mXK42GjugqjnR6yYrAiwKKbpq4iWVXNePA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -182,9 +182,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-arm64-musl": {
|
"node_modules/@swc/core-linux-arm64-musl": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.3.tgz",
|
||||||
"integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==",
|
"integrity": "sha512-s6VzyaJwaRGTi2mz2h6Ywxfmgpkc69IxhuMzl+sl34plH0V0RgnZDm14HoCGIKIzRk4+a2EcBV1ZLAfWmPACQg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -198,9 +198,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-x64-gnu": {
|
"node_modules/@swc/core-linux-x64-gnu": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.3.tgz",
|
||||||
"integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==",
|
"integrity": "sha512-IrFY48C356Z2dU2pjYg080yvMXzmSV3Lmm/Wna4cfcB1nkVLjWsuYwwRAk9CY7E19c+q8N1sMNggubAUDYoX2g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -214,9 +214,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-x64-musl": {
|
"node_modules/@swc/core-linux-x64-musl": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.3.tgz",
|
||||||
"integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==",
|
"integrity": "sha512-qoLgxBlBnnyUEDu5vmRQqX90h9jldU1JXI96e6eh2d1gJyKRA0oSK7xXmTzorv1fGHiHulv9qiJOUG+g6uzJWg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -230,9 +230,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.3.tgz",
|
||||||
"integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==",
|
"integrity": "sha512-OAd7jVVJ7nb0Ev80VAa1aeK+FldPeC4eZ35H4Qn6EICzIz0iqJo2T33qLKkSZiZEBKSoF4KcwrqYfkjLOp5qWg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -246,9 +246,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.3.tgz",
|
||||||
"integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==",
|
"integrity": "sha512-31+Le1NyfSnILFV9+AhxfFOG0DK0272MNhbIlbcv4w/iqpjkhaOnNQnLsYJD1Ow7lTX1MtIZzTjOhRlzSviRWg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -262,9 +262,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-win32-x64-msvc": {
|
"node_modules/@swc/core-win32-x64-msvc": {
|
||||||
"version": "1.6.13",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.3.tgz",
|
||||||
"integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==",
|
"integrity": "sha512-jVQPbYrwcuueI4QB0fHC29SVrkFOBcfIspYDlgSoHnEz6tmLMqUy+txZUypY/ZH/KaK0HEY74JkzgbRC1S6LFQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -284,9 +284,9 @@
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@swc/types": {
|
"node_modules/@swc/types": {
|
||||||
"version": "0.1.9",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz",
|
||||||
"integrity": "sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==",
|
"integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/counter": "^0.1.3"
|
"@swc/counter": "^0.1.3"
|
||||||
|
@ -417,9 +417,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001642",
|
"version": "1.0.30001643",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
|
||||||
"integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==",
|
"integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
@ -703,9 +703,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.829",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.829.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz",
|
||||||
"integrity": "sha512-5qp1N2POAfW0u1qGAxXEtz6P7bO1m6gpZr5hdf5ve6lxpLM7MpiM4jIPz7xcrNlClQMafbyUDDWjlIQZ1Mw0Rw==",
|
"integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
|
@ -831,9 +831,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.17",
|
"version": "2.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||||
"integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==",
|
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/normalize-range": {
|
"node_modules/normalize-range": {
|
||||||
|
@ -884,9 +884,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.39",
|
"version": "8.4.40",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
|
||||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
1
public/images/circle-check-solid.svg
Normal file
1
public/images/circle-check-solid.svg
Normal 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 |
1
public/images/circle-xmark-solid.svg
Normal file
1
public/images/circle-xmark-solid.svg
Normal 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 |
1
public/images/mobile-screen-solid.svg
Normal file
1
public/images/mobile-screen-solid.svg
Normal 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 |
|
@ -89,7 +89,7 @@ class HanyuuContext {
|
||||||
'Misuzu',
|
'Misuzu',
|
||||||
(string)$request->getCookie('msz_auth'),
|
(string)$request->getCookie('msz_auth'),
|
||||||
$_SERVER['REMOTE_ADDR'],
|
$_SERVER['REMOTE_ADDR'],
|
||||||
[60]
|
[60, 120]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ class OAuth2DeviceInfo {
|
||||||
private ?string $userId,
|
private ?string $userId,
|
||||||
private string $code,
|
private string $code,
|
||||||
private string $userCode,
|
private string $userCode,
|
||||||
private int $attempts,
|
|
||||||
private int $interval,
|
private int $interval,
|
||||||
private int $polled,
|
private int $polled,
|
||||||
private string $scope,
|
private string $scope,
|
||||||
|
@ -29,13 +28,12 @@ class OAuth2DeviceInfo {
|
||||||
userId: $result->getStringOrNull(2),
|
userId: $result->getStringOrNull(2),
|
||||||
code: $result->getString(3),
|
code: $result->getString(3),
|
||||||
userCode: $result->getString(4),
|
userCode: $result->getString(4),
|
||||||
attempts: $result->getInteger(5),
|
interval: $result->getInteger(5),
|
||||||
interval: $result->getInteger(6),
|
polled: $result->getInteger(6),
|
||||||
polled: $result->getInteger(7),
|
scope: $result->getString(7),
|
||||||
scope: $result->getString(8),
|
approval: $result->getString(8),
|
||||||
approval: $result->getString(9),
|
created: $result->getInteger(9),
|
||||||
created: $result->getInteger(10),
|
expires: $result->getInteger(10),
|
||||||
expires: $result->getInteger(11),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +59,7 @@ class OAuth2DeviceInfo {
|
||||||
public function getUserCode(): string {
|
public function getUserCode(): string {
|
||||||
return $this->userCode;
|
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 ?????
|
// how is this a stdlib function????? i mean thanks but ?????
|
||||||
// update: it was too good to be true, i need to trim anyway...
|
// update: it was too good to be true, i need to trim anyway...
|
||||||
return trim(chunk_split($this->userCode, $interval, '-'), '-');
|
return trim(chunk_split($this->userCode, $interval, '-'), '-');
|
||||||
|
|
|
@ -6,10 +6,11 @@ use RuntimeException;
|
||||||
use Index\XString;
|
use Index\XString;
|
||||||
use Index\Data\DbStatementCache;
|
use Index\Data\DbStatementCache;
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\IDbConnection;
|
||||||
use Index\Serialisation\Base32;
|
|
||||||
use Hanyuu\Apps\AppInfo;
|
use Hanyuu\Apps\AppInfo;
|
||||||
|
|
||||||
class OAuth2DevicesData {
|
class OAuth2DevicesData {
|
||||||
|
private const USER_CODE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
|
||||||
private IDbConnection $dbConn;
|
private IDbConnection $dbConn;
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
|
@ -49,14 +50,14 @@ class OAuth2DevicesData {
|
||||||
}
|
}
|
||||||
if($userCode !== null) {
|
if($userCode !== null) {
|
||||||
$selectors[] = 'dev_user_code = ?';
|
$selectors[] = 'dev_user_code = ?';
|
||||||
$values[] = $userCode;
|
$values[] = preg_replace('#[^A-Za-z2-7]#', '', strtr($userCode, ['0' => 'O', '1' => 'I', '8' => 'B']));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(empty($selectors))
|
if(empty($selectors))
|
||||||
throw new RuntimeException('Insufficient data to do device authorisation request lookup.');
|
throw new RuntimeException('Insufficient data to do device authorisation request lookup.');
|
||||||
|
|
||||||
$args = 0;
|
$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)
|
foreach($values as $value)
|
||||||
$stmt->addParameter(++$args, $value);
|
$stmt->addParameter(++$args, $value);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
@ -76,7 +77,7 @@ class OAuth2DevicesData {
|
||||||
?string $userCode = null
|
?string $userCode = null
|
||||||
): OAuth2DeviceInfo {
|
): OAuth2DeviceInfo {
|
||||||
$code ??= XString::random(60);
|
$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 = $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);
|
$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 = $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(1, $approval ? 'approved' : 'denied');
|
||||||
$stmt->addParameter(2, $userId);
|
$stmt->addParameter(2, $approval ? $userId : null);
|
||||||
$stmt->addParameter(3, $deviceInfo);
|
$stmt->addParameter(3, $deviceInfo);
|
||||||
$stmt->execute();
|
$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 {
|
public function bumpDevicePollTime(OAuth2DeviceInfo|string $deviceInfo): void {
|
||||||
$stmt = $this->cache->get('UPDATE hau_oauth2_device SET dev_polled = NOW() WHERE dev_id = ?');
|
$stmt = $this->cache->get('UPDATE hau_oauth2_device SET dev_polled = NOW() WHERE dev_id = ?');
|
||||||
$stmt->addParameter(1, $deviceInfo instanceof OAuth2DeviceInfo ? $deviceInfo->getId() : $deviceInfo);
|
$stmt->addParameter(1, $deviceInfo instanceof OAuth2DeviceInfo ? $deviceInfo->getId() : $deviceInfo);
|
||||||
|
|
0
src/OAuth2/OAuth2ErrorRoutes.php
Normal file
0
src/OAuth2/OAuth2ErrorRoutes.php
Normal file
|
@ -23,6 +23,16 @@ final class OAuth2Routes extends RouteHandler {
|
||||||
throw new InvalidArgumentException('$getAuthInfo must be callable');
|
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 {
|
private static function buildCallbackUri(string $target, array $params): string {
|
||||||
if($target === '')
|
if($target === '')
|
||||||
$target = '/oauth2/error';
|
$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')]
|
#[HttpGet('/oauth2/device/verify')]
|
||||||
public function getDeviceVerify() {
|
public function getDeviceVerify($response, $request) {
|
||||||
return 'TODO: make this page';
|
$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')]
|
#[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 [
|
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')]
|
#[HttpPost('/oauth2/device/authorise')]
|
||||||
public function postDeviceAuthorise($response, $request) {
|
public function postDeviceAuthorise($response, $request) {
|
||||||
$response->setHeader('Cache-Control', 'no-store');
|
$response->setHeader('Cache-Control', 'no-store');
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
{% extends 'oauth2/master.twig' %}
|
{% 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 %}>
|
<header class="oauth2-header"{% if auth.user.colour != 'inherit' %} style="background-color: {{ auth.user.colour }}"{% endif %}>
|
||||||
<div class="oauth2-userhead">
|
<div class="oauth2-userhead">
|
||||||
<div class="oauth2-userhead-main">
|
<div class="oauth2-userhead-main">
|
||||||
<div class="oauth2-userhead-main-avatar">
|
<div class="oauth2-userhead-main-avatar">
|
||||||
<div class="oauth2-userhead-main-avatar-image">
|
<div class="oauth2-userhead-main-avatar-image">
|
||||||
<img src="{{ auth.user.avatars.original }}" alt="">
|
<img src="{{ auth.user.avatars.x120 }}" alt="">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="oauth2-userhead-main-name">
|
<div class="oauth2-userhead-main-name">
|
||||||
|
@ -28,60 +30,52 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="oauth2-body">
|
{% endblock %}
|
||||||
<div class="oauth2-banner">
|
|
||||||
<div class="oauth2-banner-text">
|
|
||||||
Authorisation Request
|
|
||||||
</div>
|
|
||||||
<div class="oauth2-banner-logo">
|
|
||||||
Flashii
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="oauth2-authorise-requesting">
|
{% block body_content %}
|
||||||
<p>A third-party application is requesting permission to access your account.</p>
|
<div class="oauth2-authorise-requesting">
|
||||||
</div>
|
<p>A third-party application is requesting permission to access your account.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="oauth2-appinfo">
|
<div class="oauth2-appinfo">
|
||||||
<div class="oauth2-appinfo-name">
|
<div class="oauth2-appinfo-name">
|
||||||
{{ app.name }}
|
{{ app.name }}
|
||||||
</div>{# TODO: author should be listed #}
|
</div>{# TODO: author should be listed #}
|
||||||
<div class="oauth2-appinfo-links">
|
<div class="oauth2-appinfo-links">
|
||||||
<a href="{{ app.website }}" target="_blank" rel="noopener noreferrer" class="oauth2-appinfo-link" title="Website">
|
<a href="{{ app.website }}" target="_blank" rel="noopener noreferrer" class="oauth2-appinfo-link" title="Website">
|
||||||
<div class="oauth2-appinfo-link-icon oauth2-appinfo-link-icon-globe"></div>
|
<div class="oauth2-appinfo-link-icon oauth2-appinfo-link-icon-globe"></div>
|
||||||
<div class="oauth2-appinfo-link-text">{{ app.websiteDisplay }}</div>
|
<div class="oauth2-appinfo-link-text">{{ app.websiteDisplay }}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
<div class="oauth2-appinfo-summary">
|
|
||||||
<p>{{ app.summary }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="oauth2-appinfo-summary">
|
||||||
<div class="oauth2-scope">
|
<p>{{ app.summary }}</p>
|
||||||
<div class="oauth2-scope-header">This application will be able to:</div>
|
|
||||||
<div class="oauth2-scope-perms">
|
|
||||||
<div class="oauth2-scope-perm">
|
|
||||||
<div class="oauth2-scope-perm-icon"></div>
|
|
||||||
<div class="oauth2-scope-perm-text">Do anything because I have not made up scopes yet.</div>
|
|
||||||
</div>
|
|
||||||
<div class="oauth2-scope-perm">
|
|
||||||
<div class="oauth2-scope-perm-icon"></div>
|
|
||||||
<div class="oauth2-scope-perm-text">Eat soup.</div>
|
|
||||||
</div>
|
|
||||||
<div class="oauth2-scope-perm">
|
|
||||||
<div class="oauth2-scope-perm-icon"></div>
|
|
||||||
<div class="oauth2-scope-perm-text">These are placeholders.</div>
|
|
||||||
</div>
|
|
||||||
<div class="oauth2-scope-perm">
|
|
||||||
<div class="oauth2-scope-perm-icon"></div>
|
|
||||||
<div class="oauth2-scope-perm-text">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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="oauth2-authorise-buttons">
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="oauth2-scope">
|
||||||
|
<div class="oauth2-scope-header">This application will be able to:</div>
|
||||||
|
<div class="oauth2-scope-perms">
|
||||||
|
<div class="oauth2-scope-perm">
|
||||||
|
<div class="oauth2-scope-perm-icon"></div>
|
||||||
|
<div class="oauth2-scope-perm-text">Do anything because I have not made up scopes yet.</div>
|
||||||
|
</div>
|
||||||
|
<div class="oauth2-scope-perm">
|
||||||
|
<div class="oauth2-scope-perm-icon"></div>
|
||||||
|
<div class="oauth2-scope-perm-text">Eat soup.</div>
|
||||||
|
</div>
|
||||||
|
<div class="oauth2-scope-perm">
|
||||||
|
<div class="oauth2-scope-perm-icon"></div>
|
||||||
|
<div class="oauth2-scope-perm-text">These are placeholders.</div>
|
||||||
|
</div>
|
||||||
|
<div class="oauth2-scope-perm">
|
||||||
|
<div class="oauth2-scope-perm-icon"></div>
|
||||||
|
<div class="oauth2-scope-perm-text">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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oauth2-authorise-buttons">
|
||||||
|
<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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
58
templates/oauth2/device/verify.twig
Normal file
58
templates/oauth2/device/verify.twig
Normal 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 %}
|
|
@ -1,14 +1,10 @@
|
||||||
{% extends 'oauth2/master.twig' %}
|
{% extends 'oauth2/master.twig' %}
|
||||||
|
|
||||||
{% block body %}
|
{% set body_header_class = 'errorhead' %}
|
||||||
<header class="oauth2-header">
|
{% set body_header_text = 'Error' %}
|
||||||
<div class="oauth2-errorhead">
|
{% set body_title = 'An error occurred' %}
|
||||||
<div class="oauth2-errorhead-icon"></div>
|
|
||||||
<div class="oauth2-errorhead-text">
|
{% block body_content %}
|
||||||
An error occurred!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="oauth2-errorbody">
|
<div class="oauth2-errorbody">
|
||||||
<p>{{ error_description }}</p>
|
<p>{{ error_description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,36 +1,23 @@
|
||||||
{% extends 'oauth2/master.twig' %}
|
{% extends 'oauth2/master.twig' %}
|
||||||
|
|
||||||
{% block body %}
|
{% set body_header_class = 'loginhead' %}
|
||||||
<header class="oauth2-header">
|
{% set body_header_text = 'Not logged in' %}
|
||||||
<div class="oauth2-loginhead">
|
{% set body_title = 'Authorisation Request' %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="oauth2-authorise-requesting">
|
{% block body_content %}
|
||||||
{% if not app.isTrusted %}
|
<div class="oauth2-authorise-requesting">
|
||||||
<p>A third-party application is requesting permission to access your account.</p>
|
{% if app is defined and not app.isTrusted %}
|
||||||
{% endif %}
|
<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>
|
<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>
|
<p>You will be redirected to the following application after logging in.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if app is defined %}
|
||||||
<div class="oauth2-appinfo">
|
<div class="oauth2-appinfo">
|
||||||
<div class="oauth2-appinfo-name">
|
<div class="oauth2-appinfo-name">
|
||||||
{{ app.name }}
|
{{ app.name }}
|
||||||
|
@ -67,5 +54,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
|
@ -6,15 +6,39 @@
|
||||||
<title>{% if title is defined %}{{ title }} :: {% endif %}{{ globals.siteInfo.name }}</title>
|
<title>{% if title is defined %}{{ title }} :: {% endif %}{{ globals.siteInfo.name }}</title>
|
||||||
<link href="{{ asset('hanyuu.css') }}" rel="stylesheet">
|
<link href="{{ asset('hanyuu.css') }}" rel="stylesheet">
|
||||||
<link href="{{ asset('oauth2.css') }}" rel="stylesheet">
|
<link href="{{ asset('oauth2.css') }}" rel="stylesheet">
|
||||||
|
{% if csrfp_token is defined %}
|
||||||
|
<meta name="csrfp-token" content="{{ csrfp_token }}">
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="oauth2-wrapper">
|
<div class="oauth2-wrapper">
|
||||||
<div class="oauth2-dialog">
|
<div class="oauth2-dialog">
|
||||||
<div class="oauth2-dialog-body">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="{{ asset('hanyuu.js') }}"></script>
|
||||||
<script src="{{ asset('oauth2.js') }}"></script>
|
<script src="{{ asset('oauth2.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Reference in a new issue