api: Begin implementing an API

This commit is contained in:
Tom 2023-04-28 10:25:36 +02:00
parent 1a5e83be10
commit 2fac0390b1
11 changed files with 186 additions and 23 deletions

View file

@ -39,6 +39,10 @@ http:
# This email address can be configured here.
certbot_email: null
# Enable or Disable the HTTP API.
# In the future, it will be enabled by default, but at this point it is not.
api: null
# Configuration for the (public) homepage of the distillery.
home:
# the title of the distillery to be set

View file

@ -2,9 +2,12 @@ package config
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/FAU-CDI/wisski-distillery/internal/config/validators"
"github.com/tkw1536/pkglib/httpx"
"golang.org/x/net/idna"
)
@ -22,6 +25,30 @@ type HTTPConfig struct {
// It can be enabled by setting an email for certbot certificates.
// This email address can be configured here.
CertbotEmail string `yaml:"certbot_email" validate:"email"`
// API determines if the API is enabled.
// In a future version of the distillery, it will be enabled by default.
API validators.NullableBool `yaml:"api" validate:"bool" default:"false"`
}
var apiNotEnabled = httpx.Response{
StatusCode: http.StatusForbidden,
Body: []byte(`{"message":"API is not enabled"}`),
}
func (hcfg HTTPConfig) APIDecorator(methods ...string) func(http.Handler) http.Handler {
methods = append(methods, "OPTIONS") // always permit the options method!
if !hcfg.API.Value {
return func(http.Handler) http.Handler {
return httpx.PermitMethods(apiNotEnabled, methods...)
}
}
// permit only the specified methods
return func(h http.Handler) http.Handler {
return httpx.PermitMethods(h, methods...)
}
}
// JoinPath returns the root public url joined with the provided parts.

View file

@ -8,13 +8,13 @@ import (
// NullableBool represents a bool that can be null
type NullableBool struct {
Null, Value bool
Set, Value bool
}
func (nb *NullableBool) UnmarshalYAML(value *yaml.Node) error {
nb.Null = false
nb.Set = true
if err := value.Decode(&nb.Value); err != nil {
nb.Null = true
nb.Set = false
nb.Value = false
}
@ -22,19 +22,19 @@ func (nb *NullableBool) UnmarshalYAML(value *yaml.Node) error {
}
func (nb NullableBool) MarshalYAML() (interface{}, error) {
if nb.Null {
if !nb.Set {
return nil, nil
}
return nb.Value, nil
}
func ValidateBool(value *NullableBool, dflt string) (err error) {
if value.Null {
if !value.Set {
res, err := strconv.ParseBool(dflt)
if err != nil {
return err
}
value.Null = false
value.Set = true
value.Value = res
}
return err

View file

@ -0,0 +1,64 @@
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

@ -29,7 +29,6 @@ var aboutTemplate = template.Must(template.New("about.html").Parse(aboutHTML))
// aboutContext is passed to about.html
type aboutContext struct {
Instances []status.WissKI // list of WissKI Instancaes
SignedIn bool // is there a signed in user?
Logo template.HTML
SelfRedirect string
}
@ -48,6 +47,23 @@ 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
@ -75,10 +91,6 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
pc.aboutContext.Logo = logoHTML
pc.aboutContext.Instances = home.homeInstances.Get(nil)
pc.aboutContext.SelfRedirect = home.Config.Home.SelfRedirect.String()
{
user, _ := home.Dependencies.Auth.UserOf(r)
pc.aboutContext.SignedIn = user != nil
}
// render the about template
@ -89,13 +101,8 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
// and return about!
pc.About = template.HTML(builder.String())
// user is not signed in!
if pc.aboutContext.SignedIn {
pc.ListEnabled = home.Config.Home.List.Private.Value
} else {
pc.ListEnabled = home.Config.Home.List.Public.Value
}
// check if we should show the list of WissKIs
pc.ListEnabled = home.ShouldShowList(r)
// title of the list
pc.ListTitle = home.Config.Home.List.Title

View file

@ -0,0 +1,43 @@
package news
import (
"context"
"encoding/json"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/tkw1536/pkglib/httpx"
)
type API struct {
component.Base
}
var (
_ component.Routeable = (*API)(nil)
)
func (api *API) Routes() component.Routes {
return component.Routes{
Prefix: "/api/v1/news/",
Exact: true,
Decorator: api.Config.HTTP.APIDecorator("GET"),
}
}
func (api *API) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
items, err := Items()
if err != nil {
return nil, err
}
data, err := json.Marshal(items)
if err != nil {
return nil, err
}
return httpx.Response{
ContentType: "application/json",
Body: data,
}, nil
}

View file

@ -187,5 +187,9 @@ func (dis *Distillery) allComponents() []initFunc {
auto[*cron.Cron],
auto[*home.UpdateHome],
auto[*home.UpdateInstanceList],
// API
auto[*home.API],
auto[*news.API],
}
}

View file

@ -79,6 +79,13 @@ type BundleStatistics struct {
TotalMainBundles int `json:"totalMainBundles"`
}
func (bs BundleStatistics) TotalCount() (total int) {
for _, bundle := range bs.Bundles {
total += bundle.Count
}
return
}
type LastEdit struct {
Time time.Time
Valid bool
@ -101,10 +108,7 @@ func (bs BundleStatistics) LastEdit() (le LastEdit) {
}
func (bs BundleStatistics) Summary() string {
var totalCount int
for _, bundle := range bs.Bundles {
totalCount += bundle.Count
}
totalCount := bs.TotalCount()
if totalCount == 0 {
return ""
}