Add user login to admin interface

This commit is contained in:
Tom Wiesing 2022-11-23 16:57:09 +01:00
parent dbe494751a
commit 82bfc15057
No known key found for this signature in database
15 changed files with 256 additions and 79 deletions

View file

@ -2,6 +2,9 @@
This file contains signficant news items for the distillery.
# Login using Distillery Administration (2022-11-23)
- The admin interface now allows login to individual user accounts
# Showing Statistics (2022-11-16)
- The distillery nows shows generic statistics on the public homepage
- detailed statistics can be found on the admin interface

View file

@ -96,5 +96,10 @@ func (i info) Run(context wisski_distillery.Context) error {
context.Printf("- %s (%d bytes)\n", name, len(data))
})
context.Printf("Users: (count %d)\n", len(info.Users))
for _, user := range info.Users {
context.Printf("- %s\n", user)
}
return nil
}

File diff suppressed because one or more lines are too long

View file

@ -177,31 +177,34 @@
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<!--
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Composer Status
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
???
</td>
<td>
???
</td>
</tr>
</tbody>
</table>
</div>
</div>
-->
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Users
</th>
</tr>
</thead>
<tbody>
{{ $slug := .Instance.Slug }}
{{ range $index, $user := .Info.Users }}
<tr>
<td>
<code>{{ $user }}</code>
</td>
<td>
<small>
<button class="remote-link pure-button pure-button-action" role="link" data-action="login" data-params="{{ $slug }} {{ $user }} ">Login in new window</button>
</small>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">

View file

