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. 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) # Showing Statistics (2022-11-16)
- The distillery nows shows generic statistics on the public homepage - The distillery nows shows generic statistics on the public homepage
- detailed statistics can be found on the admin interface - 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("- %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 return nil
} }

File diff suppressed because one or more lines are too long

View file

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

View file

@ -1,6 +1,9 @@
package info package info
import ( import (
"encoding/json"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/wisski" "github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -8,28 +11,48 @@ import (
"github.com/tkw1536/goprogram/stream" "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{ HandleInteractive func(info *Info, instance *wisski.WissKI, str stream.IOStream, params ...string) error
"snapshot": func(info *Info, instance *wisski.WissKI, str stream.IOStream) error { HandleResult func(info *Info, instance *wisski.WissKI, params ...string) (value any, err error)
return info.Exporter.MakeExport( }
str,
exporter.ExportTask{
Dest: "",
Instance: instance,
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 { "rebuild": {
return instance.Barrel().Build(str, true) 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 { "update": {
return instance.Drush().Update(str) 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 { "cron": {
return instance.Drush().Cron(str) 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 // read the slug
slug, ok := <-conn.Read() slug, ok := <-conn.Read()
if !ok { if !ok {
conn.WriteText("Error reading slug") <-conn.WriteText("Error reading slug")
return return
} }
// resolve the instance // resolve the instance
instance, err := info.Instances.WissKI(string(slug.Bytes)) instance, err := info.Instances.WissKI(string(slug.Bytes))
if err != nil { if err != nil {
conn.WriteText("Instance not found") <-conn.WriteText("Instance not found")
return 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 // build a stream
writer := &status.LineBuffer{ writer := &status.LineBuffer{
Line: func(line string) { Line: func(line string) {
@ -74,13 +115,31 @@ func (info *Info) handleInstanceAction(conn httpx.WebSocketConnection, action in
str := stream.NewIOStream(writer, writer, nil, 0) str := stream.NewIOStream(writer, writer, nil, 0)
// and perform the action // handle the interactive action
{ if action.HandleInteractive != nil {
err := action(info, instance, str) err := action.HandleInteractive(info, instance, str, params...)
if err != nil { if err != nil {
str.EPrintln(err) str.EPrintln(err)
return return
} }
str.Println("done") 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. // AssetsControlIndex contains assets for the 'ControlIndex' entrypoint.
var AssetsControlIndex = Assets{ 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">`, 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. // AssetsControlInstance contains assets for the 'ControlInstance' entrypoint.
var AssetsControlInstance = Assets{ 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">`, 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; return println;
} }
const elements = document.getElementsByClassName('remote-action') const remote_action = document.getElementsByClassName('remote-action')
Array.from(elements).forEach((element) => { Array.from(remote_action).forEach((element) => {
const action = element.getAttribute('data-action') as string; const action = element.getAttribute('data-action') as string;
const reload = element.hasAttribute('data-force-reload'); const reload = element.hasAttribute('data-force-reload');
const param = element.getAttribute('data-param') as string | undefined; const param = element.getAttribute('data-param') as string | undefined;
@ -123,3 +123,58 @@ Array.from(elements).forEach((element) => {
}); });
}); });
}) })
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 NoPrefixes bool // TODO: Move this into the database
Prefixes []string // list of prefixes Prefixes []string // list of prefixes
Pathbuilders map[string]string // all the pathbuilders Pathbuilders map[string]string // all the pathbuilders
Users []string // all the known users
} }
// Statistics holds statistics generated by the WissKI module // 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.Prefixes],
auto[*extras.Settings], auto[*extras.Settings],
auto[*extras.Pathbuilder], auto[*extras.Pathbuilder],
auto[*extras.Users],
auto[*extras.Stats], auto[*extras.Stats],
// info // info