mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-05-05 06:37:10 -04:00
Added working oauth2.
This commit is contained in:
parent
04e4a5eee0
commit
c966e12926
19 changed files with 401 additions and 198 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -27,3 +27,4 @@ plugin_packages
|
|||
/src/test-results
|
||||
playwright-report
|
||||
state.json
|
||||
/src/static/oidc
|
||||
|
|
|
@ -8,8 +8,10 @@ FROM node:alpine as adminBuild
|
|||
|
||||
WORKDIR /opt/etherpad-lite
|
||||
COPY ./admin ./admin
|
||||
COPY ./ui ./ui
|
||||
COPY ./src/locales ./src/locales
|
||||
RUN cd ./admin && npm install -g pnpm && pnpm install && pnpm run build --outDir ./dist
|
||||
RUN cd ./ui && pnpm install && pnpm run build --outDir ./dist
|
||||
|
||||
|
||||
FROM node:alpine as build
|
||||
|
@ -116,6 +118,7 @@ FROM build as development
|
|||
|
||||
COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/
|
||||
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin
|
||||
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/ui/dist ./src/static/oidc
|
||||
|
||||
RUN bin/installDeps.sh && \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
||||
|
@ -130,6 +133,7 @@ ENV ETHERPAD_PRODUCTION=true
|
|||
|
||||
COPY --chown=etherpad:etherpad ./src ./src
|
||||
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin
|
||||
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/ui/dist ./src/static/oidc
|
||||
|
||||
RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
||||
|
|
|
@ -15,7 +15,8 @@ export default defineConfig({
|
|||
})],
|
||||
base: '/admin',
|
||||
build:{
|
||||
outDir: '../src/templates/admin'
|
||||
outDir: '../src/templates/admin',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server:{
|
||||
proxy: {
|
||||
|
|
|
@ -650,5 +650,23 @@
|
|||
/*
|
||||
* Enable/Disable case-insensitive pad names.
|
||||
*/
|
||||
"lowerCasePadIds": "${LOWER_CASE_PAD_IDS:false}"
|
||||
"lowerCasePadIds": "${LOWER_CASE_PAD_IDS:false}",
|
||||
"sso": {
|
||||
"clients": [
|
||||
{
|
||||
"client_id": "admin_client",
|
||||
"client_secret": "admin",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"],
|
||||
"redirect_uris": ["http://localhost:9001/admin/*"]
|
||||
},
|
||||
{
|
||||
"client_id": "user_client",
|
||||
"client_secret": "user",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"],
|
||||
"redirect_uris": ["http://localhost:9001/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -650,5 +650,24 @@
|
|||
/*
|
||||
* Enable/Disable case-insensitive pad names.
|
||||
*/
|
||||
"lowerCasePadIds": false
|
||||
"lowerCasePadIds": false,
|
||||
|
||||
"sso": {
|
||||
"clients": [
|
||||
{
|
||||
"client_id": "admin_client",
|
||||
"client_secret": "admin",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"],
|
||||
"redirect_uris": ["http://localhost:9001/admin/*"]
|
||||
},
|
||||
{
|
||||
"client_id": "user_client",
|
||||
"client_secret": "user",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"],
|
||||
"redirect_uris": ["http://localhost:9001/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,36 +5,37 @@ import MemoryAdapter from "./OIDCAdapter";
|
|||
import path from "path";
|
||||
const settings = require('../utils/Settings');
|
||||
import {IncomingForm} from 'formidable'
|
||||
import {Request, Response} from 'express';
|
||||
import express, {Request, Response} from 'express';
|
||||
import {format} from 'url'
|
||||
import {ParsedUrlQuery} from "node:querystring";
|
||||
import cors from 'cors'
|
||||
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
|
||||
const configuration: Configuration = {
|
||||
// refer to the documentation for other available configuration
|
||||
clients: [ {
|
||||
client_id: 'oidc_client',
|
||||
client_secret: 'a_different_secret',
|
||||
grant_types: ['authorization_code'],
|
||||
response_types: ['code'],
|
||||
redirect_uris: ['http://localhost:3001/cb', 'https://oauth.pstmn.io/v1/callback']
|
||||
},
|
||||
{
|
||||
client_id: 'app',
|
||||
client_secret: 'a_secret',
|
||||
grant_types: ['client_credentials'],
|
||||
redirect_uris: [],
|
||||
response_types: []
|
||||
}
|
||||
],
|
||||
scopes: ['openid', 'profile', 'email'],
|
||||
findAccount: async (ctx, id) => {
|
||||
const users = settings.users as {
|
||||
[username: string]: {
|
||||
password: string;
|
||||
is_admin: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const usersArray1 = Object.keys(users).map((username) => ({
|
||||
username,
|
||||
...users[username]
|
||||
}));
|
||||
|
||||
const account = usersArray1.find((user) => user.username === id);
|
||||
|
||||
return {
|
||||
accountId: id,
|
||||
claims: () => ({
|
||||
sub: id,
|
||||
test: "test",
|
||||
admin: account?.is_admin
|
||||
})
|
||||
} as Account
|
||||
},
|
||||
|
||||
ttl:{
|
||||
AccessToken: 1 * 60 * 60, // 1 hour in seconds
|
||||
AuthorizationCode: 10 * 60, // 10 minutes in seconds
|
||||
|
@ -42,6 +43,12 @@ const configuration: Configuration = {
|
|||
IdToken: 1 * 60 * 60, // 1 hour in seconds
|
||||
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
|
||||
},
|
||||
claims: {
|
||||
openid: ['sub'],
|
||||
email: ['email'],
|
||||
profile: ['name'],
|
||||
admin: ['admin']
|
||||
},
|
||||
cookies: {
|
||||
keys: ['oidc'],
|
||||
},
|
||||
|
@ -52,24 +59,89 @@ const configuration: Configuration = {
|
|||
};
|
||||
|
||||
|
||||
/*
|
||||
This function is used to initialize the OAuth2 provider
|
||||
*/
|
||||
export const expressCreateServer = async (hookName: string, args: ArgsExpressType, cb: Function) => {
|
||||
const {privateKey} = await generateKeyPair('RS256');
|
||||
const privateKeyJWK = await exportJWK(privateKey);
|
||||
// Use cors middleware
|
||||
args.app.use(cors({
|
||||
origin: ['http://localhost:3001', 'https://oauth.pstmn.io'], // replace with your allowed origins
|
||||
}));
|
||||
|
||||
const oidc = new Provider('http://localhost:9001', {
|
||||
...configuration, jwks: {
|
||||
keys: [
|
||||
privateKeyJWK
|
||||
],
|
||||
},
|
||||
conformIdTokenClaims: false,
|
||||
claims: {
|
||||
address: ['address'],
|
||||
email: ['email', 'email_verified'],
|
||||
phone: ['phone_number', 'phone_number_verified'],
|
||||
profile: ['birthdate', 'family_name', 'gender', 'given_name', 'locale', 'middle_name', 'name',
|
||||
'nickname', 'picture', 'preferred_username', 'profile', 'updated_at', 'website', 'zoneinfo'],
|
||||
},
|
||||
features:{
|
||||
userinfo: {enabled: true},
|
||||
|
||||
claimsParameter: {enabled: true},
|
||||
devInteractions: {enabled: false},
|
||||
resourceIndicators: {enabled: true, defaultResource(ctx) {
|
||||
return ctx.origin;
|
||||
},
|
||||
getResourceServerInfo(ctx, resourceIndicator, client) {
|
||||
return {
|
||||
scope: client.scope as string,
|
||||
audience: 'account',
|
||||
accessTokenFormat: 'jwt',
|
||||
};
|
||||
},
|
||||
useGrantedResource(ctx, model) {
|
||||
return true;
|
||||
},},
|
||||
jwtResponseModes: {enabled: true},
|
||||
},
|
||||
clientBasedCORS: (ctx, origin, client) => {
|
||||
return true
|
||||
},
|
||||
extraTokenClaims: async (ctx, token) => {
|
||||
|
||||
|
||||
if(token.kind === 'AccessToken') {
|
||||
// Add your custom claims here. For example:
|
||||
const users = settings.users as {
|
||||
[username: string]: {
|
||||
password: string;
|
||||
is_admin: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const usersArray1 = Object.keys(users).map((username) => ({
|
||||
username,
|
||||
...users[username]
|
||||
}));
|
||||
|
||||
const account = usersArray1.find((user) => user.username === token.accountId);
|
||||
return {
|
||||
admin: account?.is_admin
|
||||
};
|
||||
}
|
||||
},
|
||||
clients: settings.sso.clients
|
||||
});
|
||||
|
||||
|
||||
args.app.post('/interaction/:uid', async (req, res, next) => {
|
||||
args.app.post('/interaction/:uid', async (req: Http2ServerRequest, res: Http2ServerResponse, next:Function) => {
|
||||
const formid = new IncomingForm();
|
||||
try {
|
||||
// @ts-ignore
|
||||
const {login, password} = (await formid.parse(req))[0]
|
||||
const {prompt, jti, session, params, grantId} = await oidc.interactionDetails(req, res);
|
||||
const {prompt, jti, session,cid, params, grantId} = await oidc.interactionDetails(req, res);
|
||||
|
||||
const client = await oidc.Client.find(params.client_id as string);
|
||||
|
||||
switch (prompt.name) {
|
||||
case 'login': {
|
||||
|
@ -110,6 +182,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
|
|||
}
|
||||
|
||||
if (prompt.details.missingOIDCScope) {
|
||||
// @ts-ignore
|
||||
grant!.addOIDCScope(prompt.details.missingOIDCScope.join(' '));
|
||||
}
|
||||
if (prompt.details.missingOIDCClaims) {
|
||||
|
@ -128,13 +201,13 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
|
|||
}
|
||||
}
|
||||
await next();
|
||||
} catch (err) {
|
||||
} catch (err:any) {
|
||||
return res.writeHead(500).end(err.message);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
args.app.get('/interaction/:uid', async (req: Request, res: Response, next) => {
|
||||
args.app.get('/interaction/:uid', async (req: Request, res: Response, next: Function) => {
|
||||
try {
|
||||
const {
|
||||
uid, prompt, params, session,
|
||||
|
@ -145,14 +218,14 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
|
|||
switch (prompt.name) {
|
||||
case 'login': {
|
||||
res.redirect(format({
|
||||
pathname: '/views/login',
|
||||
pathname: '/views/login.html',
|
||||
query: params as ParsedUrlQuery
|
||||
}))
|
||||
break
|
||||
}
|
||||
case 'consent': {
|
||||
res.redirect(format({
|
||||
pathname: '/views/consent',
|
||||
pathname: '/views/consent.html',
|
||||
query: params as ParsedUrlQuery
|
||||
}))
|
||||
break
|
||||
|
@ -166,13 +239,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
|
|||
});
|
||||
|
||||
|
||||
args.app.get('/views/login', async (req, res) => {
|
||||
res.sendFile(path.join(settings.root,'src','static', 'oidc','login.html'));
|
||||
})
|
||||
|
||||
args.app.get('/views/consent', async (req, res) => {
|
||||
res.sendFile(path.join(settings.root,'src','static', 'oidc','consent.html'));
|
||||
})
|
||||
args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24}));
|
||||
|
||||
/*
|
||||
oidc.on('authorization.error', (ctx, error) => {
|
||||
|
|
|
@ -4,7 +4,7 @@ import type {Adapter, AdapterPayload} from "oidc-provider";
|
|||
|
||||
const options = {
|
||||
max: 500,
|
||||
sizeCalculation: (item, key) => {
|
||||
sizeCalculation: (item:any, key:any) => {
|
||||
return 1
|
||||
},
|
||||
// for use with tracking overall storage size
|
||||
|
@ -43,7 +43,6 @@ class MemoryAdapter implements Adapter{
|
|||
}
|
||||
|
||||
destroy(id:string) {
|
||||
console.log("destroy", id);
|
||||
const key = this.key(id);
|
||||
|
||||
const found = storage.get(key) as AdapterPayload;
|
||||
|
@ -61,13 +60,11 @@ class MemoryAdapter implements Adapter{
|
|||
}
|
||||
|
||||
consume(id: string) {
|
||||
console.log("consume", id);
|
||||
(storage.get(this.key(id)) as AdapterPayload)!.consumed = epochTime();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
find(id: string): Promise<AdapterPayload | void | undefined> {
|
||||
const foundSession = storage.get(this.key(id)) as AdapterPayload;
|
||||
if (storage.has(this.key(id))){
|
||||
return Promise.resolve<AdapterPayload>(storage.get(this.key(id)) as AdapterPayload);
|
||||
}
|
||||
|
@ -88,7 +85,6 @@ class MemoryAdapter implements Adapter{
|
|||
accountId: string;
|
||||
loginTs: number;
|
||||
}, expiresIn: number) {
|
||||
console.log("upsert", payload);
|
||||
const key = this.key(id);
|
||||
|
||||
storage.set(key, payload, {ttl: expiresIn * 1000});
|
||||
|
|
|
@ -342,6 +342,11 @@ exports.requireAuthentication = false;
|
|||
exports.requireAuthorization = false;
|
||||
exports.users = {};
|
||||
|
||||
/*
|
||||
* This setting is used for configuring sso
|
||||
*/
|
||||
exports.sso = {}
|
||||
|
||||
/*
|
||||
* Show settings in admin page, by default it is true
|
||||
*/
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"axios": "^1.6.8",
|
||||
"clean-css": "^5.3.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"ejs": "^3.1.9",
|
||||
"etherpad-require-kernel": "^1.0.16",
|
||||
|
@ -83,6 +84,7 @@
|
|||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/async": "^3.2.24",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/http-errors": "^2.0.4",
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Consent</title>
|
||||
<script>
|
||||
window.onload = ()=>{
|
||||
const form = document.getElementsByTagName('form')[0]
|
||||
form.action = '/interaction/' + new URLSearchParams(window.location.search).get('state')
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<form action="" autocomplete="off" method="post">
|
||||
<input type="hidden" name="prompt" value="consent"/>
|
||||
<button autofocus type="submit" class="login login-submit">Continue</button>
|
||||
</form>
|
||||
</body>
|
|
@ -1,50 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
function login() {
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const data = {};
|
||||
formData.forEach((value, key) => {
|
||||
data[key] = value;
|
||||
});
|
||||
const sessionId = new URLSearchParams(window.location.search).get('state');
|
||||
|
||||
fetch('/interaction/'+sessionId, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirect: 'follow',
|
||||
body: JSON.stringify(data),
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('error').innerText = "Error signing in";
|
||||
}
|
||||
}).catch(error => {
|
||||
document.getElementById('error').innerText = "Error signing in"+error;
|
||||
});
|
||||
});
|
||||
}
|
||||
window.onload = login;
|
||||
</script>
|
||||
<form autocomplete="off" method="post">
|
||||
<input type="hidden" name="prompt" value="login"/>
|
||||
<input required type="text" name="login" placeholder="Enter any login"/>
|
||||
<input required type="password" name="password" placeholder="and password"/>
|
||||
<button type="submit" class="login login-submit">Sign-in</button>
|
||||
<div id="error"></div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -2,12 +2,23 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + TS</title>
|
||||
<title>Consent Etherpad</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app">
|
||||
<div class="login-background login-page">
|
||||
<div class="login-box login-form">
|
||||
<h1 class="login-title">Etherpad <span id="client"></span></h1>
|
||||
<form class="login-inner-box input-control" method="post">
|
||||
<input type="hidden" name="prompt" value="consent"/>
|
||||
<input type="submit" value="Login" class="login-button"/>
|
||||
<div id="error"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/consent.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,12 +2,37 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Vite + TS</title>
|
||||
<title>SSO Etherpad</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app">
|
||||
<div class="login-background login-page">
|
||||
<div class="login-box login-form">
|
||||
<h1 class="login-title">Etherpad <span id="client"></span></h1>
|
||||
<form class="login-inner-box input-control">
|
||||
<label>
|
||||
<input class="login-textinput input-control" required type="text" name="login" placeholder="Username"/>
|
||||
</label>
|
||||
<div class="icon-input">
|
||||
<label class="password-label">
|
||||
<input class="login-textinput" type="password" required name="password" placeholder="Password"/>
|
||||
<svg id="eye-visible" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 toggle-password-visibility">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
<svg id="eye-hide" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<input type="submit" value="Login" class="login-button"/>
|
||||
<div id="error"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-singlefile": "^2.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,35 @@
|
|||
import "./style.css"
|
||||
//import {MapArrayType} from "ep_etherpad-lite/node/types/MapType";
|
||||
|
||||
const form = document.querySelector('form')!;
|
||||
const sessionId = new URLSearchParams(window.location.search).get('state');
|
||||
|
||||
form.action = '/interaction/' + sessionId;
|
||||
|
||||
/*form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const data: MapArrayType<any> = {};
|
||||
formData.forEach((value, key) => {
|
||||
data[key] = value;
|
||||
});
|
||||
const sessionId = new URLSearchParams(window.location.search).get('state');
|
||||
|
||||
fetch('/interaction/' + sessionId, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('error')!.innerText = "Error signing in";
|
||||
}
|
||||
}).catch(error => {
|
||||
document.getElementById('error')!.innerText = "Error signing in" + error;
|
||||
})
|
||||
});*/
|
|
@ -1,3 +1,58 @@
|
|||
import './style.css'
|
||||
import {MapArrayType} from "ep_etherpad-lite/node/types/MapType.ts";
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
|
||||
document.getElementById('client')!.innerText = searchParams.get('client_id')!;
|
||||
|
||||
const form = document.querySelector('form')!;
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const data: MapArrayType<any> = {};
|
||||
formData.forEach((value, key) => {
|
||||
data[key] = value;
|
||||
});
|
||||
const sessionId = new URLSearchParams(window.location.search).get('state');
|
||||
|
||||
fetch('/interaction/' + sessionId, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirect: 'follow',
|
||||
body: JSON.stringify(data),
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('error')!.innerText = "Error signing in";
|
||||
}
|
||||
}).catch(error => {
|
||||
document.getElementById('error')!.innerText = "Error signing in" + error;
|
||||
})
|
||||
});
|
||||
|
||||
const hidePassword = document.querySelector('.toggle-password-visibility')! as HTMLElement
|
||||
const showPassword = document.getElementById('eye-hide')! as HTMLElement
|
||||
const togglePasswordVisibility = () => {
|
||||
const passwordInput = document.getElementsByName('password')[0] as HTMLInputElement;
|
||||
if (passwordInput.type === 'password') {
|
||||
showPassword.style.display = 'block';
|
||||
hidePassword.style.display = 'none';
|
||||
passwordInput.type = 'text';
|
||||
} else {
|
||||
showPassword.style.display = 'none';
|
||||
hidePassword.style.display = 'block';
|
||||
passwordInput.type = 'password';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
hidePassword.addEventListener('click', togglePasswordVisibility);
|
||||
showPassword.addEventListener('click', togglePasswordVisibility);
|
||||
|
||||
|
||||
|
|
107
ui/src/style.css
107
ui/src/style.css
|
@ -3,26 +3,16 @@
|
|||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
--color-etherpad: #0f775b;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
|
@ -30,38 +20,12 @@ body {
|
|||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
margin: auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #3178c6aa);
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
|
@ -74,9 +38,11 @@ button {
|
|||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
|
@ -87,10 +53,73 @@ button:focus-visible {
|
|||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background-color: #f2f6f7;
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
color: #607278;
|
||||
}
|
||||
|
||||
body {
|
||||
background: radial-gradient(100% 100% at 50% 0%, var(--color-etherpad) 0%, #003A47 100%) fixed
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d1d1d1;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #f9f9f9;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.login-inner-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.login-inner-box input[type=submit] {
|
||||
background-color: var(--color-etherpad);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.password-label {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-label svg {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
#eye-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
label input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
|
@ -3,12 +3,15 @@ import { resolve } from 'path'
|
|||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: '/views/',
|
||||
build: {
|
||||
outDir: resolve(__dirname, '../src/static/oidc'),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
nested: resolve(__dirname, 'nested/index.html'),
|
||||
main: resolve(__dirname, 'consent.html'),
|
||||
nested: resolve(__dirname, 'login.html'),
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue