Merge branch 'next' of github.com:OliveTin/OliveTin into next

This commit is contained in:
jamesread 2026-03-02 00:35:05 +00:00
commit 0412b9ea1d
6 changed files with 131 additions and 19 deletions

View File

@ -1,5 +1,6 @@
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { WebLinksAddon } from '@xterm/addon-web-links'
import { Mutex } from './Mutex.js'
/**
@ -18,13 +19,24 @@ export class OutputTerminal {
constructor (executionTrackingId) {
this.executionTrackingId = executionTrackingId
this.writeMutex = new Mutex()
const linkHandler = {
activate (event, text, _range) {
event.preventDefault()
window.open(text, '_blank')
}
}
this.terminal = new Terminal({
convertEol: true
convertEol: true,
linkHandler
})
const fitAddon = new FitAddon()
this.terminal.loadAddon(fitAddon)
this.terminal.fit = fitAddon
this.terminal.loadAddon(new WebLinksAddon((event, uri) => linkHandler.activate(event, uri)))
this.linkHandlerConfigured = true
}
async write (out, then) {

View File

@ -15,6 +15,7 @@
"@hugeicons/vue": "^1.0.4",
"@vitejs/plugin-vue": "^6.0.4",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"iconify-icon": "^3.0.2",
"picocrank": "^1.14.0",
@ -1570,6 +1571,12 @@
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
"license": "MIT"
},
"node_modules/@xterm/addon-web-links": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
"license": "MIT"
},
"node_modules/@xterm/xterm": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",

View File

@ -28,6 +28,7 @@
"@hugeicons/vue": "^1.0.4",
"@vitejs/plugin-vue": "^6.0.4",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"iconify-icon": "^3.0.2",
"picocrank": "^1.14.0",

View File

@ -0,0 +1,13 @@
#
# Integration Test Config: xterm link handling
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: Echo URL
shell: echo "See https://example.com for more info"
popupOnStart: execution-dialog-stdout-only

View File

@ -0,0 +1,69 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, Condition } from 'selenium-webdriver'
import {
getRootAndWait,
takeScreenshotOnFailure,
getTerminalBuffer,
} from '../../lib/elements.js'
describe('config: xtermLinkHandling', function () {
before(async function () {
await runner.start('xtermLinkHandling')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver)
})
it('xterm output shows URL and link handling is configured', async function () {
await getRootAndWait()
await webdriver.wait(new Condition('wait for Echo URL button', async () => {
const btns = await webdriver.findElements(By.css('[title="Echo URL"]'))
return btns.length === 1
}), 10000)
const echoUrlButton = await webdriver.findElement(By.css('[title="Echo URL"]'))
await echoUrlButton.click()
await webdriver.wait(new Condition('wait for execution view', async () => {
const url = await webdriver.getCurrentUrl()
return url.includes('/logs/') && !url.endsWith('/logs')
}), 10000)
await webdriver.wait(new Condition('wait for execution status', async () => {
const statusElements = await webdriver.findElements(By.id('execution-dialog-status'))
return statusElements.length > 0
}), 5000)
await webdriver.wait(new Condition('wait for execution to finish', async () => {
try {
const statusElement = await webdriver.findElement(By.id('execution-dialog-status'))
const statusText = await statusElement.getText()
return !statusText.includes('Executing')
} catch (e) {
return false
}
}), 5000)
await webdriver.sleep(500)
const bufferText = await getTerminalBuffer()
expect(bufferText).to.not.be.null
expect(bufferText).to.include('https://example.com')
const linkHandlerSet = await webdriver.executeScript(`
try {
return !!(window.terminal && window.terminal.linkHandlerConfigured === true)
} catch (e) {
return false
}
`)
expect(linkHandlerSet).to.equal(true)
})
})

View File

@ -11,6 +11,7 @@ import (
"io"
"net/http"
"os"
"sync"
"time"
authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
@ -21,6 +22,7 @@ import (
type OAuth2Handler struct {
cfg *config.Config
mu sync.RWMutex
registeredStates map[string]*oauth2State
registeredProviders map[string]*oauth2.Config
}
@ -144,11 +146,13 @@ func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request)
return
}
h.mu.Lock()
h.registeredStates[state] = &oauth2State{
providerConfig: provider,
providerName: providerName,
Username: "",
}
h.mu.Unlock()
h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
@ -177,7 +181,9 @@ func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.
return nil, state, false
}
h.mu.RLock()
registeredState, ok := h.registeredStates[state]
h.mu.RUnlock()
if !ok {
log.Errorf("State not found in server: %v", state)
http.Error(w, "State not found in server", http.StatusBadRequest)
@ -287,8 +293,10 @@ func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Reque
userInfoClient := h.createUserInfoClient(ctx, registeredState.providerConfig, tok, clientSettings)
userinfo := getUserInfo(h.cfg, userInfoClient, providerConfig)
h.mu.Lock()
h.registeredStates[state].Username = userinfo.Username
h.registeredStates[state].Usergroup = h.computeUsergroup(userinfo, providerConfig)
h.mu.Unlock()
http.Redirect(w, r, "/", http.StatusFound)
}
@ -366,34 +374,36 @@ func getDataField(data map[string]any, field string) string {
return stringVal
}
func (h *OAuth2Handler) lookupOAuth2UserByState(state string) (*authTypes.AuthenticatedUser, bool) {
h.mu.RLock()
serverState, found := h.registeredStates[state]
if !found {
h.mu.RUnlock()
return nil, false
}
user := &authTypes.AuthenticatedUser{
Username: serverState.Username,
UsergroupLine: serverState.Usergroup,
Provider: "oauth2",
SID: state,
}
h.mu.RUnlock()
return user, true
}
func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
cookie, err := context.Request.Cookie("olivetin-sid-oauth")
user := &authTypes.AuthenticatedUser{}
if err != nil {
if err != nil || cookie.Value == "" {
return nil
}
if cookie.Value == "" {
return nil
}
serverState, found := h.registeredStates[cookie.Value]
user, found := h.lookupOAuth2UserByState(cookie.Value)
if !found {
log.WithFields(log.Fields{
"sid": cookie.Value,
"provider": "oauth2",
}).Warnf("Stale session")
return nil
}
user.Username = serverState.Username
user.UsergroupLine = serverState.Usergroup
user.Provider = "oauth2"
user.SID = cookie.Value
return user
}