@ -1,6 +1,9 @@
package info
import (
"encoding/json"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -8,28 +11,48 @@ import (
"github.com/tkw1536/goprogram/stream"
)
type instanceActionFunc = func(info *Info, instance *wisski.WissKI, str stream.IOStream) error
type InstanceAction struct {
NumParams int
var socketInstanceActions = map[string]instanceActionFunc{
"snapshot": func(info *Info, instance *wisski.WissKI, str stream.IOStream) error {
return info.Exporter.MakeExport(
str,
exporter.ExportTask{
Dest: "",
Instance: instance,
HandleInteractive func(info *Info, instance *wisski.WissKI, str stream.IOStream, params ...string) error
HandleResult func(info *Info, instance *wisski.WissKI, params ...string) (value any, err error)
}
StagingOnly: false,
},
)
var socketInstanceActions = map[string]InstanceAction{
"snapshot": {
HandleInteractive: func(info *Info, instance *wisski.WissKI, str stream.IOStream, params ...string) error {
return info.Exporter.MakeExport(
str,
exporter.ExportTask{
Dest: "",
Instance: instance,
StagingOnly: false,
},
)
},
},
"rebuild": func(_ *Info, instance *wisski.WissKI, str stream.IOStream) error {
return instance.Barrel().Build(str, true)
"rebuild": {
HandleInteractive: func(_ *Info, instance *wisski.WissKI, str stream.IOStream, params ...string) error {
return instance.Barrel().Build(str, true)
},
},
"update": func(_ *Info, instance *wisski.WissKI, str stream.IOStream) error {
return instance.Drush().Update(str)
"update": {
HandleInteractive: func(_ *Info, instance *wisski.WissKI, str stream.IOStream, params ...string) error {
return instance.Drush().Update(str)
},
},
"cron": func(_ *Info, instance *wisski.WissKI, str stream.IOStream) error {
return instance.Drush().Cron(str)
"cron": {
HandleInteractive: func(_ *Info, instance *wisski.WissKI, str stream.IOStream, params ...string) error {
return instance.Drush().Cron(str)
},
},
"login": {
NumParams: 1,
HandleResult: func(_ *Info, instance *wisski.WissKI, params ...string) (any, error) {
link, err := instance.Drush().Login(stream.FromNil(), params[0])
return link, err
},
},
}
@ -47,22 +70,40 @@ func (info *Info) serveSocket(conn httpx.WebSocketConnection) {
}
}
func (info *Info) handleInstanceAction(conn httpx.WebSocketConnection, action instanceActionFunc) {
var instanceParamsTimeout = time.Second
func (info *Info) handleInstanceAction(conn httpx.WebSocketConnection, action InstanceAction) {
// read the slug
slug, ok := <-conn.Read()
if !ok {
conn.WriteText("Error reading slug")
<-conn.WriteText("Error reading slug")
return
}
// resolve the instance
instance, err := info.Instances.WissKI(string(slug.Bytes))
if err != nil {
conn.WriteText("Instance not found")
<-conn.WriteText("Instance not found")
return
}
// read the parameters
params := make([]string, action.NumParams)
for i := range params {
select {
case message, ok := <-conn.Read():
if !ok {
<-conn.WriteText("Insufficient parameters")
return
}
params[i] = string(message.Bytes)
case <-time.After(instanceParamsTimeout):
<-conn.WriteText("Timed out reading parameters")
return
}
}
// build a stream
writer := &status.LineBuffer{
Line: func(line string) {
@ -74,13 +115,31 @@ func (info *Info) handleInstanceAction(conn httpx.WebSocketConnection, action in
str := stream.NewIOStream(writer, writer, nil, 0)
// and perform the action
{
err := action(info, instance, str)
// handle the interactive action
if action.HandleInteractive != nil {
err := action.HandleInteractive(info, instance, str, params...)
if err != nil {
str.EPrintln(err)
return
}
str.Println("done")
}
// handle the result computation
if action.HandleResult != nil {
result, err := action.HandleResult(info, instance, params...)
if err != nil {
str.Println("false")
return
}
data, err := json.Marshal(result)
if err != nil {
str.Println("false")
return
}
data = append(data, "\n"...)
str.Println("true")
str.Stdout.Write(data)
}
}

View file

@ -16,13 +16,13 @@ var AssetsComponentsIndex = Assets{
// AssetsControlIndex contains assets for the 'ControlIndex' entrypoint.
var AssetsControlIndex = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlIndex.6d1d8ee0.js"></script><script src="/static/ControlIndex.03d7b00f.js" nomodule="" defer></script>`,
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlIndex.a72fc239.js"></script><script src="/static/ControlIndex.75d2a312.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css">`,
}
// AssetsControlInstance contains assets for the 'ControlInstance' entrypoint.
var AssetsControlInstance = Assets{
Scripts: `<script nomodule="" defer src="/static/ControlIndex.03d7b00f.js"></script><script type="module" src="/static/ControlIndex.6d1d8ee0.js"></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlInstance.66b95713.js"></script><script src="/static/ControlInstance.9cc7166d.js" nomodule="" defer></script>`,
Scripts: `<script nomodule="" defer src="/static/ControlIndex.75d2a312.js"></script><script type="module" src="/static/ControlIndex.a72fc239.js"></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlInstance.66b95713.js"></script><script src="/static/ControlInstance.9cc7166d.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css"><link rel="stylesheet" href="/static/ControlInstance.38d394c2.css">`,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -46,8 +46,8 @@ function makeTextBuffer(target: HTMLElement, scrollContainer: HTMLElement, size:
return println;
}
const elements = document.getElementsByClassName('remote-action')
Array.from(elements).forEach((element) => {
const remote_action = document.getElementsByClassName('remote-action')
Array.from(remote_action).forEach((element) => {
const action = element.getAttribute('data-action') as string;
const reload = element.hasAttribute('data-force-reload');
const param = element.getAttribute('data-param') as string | undefined;
@ -122,4 +122,59 @@ Array.from(elements).forEach((element) => {
close();
});
});
})
})
const remote_link = document.getElementsByClassName('remote-link')
Array.from(remote_link).forEach((element) => {
const action = element.getAttribute('data-action') as string;
const param = element.getAttribute('data-params') as string | undefined;
const params = param?.split(" ");
element.addEventListener('click', function (ev) {
ev.preventDefault();
getValue(action, params).then(v => {
window.open(v);
}).catch(e => {
console.error(e);
})
});
})
async function getValue(action: string, params?: Array<string>): Promise<any> {
return new Promise((rs, rj) => {
let buffer = "";
var resolve = function() {
const index = buffer.indexOf('\n')
if (index < 0) {
rj("invalid buffer");
return
}
// check that the server sent back true
const ok = buffer.substring(0, index) === 'true';
if(!ok) {
rj(buffer);
return
}
// parse the rest as json
const value = JSON.parse(buffer.substring(index+1))
rs(value);
}
connectSocket((socket) => {
socket.send(action);
if (params) {
params.forEach(p => socket.send(p))
}
}, (data) => {
buffer += data + "\n";
}).then(() => {
resolve();
}).catch(() => {
buffer = "false\n";
resolve();
});
})
}

View file

@ -36,6 +36,7 @@ type WissKI struct {
NoPrefixes bool // TODO: Move this into the database
Prefixes []string // list of prefixes
Pathbuilders map[string]string // all the pathbuilders
Users []string // all the known users
}
// Statistics holds statistics generated by the WissKI module

View file

@ -0,0 +1,34 @@
package extras
import (
_ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/phpx"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
)
type Users struct {
ingredient.Base
PHP *php.PHP
}
//go:embed users.php
var usersPHP string
// All returns all known usernames
func (u *Users) All(server *phpx.Server) (users []string, err error) {
err = u.PHP.ExecScript(server, &users, usersPHP, "list_users")
return
}
func (u *Users) Fetch(flags ingredient.FetcherFlags, info *status.WissKI) (err error) {
if flags.Quick {
return
}
info.Users, _ = u.All(flags.Server)
return
}

View file

@ -0,0 +1,16 @@
<?php
use Drupal\user\Entity\User;
/** lists all the users */
function list_users() {
$usernames = [];
$users = User::loadMultiple(NULL);
foreach($users as $user){
$name = $user->get('name')->getString();
if(empty($name)) continue;
$usernames[] = $name;
}
return $usernames;
}

View file

@ -99,6 +99,7 @@ func (wisski *WissKI) allIngredients() []initFunc {
auto[*extras.Prefixes],
auto[*extras.Settings],
auto[*extras.Pathbuilder],
auto[*extras.Users],
auto[*extras.Stats],
// info