Added working oauth2.

This commit is contained in:
SamTV12345 2024-03-25 15:34:37 +01:00
parent 04e4a5eee0
commit c966e12926
19 changed files with 401 additions and 198 deletions

1
.gitignore vendored
View file

@ -27,3 +27,4 @@ plugin_packages
/src/test-results /src/test-results
playwright-report playwright-report
state.json state.json
/src/static/oidc

View file

@ -8,8 +8,10 @@ FROM node:alpine as adminBuild
WORKDIR /opt/etherpad-lite WORKDIR /opt/etherpad-lite
COPY ./admin ./admin COPY ./admin ./admin
COPY ./ui ./ui
COPY ./src/locales ./src/locales COPY ./src/locales ./src/locales
RUN cd ./admin && npm install -g pnpm && pnpm install && pnpm run build --outDir ./dist 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 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 ./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/admin/dist ./src/templates/admin
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/ui/dist ./src/static/oidc
RUN bin/installDeps.sh && \ RUN bin/installDeps.sh && \
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \ 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 ./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/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 && \ RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \ if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \

View file

@ -15,7 +15,8 @@ export default defineConfig({
})], })],
base: '/admin', base: '/admin',
build:{ build:{
outDir: '../src/templates/admin' outDir: '../src/templates/admin',
emptyOutDir: true,
}, },
server:{ server:{
proxy: { proxy: {

View file

@ -650,5 +650,23 @@
/* /*
* Enable/Disable case-insensitive pad names. * 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/*"]
}
]
}
} }

View file

@ -650,5 +650,24 @@
/* /*
* Enable/Disable case-insensitive pad names. * 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/*"]
}
]
}
} }

View file

@ -5,36 +5,37 @@ import MemoryAdapter from "./OIDCAdapter";
import path from "path"; import path from "path";
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
import {IncomingForm} from 'formidable' import {IncomingForm} from 'formidable'
import {Request, Response} from 'express'; import express, {Request, Response} from 'express';
import {format} from 'url' import {format} from 'url'
import {ParsedUrlQuery} from "node:querystring"; import {ParsedUrlQuery} from "node:querystring";
import cors from 'cors'
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
const configuration: Configuration = { 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'], scopes: ['openid', 'profile', 'email'],
findAccount: async (ctx, id) => { 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 { return {
accountId: id, accountId: id,
claims: () => ({ claims: () => ({
sub: id, sub: id,
test: "test",
admin: account?.is_admin
}) })
} as Account } as Account
}, },
ttl:{ ttl:{
AccessToken: 1 * 60 * 60, // 1 hour in seconds AccessToken: 1 * 60 * 60, // 1 hour in seconds
AuthorizationCode: 10 * 60, // 10 minutes in seconds AuthorizationCode: 10 * 60, // 10 minutes in seconds
@ -42,6 +43,12 @@ const configuration: Configuration = {
IdToken: 1 * 60 * 60, // 1 hour in seconds IdToken: 1 * 60 * 60, // 1 hour in seconds
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
}, },
claims: {
openid: ['sub'],
email: ['email'],
profile: ['name'],
admin: ['admin']
},
cookies: { cookies: {
keys: ['oidc'], 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) => { export const expressCreateServer = async (hookName: string, args: ArgsExpressType, cb: Function) => {
const {privateKey} = await generateKeyPair('RS256'); const {privateKey} = await generateKeyPair('RS256');
const privateKeyJWK = await exportJWK(privateKey); 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', { const oidc = new Provider('http://localhost:9001', {
...configuration, jwks: { ...configuration, jwks: {
keys: [ keys: [
privateKeyJWK 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(); const formid = new IncomingForm();
try { try {
// @ts-ignore
const {login, password} = (await formid.parse(req))[0] 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) { switch (prompt.name) {
case 'login': { case 'login': {
@ -110,6 +182,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
} }
if (prompt.details.missingOIDCScope) { if (prompt.details.missingOIDCScope) {
// @ts-ignore
grant!.addOIDCScope(prompt.details.missingOIDCScope.join(' ')); grant!.addOIDCScope(prompt.details.missingOIDCScope.join(' '));
} }
if (prompt.details.missingOIDCClaims) { if (prompt.details.missingOIDCClaims) {
@ -128,13 +201,13 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
} }
} }
await next(); await next();
} catch (err) { } catch (err:any) {
return res.writeHead(500).end(err.message); 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 { try {
const { const {
uid, prompt, params, session, uid, prompt, params, session,
@ -145,14 +218,14 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
switch (prompt.name) { switch (prompt.name) {
case 'login': { case 'login': {
res.redirect(format({ res.redirect(format({
pathname: '/views/login', pathname: '/views/login.html',
query: params as ParsedUrlQuery query: params as ParsedUrlQuery
})) }))
break break
} }
case 'consent': { case 'consent': {
res.redirect(format({ res.redirect(format({
pathname: '/views/consent', pathname: '/views/consent.html',
query: params as ParsedUrlQuery query: params as ParsedUrlQuery
})) }))
break break
@ -166,13 +239,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
}); });
args.app.get('/views/login', async (req, res) => { args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24}));
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'));
})
/* /*
oidc.on('authorization.error', (ctx, error) => { oidc.on('authorization.error', (ctx, error) => {

View file

@ -4,7 +4,7 @@ import type {Adapter, AdapterPayload} from "oidc-provider";
const options = { const options = {
max: 500, max: 500,
sizeCalculation: (item, key) => { sizeCalculation: (item:any, key:any) => {
return 1 return 1
}, },
// for use with tracking overall storage size // for use with tracking overall storage size
@ -43,7 +43,6 @@ class MemoryAdapter implements Adapter{
} }
destroy(id:string) { destroy(id:string) {
console.log("destroy", id);
const key = this.key(id); const key = this.key(id);
const found = storage.get(key) as AdapterPayload; const found = storage.get(key) as AdapterPayload;
@ -61,13 +60,11 @@ class MemoryAdapter implements Adapter{
} }
consume(id: string) { consume(id: string) {
console.log("consume", id);
(storage.get(this.key(id)) as AdapterPayload)!.consumed = epochTime(); (storage.get(this.key(id)) as AdapterPayload)!.consumed = epochTime();
return Promise.resolve(); return Promise.resolve();
} }
find(id: string): Promise<AdapterPayload | void | undefined> { find(id: string): Promise<AdapterPayload | void | undefined> {
const foundSession = storage.get(this.key(id)) as AdapterPayload;
if (storage.has(this.key(id))){ if (storage.has(this.key(id))){
return Promise.resolve<AdapterPayload>(storage.get(this.key(id)) as AdapterPayload); return Promise.resolve<AdapterPayload>(storage.get(this.key(id)) as AdapterPayload);
} }
@ -88,7 +85,6 @@ class MemoryAdapter implements Adapter{
accountId: string; accountId: string;
loginTs: number; loginTs: number;
}, expiresIn: number) { }, expiresIn: number) {
console.log("upsert", payload);
const key = this.key(id); const key = this.key(id);
storage.set(key, payload, {ttl: expiresIn * 1000}); storage.set(key, payload, {ttl: expiresIn * 1000});

View file

@ -342,6 +342,11 @@ exports.requireAuthentication = false;
exports.requireAuthorization = false; exports.requireAuthorization = false;
exports.users = {}; exports.users = {};
/*
* This setting is used for configuring sso
*/
exports.sso = {}
/* /*
* Show settings in admin page, by default it is true * Show settings in admin page, by default it is true
*/ */

View file

@ -34,6 +34,7 @@
"axios": "^1.6.8", "axios": "^1.6.8",
"clean-css": "^5.3.3", "clean-css": "^5.3.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"ejs": "^3.1.9", "ejs": "^3.1.9",
"etherpad-require-kernel": "^1.0.16", "etherpad-require-kernel": "^1.0.16",
@ -83,6 +84,7 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.42.1", "@playwright/test": "^1.42.1",
"@types/async": "^3.2.24", "@types/async": "^3.2.24",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/formidable": "^3.4.5", "@types/formidable": "^3.4.5",
"@types/http-errors": "^2.0.4", "@types/http-errors": "^2.0.4",

View file

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

View file

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

View file

@ -2,12 +2,23 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title> <title>Consent Etherpad</title>
</head> </head>
<body> <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> <script type="module" src="/src/consent.ts"></script>
</body> </body>
</html> </html>

View file

@ -2,12 +2,37 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <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"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Vite + TS</title> <title>SSO Etherpad</title>
</head> </head>
<body> <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> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

View file

@ -10,6 +10,7 @@
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.2.0" "vite": "^5.2.0",
"vite-plugin-singlefile": "^2.0.1"
} }
} }

