Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build-admin": "tsc && vite build --outDir ../assets/js/admin --emptyOutDir",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"build-copy": "tsc && vite build --outDir ../src/templates/admin --emptyOutDir",
"preview": "vite preview"
Expand Down
43 changes: 1 addition & 42 deletions admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,18 @@ import {useEffect, useState} from 'react'
import './App.css'

import {NavLink, Outlet} from "react-router-dom";
import {useStore} from "./store/store.ts";
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
import {Trans, useTranslation} from "react-i18next";
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall, LucideMenu} from "lucide-react";
import settingSocket from "./utils/globals.ts";



export const App = () => {
const setSettings = useStore(state => state.setSettings);
const {t} = useTranslation()
const [sidebarOpen, setSidebarOpen] = useState<boolean>(true)

useEffect(() => {
document.title = t('admin.page-title')

useStore.getState().setShowLoading(true);

settingSocket.on('connect', () => {
});


settingSocket.on('connect', () => {
useStore.getState().setShowLoading(false)
settingSocket.emit('load', {});
console.log('connected');
});

settingSocket.on('disconnect', (reason: string) => {
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
useStore.getState().setShowLoading(true)
if (reason === 'io server disconnect') {
settingSocket.connect();
}
});

settingSocket.on('settings', (settings: {
results: string
}) => {
/* Check whether the settings.json is authorized to be viewed */
if (settings.results === 'NOT_ALLOWED') {
console.log('Not allowed to view settings.json')
return;
}

/* Check to make sure the JSON is clean before proceeding */
setSettings(JSON.stringify(settings.results));
useStore.getState().setShowLoading(false);
});

settingSocket.on('saveprogress', (status: string) => {
console.log(status)
})
}, []);

return <div id="wrapper" className={`${sidebarOpen ? '': 'closed' }`}>
Expand Down
1 change: 1 addition & 0 deletions admin/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./utils/authUtils"
import './utils/globals.ts'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
Expand Down
2 changes: 1 addition & 1 deletion admin/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type StoreState = {
export const useStore = create<StoreState>()((set) => ({
settings: undefined,
setSettings: (settings: string) => set({settings}),
showLoading: false,
showLoading: true,
setShowLoading: (show: boolean) => set({showLoading: show}),
setToastState: (val )=>set({toastState: val}),
toastState: {
Expand Down
35 changes: 24 additions & 11 deletions admin/src/utils/authUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ async function generateCodeChallenge(verifier: string) {
const state = generateState();

const config = getConfigFromHtmlFile();
console.error("Config is", config)


const token = sessionStorage.getItem('token')
Expand Down Expand Up @@ -91,33 +92,40 @@ export function decodeJwt(token: string): Record<string, unknown> | null {


if (!isExpired(token, 60) && token) {
console.log('Existing token is valid')
let refreshToken = sessionStorage.getItem('refresh_token')
if (!refreshToken) {
sessionStorage.clear()
window.location.reload()
throw new Error('Refresh token not set')
}
setInterval(() => {
refreshToken = sessionStorage.getItem('refresh_token') || refreshToken!
fetch(config?.authority + "/../token", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: token,
refresh_token: refreshToken,
client_id: config?.clientId ?? ''
})
}).then((refreshResp) => {
if (refreshResp.ok) {
refreshResp.json().then((refreshData) => {
if (refreshData.id_token) {
sessionStorage.setItem('refresh_token', refreshData.refresh_token)
sessionStorage.setItem('token', refreshData.id_token)
}
})
}
})
}, 50000)
}, 60_000)
} else {
if (window.location.search.includes('code=')) {
console.log('Redirecting to', window.location.href, "with client" + config?.clientId)
try {
// TODO const codeVerifier = sessionStorage.getItem('pkce_code_verifier') || '';
const codeVerifier = sessionStorage.getItem('pkce_code_verifier') || '';
const resp = await fetch(config?.authority + "/../token", {
method: 'POST',
headers: {
Expand All @@ -128,10 +136,11 @@ if (!isExpired(token, 60) && token) {
code: new URLSearchParams(window.location.search).get('code') || '',
redirect_uri: config?.redirectUri ?? '',
client_id: config?.clientId ?? '',
code_verifier: codeVerifier
})
})
if (resp.ok) {
const tokenResponse = await resp.json()
let tokenResponse = await resp.json()
if (tokenResponse.id_token) {
sessionStorage.setItem('token', tokenResponse.id_token)
const params = new URLSearchParams(window.location.search);
Expand All @@ -145,7 +154,9 @@ if (!isExpired(token, 60) && token) {
window.history.replaceState({}, '', newUrl);
}
setInterval(() => {
console.log('Redirecting to', window.location.href, "with client" + config?.clientId);
if (tokenResponse.refresh_token) {
console.log(config)
fetch(config?.authority + "/../token", {
method: 'POST',
headers: {
Expand All @@ -156,17 +167,19 @@ if (!isExpired(token, 60) && token) {
refresh_token: tokenResponse.refresh_token,
client_id: config?.clientId ?? ''
})
}).then((refreshResp) => {
}).then(async (refreshResp) => {
if (refreshResp.ok) {
refreshResp.json().then((refreshData) => {
tokenResponse = refreshData;
if (refreshData.id_token) {
sessionStorage.setItem('refresh_token', refreshData.refresh_token)
sessionStorage.setItem('token', refreshData.id_token)
}
})
}
})
}
}, 50000)
}, 60_000)
} else {
throw new Error('Error during OIDC login: ' + resp.statusText)
}
Expand All @@ -177,12 +190,12 @@ if (!isExpired(token, 60) && token) {
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem('pkce_code_verifier', codeVerifier);
const codeChallenge = await generateCodeChallenge(codeVerifier)
const scope = config?.scope.map(s => {
/*const scope = config?.scope.map(s => {
return "scope=" + encodeURIComponent(s)
}).join("&")

}).join("&")*/
const scope = "scope=" + encodeURIComponent((config?.scope ?? []).join(' '))
console.log("Scopes are", scope)
const requestUrl = `${config?.authority + "auth"}?client_id=${config?.clientId}&redirect_uri=${encodeURIComponent(config?.redirectUri ?? '')}&response_type=code&${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256&state=${state}`
console.error(requestUrl)
window.location.replace(requestUrl)
}
}
Expand Down
31 changes: 31 additions & 0 deletions admin/src/utils/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package utils

import (
"github.com/ether/etherpad-go/lib/apool"
"github.com/ether/etherpad-go/lib/models/revision"
)

func CreateRevision(changeset string, timestamp int64, isKeyRev bool, authorId *string, atext apool.AText, attribPool apool.APool) revision.Revision {
if authorId != nil {
attribPool.PutAttrib(apool.Attribute{
Key: "author",
Value: *authorId,
}, nil)
}

rev := revision.Revision{
Changeset: changeset,
Meta: revision.RevisionMeta{
Author: authorId,
Timestamp: timestamp,
},
}

if isKeyRev {
rev.Meta.Atext = &atext
rev.Meta.APool = &attribPool
}
rev.Meta.IsKeyRev = isKeyRev

return rev
}
40 changes: 39 additions & 1 deletion admin/src/utils/globals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
import {connect} from "./socketio.ts";
import {useStore} from "../store/store.ts";

const settingSocket = connect(`settings`);
const settingSocket = connect();

settingSocket.on('connect', () => {
});


settingSocket.on('connect', () => {
useStore.getState().setShowLoading(false)
settingSocket.emit('load', {});
console.log('connected');
});

settingSocket.on('disconnect', (reason: string) => {
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
useStore.getState().setShowLoading(true)
if (reason === 'io server disconnect') {
settingSocket.connect();
}
});

settingSocket.on('settings', (settings: {
results: string
}) => {
/* Check whether the settings.json is authorized to be viewed */
if (settings.results === 'NOT_ALLOWED') {
console.log('Not allowed to view settings.json')
return;
}

/* Check to make sure the JSON is clean before proceeding */
useStore.getState().setSettings(JSON.stringify(settings.results));
useStore.getState().setShowLoading(false);
});

settingSocket.on('saveprogress', (status: string) => {
console.log(status)
})

export default settingSocket
13 changes: 5 additions & 8 deletions admin/src/utils/socketIoWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
// typescript
export function createSocket(path: string): WebSocket {
export function createSocket(): WebSocket {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const url = `${protocol}//${window.location.host}/admin/ws?namespace=${encodeURIComponent(path)}`
const url = `${protocol}//${window.location.host}/admin/ws?token=${sessionStorage.getItem('token') || ''}`
return new WebSocket(url)
}

export class SocketIoWrapper {
private socket: WebSocket
private static readonly eventCallbacks: { [key: string]: Function[] } = {}
private readonly namespace: string
private queueMessages: Array<{ event: string; data?: any }> = []

constructor(namespace: string) {
this.namespace = namespace
constructor() {
try {
this.socket = createSocket(namespace)
this.socket = createSocket()
} catch (e) {
console.error('WebSocket creation failed:', e)
throw e
Expand Down Expand Up @@ -73,7 +70,7 @@ export class SocketIoWrapper {
setTimeout(() => {
console.log('Reconnecting...')
try {
const socket = createSocket(this.namespace)
const socket = createSocket()
socket.onopen = this.handleOpen
socket.onclose = this.handleClose
socket.onerror = this.handleError
Expand Down
4 changes: 2 additions & 2 deletions admin/src/utils/socketio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ declare global {
* @return socket.io Socket object
* @param namespace
*/
export const connect = (namespace: string) => {
export const connect = () => {
// The API for socket.io's io() function is awkward. The documentation says that the first
// argument is a URL, but it is not the URL of the socket.io endpoint. The URL's path part is used
// as the name of the socket.io namespace to join, and the rest of the URL (including query
// parameters, if present) is combined with the `path` option (which defaults to '/socket.io', but
// is overridden here to allow users to host Etherpad at something like '/etherpad') to get the
// URL of the socket.io endpoint.

window.socket = new SocketIoWrapper(namespace)
window.socket = new SocketIoWrapper()

window.socket.on('connect_error', (error: any) => {
console.log('Error connecting to pad', error);
Expand Down
23 changes: 21 additions & 2 deletions admin/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ function chartingLibrary(): PluginOption {
enforce: 'pre',
apply: 'serve',
transformIndexHtml: async (html, ctx)=>{
const resp = await fetch('http://localhost:3000/admin/index.html')
return await resp.text()
return html.replace('<div id="loading"></div>', 'div id="loading"></div><span id="config" data-config="{&#34;authority&#34;:&#34;http://localhost:3000/oauth2/&#34;,&#34;clientId&#34;:&#34;admin_client&#34;,&#34;jwksUri&#34;:&#34;http://localhost:3000/oauth2/.well-known/jwks.json&#34;,&#34;redirectUri&#34;:&#34;http://localhost:5173/admin/&#34;,&#34;scope&#34;:[&#34;openid&#34;,&#34;profile&#34;,&#34;email&#34;,&#34;offline&#34;]}"></span>')
}
};
}
Expand Down Expand Up @@ -40,6 +39,26 @@ export default defineConfig({
'/admin-auth/': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/admin/locales': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/p/': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/js/pad': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/css': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/socket.io/': {
target: 'http://localhost:3000',
changeOrigin: true,
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions assets/login/login.templ
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package login
import settings "github.com/ether/etherpad-go/lib/settings"


templ Login(client settings.SSOClient, errorMessage *string) {
templ Login(client settings.SSOClient, scopes []string, errorMessage *string) {
<html lang="de">
<head>
<title>Login Etherpad</title>
Expand All @@ -21,9 +21,9 @@ templ Login(client settings.SSOClient, errorMessage *string) {
<input id="password" required class="login-textinput input-control" placeholder="Password" type="text" name="password" />
<input class="login-button" type="submit" value="Login"/>
<ul hidden="hidden">
<li><input name="scopes" type="hidden" value="openid"></li>
<li><input name="scopes" type="hidden" value="email"></li>
<li><input name="scopes" type="hidden" value="profile"></li>
for _, scope := range scopes {
<li><input name="scopes" type="hidden" value={scope}></li>
}
</ul>
if errorMessage != nil {
<div class="login-error-message">{*errorMessage}</div>
Expand Down
Loading