Implement basic API scoping

This commit is contained in:
Tom 2023-05-04 15:13:51 +02:00
parent 064ae2f564
commit 9db53d39c4
21 changed files with 519 additions and 264 deletions

View file

@ -0,0 +1,175 @@
// Package api implements a common handler used by the api routes
package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/config"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/lazy"
)
// Handler represents an API handler that returns a REST response.
// The response is automatically marshaled using T.
type Handler[T any] struct {
Config *config.Config
Auth *auth.Auth // Handler to handle Auth
Methods []string // HTTP methods to allow
methods lazy.Lazy[map[string]struct{}]
Scope scopes.Scope
ScopeParam func(*http.Request) string
Handler func(string, *http.Request) (T, error)
}
var apiNotEnabled = &Response{
Status: http.StatusNotImplemented,
Message: "API is not implemented on this server",
}
var apiMethodNotAllowed = &Response{
Status: http.StatusMethodNotAllowed,
Message: "method not allowed",
}
var apiInternalServerError = &Response{
Status: http.StatusInternalServerError,
Message: "internal server error",
}
var apiBadRequest = &Response{
Status: http.StatusBadRequest,
Message: "bad request",
}
var apiNotFound = &Response{
Status: http.StatusNotFound,
Message: "not found",
}
var apiForbidden = &Response{
Status: http.StatusForbidden,
Message: "forbidden",
}
// ServeHTTP servers an api call
func (handler *Handler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// check that the api is actually enabled!
if !handler.Config.HTTP.API.Value {
apiNotEnabled.ServeHTTP(w, r)
return
}
// get the permitted methods
methods := handler.methods.Get(func() map[string]struct{} {
m := make(map[string]struct{}, len(handler.Methods)+1)
for _, method := range handler.Methods {
m[method] = struct{}{}
}
m["OPTIONS"] = struct{}{}
return m
})
// check that the method is permitted
if _, ok := methods[r.Method]; !ok {
apiMethodNotAllowed.ServeHTTP(w, r)
return
}
// we now delegate to user-level code;
// so we now need to make sure that panic()s are caught.
var stage string
defer func() {
// recover any error
rec := recover()
if rec == nil {
return
}
// log the error, and serve the default internal server error
zerolog.Ctx(r.Context()).Error().Str("panic", fmt.Sprint(rec)).Str("stage", stage).Str("route", r.URL.RequestURI()).Msg("api handler caused panic()")
apiInternalServerError.ServeHTTP(w, r)
}()
// read the parameter
stage = "param"
var param string
if handler.ScopeParam != nil {
param = handler.ScopeParam(r)
}
// check that the scope is correct
stage = "scope"
if err := handler.Auth.CheckScope(param, handler.Scope, r); err != nil {
(&Response{
Status: http.StatusForbidden,
Message: err.Error(),
}).ServeHTTP(w, r)
return
}
stage = "handler"
result, err := handler.Handler(param, r)
switch true {
case err == nil: /* keep going */
// handle common httpx errors
case errors.Is(err, httpx.ErrInternalServerError):
apiInternalServerError.ServeHTTP(w, r)
return
case errors.Is(err, httpx.ErrBadRequest):
apiBadRequest.ServeHTTP(w, r)
return
case errors.Is(err, httpx.ErrNotFound):
apiNotFound.ServeHTTP(w, r)
return
case errors.Is(err, httpx.ErrForbidden):
apiForbidden.ServeHTTP(w, r)
return
case errors.Is(err, httpx.ErrMethodNotAllowed):
apiMethodNotAllowed.ServeHTTP(w, r)
return
// generic error
default:
(&Response{
Status: http.StatusInternalServerError,
Message: err.Error(),
}).ServeHTTP(w, r)
return
}
stage = "marshal"
// encode the result into json and send it as the response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
}
// Response represents a generic response to any request.
// Response objects cache response serialization
type Response struct {
Status int `json:"status"`
Message string `json:"message"`
res lazy.Lazy[httpx.Response]
}
func (g *Response) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.res.Get(func() httpx.Response {
bytes, _ := json.Marshal(g)
return httpx.Response{
ContentType: "application/json",
Body: bytes,
StatusCode: g.Status,
}
}).ServeHTTP(w, r)
}

View file

@ -8,6 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/users"
@ -30,7 +31,7 @@ var (
func (next *Next) Routes() component.Routes {
return component.Routes{
Prefix: "/next/",
Decorator: next.Dependencies.Auth.Require(component.ScopeUserLoggedIn, nil),
Decorator: next.Dependencies.Auth.Require(scopes.ScopeUserLoggedIn, nil),
}
}

View file

@ -8,6 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2"
@ -39,7 +40,7 @@ func (panel *UserPanel) Routes() component.Routes {
return component.Routes{
Prefix: "/user/",
CSRF: true,
Decorator: panel.Dependencies.Auth.Require(component.ScopeUserLoggedIn, nil),
Decorator: panel.Dependencies.Auth.Require(scopes.ScopeUserLoggedIn, nil),
}
}
@ -115,7 +116,7 @@ func (panel *UserPanel) HandleRoute(ctx context.Context, route string) (http.Han
}
// ensure that the user is logged in!
return panel.Dependencies.Auth.Protect(router, component.ScopeUserLoggedIn, nil), nil
return panel.Dependencies.Auth.Protect(router, scopes.ScopeUserLoggedIn, nil), nil
}
type userFormContext struct {

View file

@ -18,9 +18,13 @@ var (
_ component.ScopeProvider = (*UserLoggedIn)(nil)
)
const (
ScopeAdminLoggedIn Scope = "login.admin"
)
func (*AdminLoggedIn) Scope() component.ScopeInfo {
return component.ScopeInfo{
Scope: component.ScopeAdminLoggedIn,
Scope: ScopeAdminLoggedIn,
Description: "session has a signed in admin",
DeniedMessage: "user must be signed into an admin account with TOTP enabled",
TakesParam: false,

View file

@ -0,0 +1,38 @@
package scopes
import (
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
)
type ListInstancesScope struct {
component.Base
Dependencies struct {
Auth *auth.Auth
}
}
var (
_ component.ScopeProvider = (*ListInstancesScope)(nil)
)
const (
ScopeInstanceDirectory Scope = "instances.directory"
)
func (*ListInstancesScope) Scope() component.ScopeInfo {
return component.ScopeInfo{
Scope: ScopeInstanceDirectory,
Description: "get a public directory of instances",
DeniedMessage: "",
TakesParam: false,
}
}
func (lis *ListInstancesScope) HasScope(param string, r *http.Request) (bool, error) {
// TODO: at the moment everyone has this permission
// this should change in the future!
return true, nil
}

View file

@ -0,0 +1,37 @@
package scopes
import (
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
)
type ListNewsScope struct {
component.Base
Dependencies struct {
Auth *auth.Auth
}
}
var (
_ component.ScopeProvider = (*ListNewsScope)(nil)
)
const (
ScopeListNews Scope = "news.list"
)
func (*ListNewsScope) Scope() component.ScopeInfo {
return component.ScopeInfo{
Scope: ScopeListNews,
Description: "list news items",
DeniedMessage: "",
TakesParam: false,
}
}
func (lns *ListNewsScope) HasScope(param string, r *http.Request) (bool, error) {
// TODO: at the moment everyone has this permission
return true, nil
}

View file

@ -0,0 +1,6 @@
// Package scopes implements and provides scopes used by the API
package scopes
import "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
type Scope = component.Scope

View file

@ -18,9 +18,13 @@ var (
_ component.ScopeProvider = (*UserLoggedIn)(nil)
)
const (
ScopeUserLoggedIn Scope = "login.user"
)
func (*UserLoggedIn) Scope() component.ScopeInfo {
return component.ScopeInfo{
Scope: component.ScopeUserLoggedIn,
Scope: ScopeUserLoggedIn,
Description: "session has an associated user",
TakesParam: false,
}

View file

@ -11,6 +11,7 @@ import (
"github.com/FAU-CDI/wdresolve/resolvers"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
@ -102,7 +103,7 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
IndexContext: context,
}
if resolver.Dependencies.Auth.CheckScope("", component.ScopeUserLoggedIn, r) != nil {
if resolver.Dependencies.Auth.CheckScope("", scopes.ScopeUserLoggedIn, r) != nil {
ctx.IndexContext.Prefixes = nil
}
httpx.WriteHTML(tpl.Context(r, ctx), nil, t, "", w, r)

View file

@ -51,11 +51,6 @@ func (scope ScopeInfo) CheckError(err error) error {
return CheckError{Scope: scope.Scope, Err: err}
}
const (
ScopeUserLoggedIn Scope = "login.user"
ScopeAdminLoggedIn Scope = "login.admin"
)
// ScopeProvider is a component that can check a specific scope
type ScopeProvider interface {
Component

View file

@ -7,6 +7,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin/socket"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/julienschmidt/httprouter"
@ -46,12 +47,12 @@ func (admin *Admin) Routes() component.Routes {
return component.Routes{
Prefix: "/admin/",
CSRF: true,
Decorator: admin.Dependencies.Auth.Require(component.ScopeAdminLoggedIn, nil),
Decorator: admin.Dependencies.Auth.Require(scopes.ScopeAdminLoggedIn, nil),
}
}
func (admin *Admin) Menu(r *http.Request) []component.MenuItem {
if admin.Dependencies.Auth.CheckScope("", component.ScopeAdminLoggedIn, r) != nil {
if admin.Dependencies.Auth.CheckScope("", scopes.ScopeAdminLoggedIn, r) != nil {
return nil
}
return []component.MenuItem{

View file

@ -1,64 +0,0 @@
package home
import (
"context"
"net/http"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/tkw1536/pkglib/httpx"
)
type API struct {
component.Base
Dependencies struct {
Home *Home
}
}
var (
_ component.Routeable = (*API)(nil)
)
func (api *API) Routes() component.Routes {
return component.Routes{
Prefix: "/api/v1/systems",
Exact: true,
Decorator: api.Config.HTTP.APIDecorator("GET"),
}
}
type APISystemInfo struct {
Slug string
URL string
Tagline string
EntityCount int
BundleCount int
LastEdit time.Time
}
func (api *API) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
return httpx.JSON(func(r *http.Request) ([]APISystemInfo, error) {
var statuses []status.WissKI
if api.Dependencies.Home.ShouldShowList(r) {
statuses = api.Dependencies.Home.homeInstances.Get(nil)
}
if len(statuses) == 0 {
return []APISystemInfo{}, nil
}
infos := make([]APISystemInfo, len(statuses))
for i, status := range statuses {
infos[i].Slug = status.Slug
infos[i].URL = status.URL
infos[i].EntityCount = status.Statistics.Bundles.TotalCount()
infos[i].BundleCount = status.Statistics.Bundles.TotalBundles
infos[i].LastEdit = status.Statistics.Bundles.LastEdit().Time
}
return infos, nil
}), nil
}

View file

@ -6,23 +6,16 @@ import (
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/list"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/tkw1536/pkglib/lazy"
)
type Home struct {
component.Base
Dependencies struct {
Templating *templating.Templating
Instances *instances.Instances
Auth *auth.Auth
ListInstances *list.ListInstances
Templating *templating.Templating
}
instanceNames lazy.Lazy[map[string]struct{}] // instance names
homeInstances lazy.Lazy[[]status.WissKI] // list of home instances (updated via cron)
}
var (
@ -61,21 +54,8 @@ func (home *Home) HandleRoute(ctx context.Context, route string) (http.Handler,
}), nil
}
func (home *Home) instanceMap(ctx context.Context) (map[string]struct{}, error) {
wissKIs, err := home.Dependencies.Instances.All(ctx)
if err != nil {
return nil, err
}
names := make(map[string]struct{}, len(wissKIs))
for _, w := range wissKIs {
names[w.Slug] = struct{}{}
}
return names, nil
}
func (home *Home) serveWissKI(w http.ResponseWriter, slug string, r *http.Request) {
if _, ok := home.instanceNames.Get(nil)[slug]; !ok {
if _, ok := home.Dependencies.ListInstances.Names()[slug]; !ok {
// Get(nil) guaranteed to work by precondition
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "WissKI %q not found\n", slug)

View file

@ -1,86 +0,0 @@
package home
import (
"context"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"golang.org/x/sync/errgroup"
)
// loadInstances loads all the instances into the home route
func (home *Home) loadInstances(ctx context.Context) ([]status.WissKI, error) {
// find all the WissKIs
wissKIs, err := home.Dependencies.Instances.All(ctx)
if err != nil {
return nil, err
}
instances := make([]status.WissKI, len(wissKIs))
// determine their infos
var eg errgroup.Group
for i, instance := range wissKIs {
i := i
wissKI := instance
eg.Go(func() (err error) {
instances[i], err = wissKI.Info().Information(ctx, false)
return
})
}
eg.Wait()
// and return the new instances
return instances, nil
}
// UpdateInstanceList updates the instances list of the home struct
type UpdateInstanceList struct {
component.Base
Dependencies struct {
Home *Home
}
}
var (
_ component.Cronable = (*UpdateInstanceList)(nil)
)
func (*UpdateInstanceList) TaskName() string {
return "instance list"
}
func (ul *UpdateInstanceList) Cron(ctx context.Context) error {
names, err := ul.Dependencies.Home.instanceMap(ctx)
if err != nil {
return err
}
ul.Dependencies.Home.instanceNames.Set(names)
return nil
}
type UpdateHome struct {
component.Base
Dependencies struct {
Home *Home
}
}
var (
_ component.Cronable = (*UpdateHome)(nil)
)
func (ur *UpdateHome) TaskName() string {
return "home instances fetch"
}
func (ur *UpdateHome) Cron(ctx context.Context) error {
instances, err := ur.Dependencies.Home.loadInstances(ctx)
if err != nil {
return err
}
ur.Dependencies.Home.homeInstances.Set(instances)
return nil
}

View file

@ -47,23 +47,6 @@ type publicContext struct {
const logoHTML = template.HTML(`<img src="/logo.svg" alt="WissKI Distillery Logo" class="biglogo">`)
// ShouldShowList determines if the given request should show a WissKI list
func (home *Home) ShouldShowList(r *http.Request) bool {
allowPrivate := home.Config.Home.List.Private.Value
allowPublic := home.Config.Home.List.Public.Value
if allowPrivate == allowPublic {
return allowPrivate
}
user, _ := home.Dependencies.Auth.UserOf(r)
if user == nil {
return allowPublic
} else {
return allowPrivate
}
}
func (home *Home) publicHandler(ctx context.Context) http.Handler {
title := home.Config.Home.Title
@ -89,7 +72,7 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
// prepare about
pc.aboutContext.Logo = logoHTML
pc.aboutContext.Instances = home.homeInstances.Get(nil)
pc.aboutContext.Instances = home.Dependencies.ListInstances.Infos()
pc.aboutContext.SelfRedirect = home.Config.Home.SelfRedirect.String()
// render the about template
@ -102,7 +85,7 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
pc.About = template.HTML(builder.String())
// check if we should show the list of WissKIs
pc.ListEnabled = home.ShouldShowList(r)
pc.ListEnabled = home.Dependencies.ListInstances.ShouldShowList(r)
// title of the list
pc.ListTitle = home.Config.Home.List.Title

View file

@ -6,22 +6,20 @@
</div>
{{range .Instances}}
{{ if and .Running (not .NoPrefixes) }}
<div class="pure-u-1 pure-u-md-1-3">
<h3>{{.Slug}}</h3>
<p>
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer" class="wisskilink">{{.URL}}</a><br>
<small>
{{ .Statistics.Bundles.Summary }}
<div class="pure-u-1 pure-u-md-1-3">
<h3>{{.Slug}}</h3>
<p>
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer" class="wisskilink">{{.URL}}</a><br>
<small>
{{ .Statistics.Bundles.Summary }}
{{ $edit := .Statistics.Bundles.LastEdit }}
{{ if $edit.Valid }}
<br />
last edited {{ $edit.Time.Format "2006-01-02T15:04:05Z07:00" }}
{{ end }}
</small>
</p>
</div>
{{ end }}
{{ $edit := .Statistics.Bundles.LastEdit }}
{{ if $edit.Valid }}
<br />
last edited {{ $edit.Time.Format "2006-01-02T15:04:05Z07:00" }}
{{ end }}
</small>
</p>
</div>
{{ end }}
{{ end }}

View file

@ -0,0 +1,71 @@
package list
import (
"context"
"net/http"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/api"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/status"
)
// API implements an API to list all instances
type API struct {
component.Base
Dependencies struct {
ListInstances *ListInstances
Auth *auth.Auth
}
}
func (lia *API) Routes() component.Routes {
return component.Routes{
Prefix: "/api/v1/instances/directory",
Exact: true,
}
}
// APISystem represents a system returned by the api
type APISystem struct {
Slug string
URL string
Tagline string
EntityCount int
BundleCount int
LastEdit time.Time
}
func (a *API) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
return &api.Handler[[]APISystem]{
Config: a.Config,
Auth: a.Dependencies.Auth,
Methods: []string{"GET"},
Scope: scopes.ScopeInstanceDirectory,
Handler: func(s string, r *http.Request) ([]APISystem, error) {
var statuses []status.WissKI
if a.Dependencies.ListInstances.ShouldShowList(r) {
statuses = a.Dependencies.ListInstances.infos.Get(nil)
}
if len(statuses) == 0 {
return []APISystem{}, nil
}
infos := make([]APISystem, len(statuses))
for i, status := range statuses {
infos[i].Slug = status.Slug
infos[i].URL = status.URL
infos[i].EntityCount = status.Statistics.Bundles.TotalCount()
infos[i].BundleCount = status.Statistics.Bundles.TotalBundles
infos[i].LastEdit = status.Statistics.Bundles.LastEdit().Time
}
return infos, nil
},
}, nil
}

View file

@ -0,0 +1,130 @@
package list
import (
"context"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/tkw1536/pkglib/lazy"
"golang.org/x/sync/errgroup"
)
// ListInstances holds information about all instances
type ListInstances struct {
component.Base
names lazy.Lazy[map[string]struct{}] // instance names
infos lazy.Lazy[[]status.WissKI] // list of home instances (updated via cron)
Dependencies struct {
Auth *auth.Auth
Instances *instances.Instances
}
}
func (li *ListInstances) Names() map[string]struct{} {
return li.names.Get(nil)
}
func (li *ListInstances) Infos() []status.WissKI {
return li.infos.Get(nil)
}
// ShouldShowList determines if a list should be shown for the given request
func (li *ListInstances) ShouldShowList(r *http.Request) bool {
allowPrivate := li.Config.Home.List.Private.Value
allowPublic := li.Config.Home.List.Public.Value
if allowPrivate == allowPublic {
return allowPrivate
}
user, _ := li.Dependencies.Auth.UserOf(r)
if user == nil {
return allowPublic
} else {
return allowPrivate
}
}
var (
_ component.Cronable = (*ListInstances)(nil)
)
func (li *ListInstances) TaskName() string {
return "instance list and status"
}
func (li *ListInstances) Cron(ctx context.Context) (err error) {
{
names, e := li.getNames(ctx)
if err == nil {
li.names.Set(names)
} else {
err = e
}
}
{
infos, e := li.getInfos(ctx)
if err == nil {
li.infos.Set(infos)
} else {
err = e
}
}
return
}
// getNames returns the names of the given instances
func (li *ListInstances) getNames(ctx context.Context) (map[string]struct{}, error) {
wissKIs, err := li.Dependencies.Instances.All(ctx)
if err != nil {
return nil, err
}
names := make(map[string]struct{}, len(wissKIs))
for _, w := range wissKIs {
names[w.Slug] = struct{}{}
}
return names, nil
}
// getInfos returns the names of the given instances
func (li *ListInstances) getInfos(ctx context.Context) ([]status.WissKI, error) {
// find all the WissKIs
wissKIs, err := li.Dependencies.Instances.All(ctx)
if err != nil {
return nil, err
}
infos := make([]status.WissKI, len(wissKIs))
// determine their infos
var eg errgroup.Group
for i, instance := range wissKIs {
i := i
wissKI := instance
eg.Go(func() (err error) {
infos[i], err = wissKI.Info().Information(ctx, false)
return
})
}
eg.Wait()
// filter them by those that are running and do not have prefixes excluded
infosF := infos[:0]
for _, info := range infos {
if info.NoPrefixes || !info.Running {
continue
}
infosF = append(infosF, info)
}
// and return them
return infos[:len(infosF):len(infosF)], nil
}

View file

@ -2,15 +2,19 @@ package news
import (
"context"
"encoding/json"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/tkw1536/pkglib/httpx"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/api"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
)
type API struct {
component.Base
Dependencies struct {
Auth *auth.Auth
}
}
var (
@ -19,25 +23,21 @@ var (
func (api *API) Routes() component.Routes {
return component.Routes{
Prefix: "/api/v1/news/",
Exact: true,
Decorator: api.Config.HTTP.APIDecorator("GET"),
Prefix: "/api/v1/news/",
Exact: true,
}
}
func (api *API) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
items, err := Items()
if err != nil {
return nil, err
}
func (a *API) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
return &api.Handler[[]Item]{
Config: a.Config,
Auth: a.Dependencies.Auth,
data, err := json.Marshal(items)
if err != nil {
return nil, err
}
Methods: []string{"GET"},
return httpx.Response{
ContentType: "application/json",
Body: data,
Scope: scopes.ScopeListNews,
Handler: func(s string, r *http.Request) ([]Item, error) {
return Items()
},
}, nil
}