View file

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

View file

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

View file

@ -1,3 +1,58 @@
import './style.css' 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);

View file

@ -3,26 +3,16 @@
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} --color-etherpad: #0f775b;
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
} }
body { body {
font-size: 16px;
margin: 0; margin: 0;
display: flex; display: flex;
place-items: center; place-items: center;
@ -30,38 +20,12 @@ body {
min-height: 100vh; min-height: 100vh;
} }
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app { #app {
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: auto;
padding: 2rem; 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 { button {
border-radius: 8px; border-radius: 8px;
@ -74,9 +38,11 @@ button {
cursor: pointer; cursor: pointer;
transition: border-color 0.25s; transition: border-color 0.25s;
} }
button:hover { button:hover {
border-color: #646cff; border-color: #646cff;
} }
button:focus, button:focus,
button:focus-visible { button:focus-visible {
outline: 4px auto -webkit-focus-ring-color; outline: 4px auto -webkit-focus-ring-color;
@ -87,10 +53,73 @@ button:focus-visible {
color: #213547; color: #213547;
background-color: #ffffff; background-color: #ffffff;
} }
a:hover { a:hover {
color: #747bff; color: #747bff;
} }
button { button {
background-color: #f9f9f9; 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;
}

View file

@ -3,12 +3,15 @@ import { resolve } from 'path'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
base: '/views/',
build: { build: {
outDir: resolve(__dirname, '../src/static/oidc'),
rollupOptions: { rollupOptions: {
input: { input: {
main: resolve(__dirname, 'index.html'), main: resolve(__dirname, 'consent.html'),
nested: resolve(__dirname, 'nested/index.html'), nested: resolve(__dirname, 'login.html'),
}, },
}, },
emptyOutDir: true,
}, },
}) })