Merge branch 'next' of github.com:OliveTin/OliveTin into next
This commit is contained in:
commit
0412b9ea1d
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -28,13 +28,14 @@
|
|||
"@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",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.29",
|
||||
"vue": "^3.5.29",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^5.0.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue