An API for Guichet #23

Merged
quentin merged 14 commits from api into main 2023-09-26 06:44:36 +00:00
8 changed files with 333 additions and 155 deletions
Showing only changes of commit bc368943a4 - Show all commits

2
api.go
View file

@ -29,7 +29,7 @@ type BucketRequest struct {
http *http.Request http *http.Request
} }
func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { func handleAPIWebsite(w http.ResponseWriter, r *http.Request) {
br, err := buildBucketRequest(w, r) br, err := buildBucketRequest(w, r)
if err != nil { if err != nil {

124
garage.go
View file

@ -49,7 +49,7 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
func grgCreateWebsite(gkey, bucket string, quotas *UserQuota) (*garage.BucketInfo, error) { func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
client, ctx := gadmin() client, ctx := gadmin()
br := garage.NewCreateBucketRequest() br := garage.NewCreateBucketRequest()
@ -61,32 +61,40 @@ func grgCreateWebsite(gkey, bucket string, quotas *UserQuota) (*garage.BucketInf
fmt.Printf("%+v\n", err) fmt.Printf("%+v\n", err)
return nil, err return nil, err
} }
return binfo, nil
}
func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) {
client, ctx := gadmin()
// Allow user's key // Allow user's key
ar := garage.AllowBucketKeyRequest{ ar := garage.AllowBucketKeyRequest{
BucketId: *binfo.Id, BucketId: bid,
AccessKeyId: gkey, AccessKeyId: gkey,
Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true), Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true),
} }
binfo, _, err = client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute() binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute()
if err != nil { if err != nil {
fmt.Printf("%+v\n", err) fmt.Printf("%+v\n", err)
return nil, err return nil, err
} }
// Expose website and set quota return binfo, nil
}
func allowWebsiteDefault() *garage.UpdateBucketRequestWebsiteAccess {
wr := garage.NewUpdateBucketRequestWebsiteAccess() wr := garage.NewUpdateBucketRequestWebsiteAccess()
wr.SetEnabled(true) wr.SetEnabled(true)
wr.SetIndexDocument("index.html") wr.SetIndexDocument("index.html")
wr.SetErrorDocument("error.html") wr.SetErrorDocument("error.html")
qr := quotas.DefaultWebsiteQuota() return wr
}
ur := garage.NewUpdateBucketRequest() func grgUpdateBucket(bid string, ur *garage.UpdateBucketRequest) (*garage.BucketInfo, error) {
ur.SetWebsiteAccess(*wr) client, ctx := gadmin()
ur.SetQuotas(*qr)
binfo, _, err = client.BucketApi.UpdateBucket(ctx, *binfo.Id).UpdateBucketRequest(*ur).Execute() binfo, _, err := client.BucketApi.UpdateBucket(ctx, bid).UpdateBucketRequest(*ur).Execute()
if err != nil { if err != nil {
fmt.Printf("%+v\n", err) fmt.Printf("%+v\n", err)
return nil, err return nil, err
@ -154,7 +162,7 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) {
// --- Start page rendering functions // --- Start page rendering functions
func handleGarageKey(w http.ResponseWriter, r *http.Request) { func handleWebsiteConfigure(w http.ResponseWriter, r *http.Request) {
user := RequireUserHtml(w, r) user := RequireUserHtml(w, r)
if user == nil { if user == nil {
return return
@ -164,22 +172,48 @@ func handleGarageKey(w http.ResponseWriter, r *http.Request) {
tKey.Execute(w, user) tKey.Execute(w, user)
} }
func handleGarageWebsiteList(w http.ResponseWriter, r *http.Request) { func handleWebsiteList(w http.ResponseWriter, r *http.Request) {
user := RequireUserHtml(w, r) user := RequireUserHtml(w, r)
if user == nil { if user == nil {
return return
} }
tWebsiteList := getTemplate("garage_website_list.html") ctrl, err := NewWebsiteController(user)
tWebsiteList.Execute(w, user) if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { list := ctrl.List()
if len(list) > 0 {
http.Redirect(w, r, "/website/inspect/"+list[0].Pretty, http.StatusFound)
} else {
http.Redirect(w, r, "/website/new", http.StatusFound)
}
}
type WebsiteNewTpl struct {
ctrl *WebsiteController
err error
}
func handleWebsiteNew(w http.ResponseWriter, r *http.Request) {
user := RequireUserHtml(w, r) user := RequireUserHtml(w, r)
if user == nil { if user == nil {
return return
} }
ctrl, err := NewWebsiteController(user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tpl := &WebsiteNewTpl{
ctrl: ctrl,
err: nil,
}
tWebsiteNew := getTemplate("garage_website_new.html") tWebsiteNew := getTemplate("garage_website_new.html")
if r.Method == "POST" { if r.Method == "POST" {
r.ParseForm() r.ParseForm()
@ -188,73 +222,47 @@ func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) {
if bucket == "" { if bucket == "" {
bucket = strings.Join(r.Form["bucket2"], "") bucket = strings.Join(r.Form["bucket2"], "")
} }
if bucket == "" {
log.Println("Form empty")
// @FIXME we need to return the error to the user
tWebsiteNew.Execute(w, nil)
return
}
keyInfo, err := user.S3KeyInfo() view, err := ctrl.Create(bucket)
if err != nil { if err != nil {
log.Println(err) tpl.err = err
// @FIXME we need to return the error to the user tWebsiteNew.Execute(w, tpl)
tWebsiteNew.Execute(w, nil)
return
} }
binfo, err := grgCreateWebsite(*keyInfo.AccessKeyId, bucket, user.Quota) http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound)
if err != nil {
log.Println(err)
// @FIXME we need to return the error to the user
tWebsiteNew.Execute(w, nil)
return
}
http.Redirect(w, r, "/garage/website/b/"+*binfo.Id, http.StatusFound)
return return
} }
tWebsiteNew.Execute(w, nil) tWebsiteNew.Execute(w, nil)
} }
type webInspectView struct { type WebsiteInspectTpl struct {
User *LoggedUser Ctrl *WebsiteController
Bucket *garage.BucketInfo View *WebsiteView
IndexDoc string
ErrorDoc string
MaxObjects int64
MaxSize int64
UsedSizePct float64
} }
func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) { func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) {
user := RequireUserHtml(w, r) user := RequireUserHtml(w, r)
if user == nil { if user == nil {
return return
} }
bucketId := mux.Vars(r)["bucket"] ctrl, err := NewWebsiteController(user)
// @FIXME check that user owns the bucket....
binfo, err := grgGetBucket(bucketId)
if err != nil { if err != nil {
log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
wc := binfo.GetWebsiteConfig() bucketName := mux.Vars(r)["bucket"]
q := binfo.GetQuotas()
view := webInspectView{ view, err := ctrl.Inspect(bucketName)
User: user, if err != nil {
Bucket: binfo, http.Error(w, err.Error(), http.StatusInternalServerError)
IndexDoc: (&wc).GetIndexDocument(), return
ErrorDoc: (&wc).GetErrorDocument(),
MaxObjects: (&q).GetMaxObjects(),
MaxSize: (&q).GetMaxSize(),
} }
tpl := &WebsiteInspectTpl{ ctrl, view }
tWebsiteInspect := getTemplate("garage_website_inspect.html") tWebsiteInspect := getTemplate("garage_website_inspect.html")
tWebsiteInspect.Execute(w, &view) tWebsiteInspect.Execute(w, &tpl)
} }

10
main.go
View file

@ -147,7 +147,7 @@ func server(args []string) {
r.HandleFunc("/login", handleLogin) r.HandleFunc("/login", handleLogin)
r.HandleFunc("/logout", handleLogout) r.HandleFunc("/logout", handleLogout)
r.HandleFunc("/api/unstable/garage/bucket/{bucket}", handleAPIGarageBucket) r.HandleFunc("/api/unstable/website/{bucket}", handleAPIWebsite)
r.HandleFunc("/profile", handleProfile) r.HandleFunc("/profile", handleProfile)
r.HandleFunc("/passwd", handlePasswd) r.HandleFunc("/passwd", handlePasswd)
@ -156,10 +156,10 @@ func server(args []string) {
r.HandleFunc("/directory/search", handleDirectorySearch) r.HandleFunc("/directory/search", handleDirectorySearch)
r.HandleFunc("/directory", handleDirectory) r.HandleFunc("/directory", handleDirectory)
r.HandleFunc("/garage/key", handleGarageKey) r.HandleFunc("/website", handleWebsiteList)
r.HandleFunc("/garage/website", handleGarageWebsiteList) r.HandleFunc("/website/new", handleWebsiteNew)
r.HandleFunc("/garage/website/new", handleGarageWebsiteNew) r.HandleFunc("/website/configure", handleWebsiteConfigure)
r.HandleFunc("/garage/website/b/{bucket}", handleGarageWebsiteInspect) r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect)
r.HandleFunc("/invite/new_account", handleInviteNewAccount) r.HandleFunc("/invite/new_account", handleInviteNewAccount)
r.HandleFunc("/invite/send_code", handleInviteSendCode) r.HandleFunc("/invite/send_code", handleInviteSendCode)

View file

@ -3,7 +3,7 @@
{{define "body"}} {{define "body"}}
<div class="d-flex"> <div class="d-flex">
<h4>Mes identifiants</h4> <h4>Mes identifiants</h4>
<a class="ml-auto btn btn-link" href="/garage/website">Mes sites webs</a> <a class="ml-auto btn btn-link" href="/website">Mes sites webs</a>
<a class="ml-4 btn btn-info" href="/">Menu principal</a> <a class="ml-4 btn btn-info" href="/">Menu principal</a>
</div> </div>

View file

@ -2,57 +2,54 @@
{{define "body"}} {{define "body"}}
<div class="d-flex"> <div class="d-flex">
<h4>Inspecter le site web</h4> <a class="ml-4 btn btn-primary" href="/website/new">Nouveau site web</a>
<a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a> <!--<h4>Inspecter les sites webs</h4>-->
<a class="ml-4 btn btn-success" href="/garage/website/new">Nouveau site web</a> <a class="ml-auto btn btn-link" href="/website/configure">Mes identifiants</a>
<a class="ml-4 btn btn-info" href="/garage/website">Mes sites webs</a> <a class="ml-4 btn btn-info" href="/">Menu principal</a>
</div> </div>
<table class="table mt-4"> <div class="row mt-3" >
<tbody> <div class="col-md-3">
<tr> <div class="list-group">
<th scope="row">ID</th> {{ $view := .View }}
<td>{{ .Bucket.Id }}</td> {{ range $wid := .Ctrl.List }}
</tr> {{ if eq $wid.Internal $view.Name.Internal }}
<tr> <a href="/website/inspect/{{ $wid.Pretty }}" class="list-group-item list-group-item-action active">
<th scope="row">URLs</th> {{ $wid.Url }}
<td> </a>
{{ range $alias := .Bucket.GlobalAliases }}
{{ if contains $alias "." }}
https://{{ $alias }}
{{ else }} {{ else }}
https://{{ $alias }}.web.deuxfleurs.fr <a href="/website/inspect/{{ $wid.Pretty }}" class="list-group-item list-group-item-action">
{{ $wid.Url }}
</a>
{{ end }} {{ end }}
{{ end }} {{ end }}
</td> </div>
</tr> </div>
<tr> <div class="col-md-9">
<th scope="row">Document d'index</th> <h2>{{ .View.Name.Url }}</h2>
<td> {{ .IndexDoc }}</td>
</tr>
<tr>
<th scope="row">Document d'erreur</th>
<td>{{ .ErrorDoc }}</td>
</tr>
<tr>
<th scope="row">Nombre de fichiers</th>
<td>{{ .Bucket.Objects }} / {{ .MaxObjects }}</td>
</tr>
<tr>
<th scope="row">Espace utilisé</th>
<td>{{ .Bucket.Bytes }} / {{ .MaxSize }} octets</td>
</tr>
</tbody>
</table>
<h4>Configurer le nom de domaine</h4> <h5 class="mt-3">Quotas</h5>
<div class="progress mt-3">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ .View.Size.Current }}" aria-valuemin="0" aria-valuemax="{{ .View.Size.Max }}" style="width: {{ .View.Size.Percent }}%; min-width: 2em;">
{{ .View.Size.Ratio }}%
</div>
</div>
{{ range $alias := .Bucket.GlobalAliases }} <p class="text-center">
{{ if contains $alias "." }} {{ .View.Size.PrettyCurrent }} utilisé sur un maximum de {{ .View.Size.PrettyMax }}
<p> Le nom de domaine {{ $alias }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée <code>CNAME garage.deuxfleurs.fr</code> ou <code>ALIAS garage.deuxfleurs.fr</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p> {{ if gt .View.Files.Ratio 0.5 }}
{{ else }} <br>{{ .View.Files.Current }} fichiers sur un maximum de {{ .View.Files.Max }}
<p> Le nom de domaine https://{{ $alias }}.web.deuxfleurs.fr est fourni par Deuxfleurs, il n'y a pas de configuration à faire.</p>
{{ end }} {{ end }}
</p>
{{ if .View.Name.Expanded }}
<h5 class="mt-5">Vous ne savez pas comment configurer votre nom de domaine ?</h5>
<p> Le nom de domaine {{ .View.Name.Url }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée <code>CNAME garage.deuxfleurs.fr</code> ou <code>ALIAS garage.deuxfleurs.fr</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p>
{{ end }} {{ end }}
</div>
</div>
{{ end }} {{ end }}

View file

@ -1,38 +0,0 @@
{{define "title"}}Sites webs |{{end}}
{{define "body"}}
<div class="d-flex">
<h4>Sites webs</h4>
<a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a>
<a class="ml-4 btn btn-success" href="/garage/website/new">Nouveau site web</a>
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
</div>
<table class="table mt-4">
<thead>
<th scope="col">ID</th>
<th scope="col">URLs</th>
</thead>
<tbody>
{{ range $buck := .S3KeyInfo.Buckets }}
{{ if $buck.GlobalAliases }}
<tr>
<td>
<a href="/garage/website/b/{{$buck.Id}}">{{$buck.Id}}</a>
</td>
<td>
{{ range $alias := $buck.GlobalAliases }}
{{ if contains $alias "." }}
https://{{ $alias }}
{{ else }}
https://{{ $alias }}.web.deuxfleurs.fr
{{ end }}
{{ end }}
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
{{end}}

View file

@ -27,8 +27,8 @@
Garage Garage
</div> </div>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="/garage/key">Mes identifiants</a> <a class="list-group-item list-group-item-action" href="/website/configure">Mes identifiants</a>
<a class="list-group-item list-group-item-action" href="/garage/website">Mes sites webs</a> <a class="list-group-item list-group-item-action" href="/website">Mes sites webs</a>
</div> </div>
</div> </div>
</div> </div>

211
website.go Normal file
View file

@ -0,0 +1,211 @@
package main
import (
"fmt"
"sort"
"strings"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
)
var (
ErrWebsiteNotFound = fmt.Errorf("Website not found")
ErrFetchBucketInfo = fmt.Errorf("Failed to fetch bucket information")
ErrWebsiteQuotaReached = fmt.Errorf("Can't create additional websites, quota reached")
ErrEmptyBucketName = fmt.Errorf("You can't create a website with an empty name")
ErrCantCreateBucket = fmt.Errorf("Can't create this bucket. Maybe another bucket already exists with this name or you have an invalid character")
ErrCantAllowKey = fmt.Errorf("Can't allow given key on the target bucket")
ErrCantConfigureBucket = fmt.Errorf("Unable to configure the bucket (activating website, adding quotas, etc.)")
)
type QuotaStat struct {
Current int64
Max int64
Ratio float64
Burstable bool
}
func NewQuotaStat(current, max int64, burstable bool) QuotaStat {
return QuotaStat {
Current: current,
Max: max,
Ratio: float64(current) / float64(max),
Burstable: burstable,
}
}
func (q *QuotaStat) IsFull() bool {
return q.Current >= q.Max
}
func (q *QuotaStat) Percent() int64 {
return int64(q.Ratio * 100)
}
func (q *QuotaStat) PrettyCurrent() string {
return prettyValue(q.Current)
}
func (q *QuotaStat) PrettyMax() string {
return prettyValue(q.Max)
}
func prettyValue(v int64) string {
if v < 1024 {
return fmt.Sprintf("%d octets", v)
}
v = v / 1024
if v < 1024 {
return fmt.Sprintf("%d kio", v)
}
v = v / 1024
if v < 1024 {
return fmt.Sprintf("%d Mio", v)
}
v = v / 1024
if v < 1024 {
return fmt.Sprintf("%d Gio", v)
}
v = v / 1024
return fmt.Sprintf("%d Tio", v)
}
type WebsiteId struct {
Pretty string
Internal string
Alt []string
Expanded bool
Url string
}
func NewWebsiteId(id string, aliases []string) *WebsiteId {
pretty := id
var alt []string
if len(aliases) > 0 {
pretty = aliases[0]
alt = aliases[1:]
}
expanded := strings.Contains(pretty, ".")
url := pretty
if !expanded {
url = fmt.Sprintf("%s.web.deuxfleurs.fr", pretty)
}
return &WebsiteId { pretty, id, alt, expanded, url }
}
func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId {
return NewWebsiteId(*binfo.Id, binfo.GlobalAliases)
}
type WebsiteController struct {
User *LoggedUser
WebsiteIdx map[string]*WebsiteId
PrettyList []string
WebsiteCount QuotaStat
}
func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) {
idx := map[string]*WebsiteId{}
var wlist []string
keyInfo, err := user.S3KeyInfo()
if err != nil {
return nil, err
}
for _, bckt := range(keyInfo.Buckets) {
if len(bckt.GlobalAliases) > 0 {
wid := NewWebsiteId(*bckt.Id, bckt.GlobalAliases)
idx[wid.Pretty] = wid
wlist = append(wlist, wid.Pretty)
}
}
sort.Strings(wlist)
maxW := user.Quota.WebsiteCount
quota := NewQuotaStat(int64(len(wlist)), maxW, true)
return &WebsiteController { user, idx, wlist, quota }, nil
}
func (w *WebsiteController) List() []*WebsiteId {
r := make([]*WebsiteId, 0, len(w.PrettyList))
for _, k := range w.PrettyList {
r = append(r, w.WebsiteIdx[k])
}
return r
}
func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
website, ok := w.WebsiteIdx[pretty]
if !ok {
return nil, ErrWebsiteNotFound
}
binfo, err := grgGetBucket(website.Internal)
if err != nil {
return nil, ErrFetchBucketInfo
}
return NewWebsiteView(binfo), nil
}
func (w *WebsiteController) Patch(patch *WebsitePatch) (*WebsiteView, error) {
return nil, nil
}
func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
if pretty == "" {
return nil, ErrEmptyBucketName
}
if w.WebsiteCount.IsFull() {
return nil, ErrWebsiteQuotaReached
}
binfo, err := grgCreateBucket(pretty)
if err != nil {
return nil, ErrCantCreateBucket
}
s3key, err := w.User.S3KeyInfo()
if err != nil {
return nil, err
}
binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId)
if err != nil {
return nil, ErrCantAllowKey
}
qr := w.User.Quota.DefaultWebsiteQuota()
wr := allowWebsiteDefault()
ur := garage.NewUpdateBucketRequest()
ur.SetWebsiteAccess(*wr)
ur.SetQuotas(*qr)
binfo, err = grgUpdateBucket(*binfo.Id, ur)
if err != nil {
return nil, ErrCantConfigureBucket
}
return NewWebsiteView(binfo), nil
}
type WebsiteView struct {
Name *WebsiteId
Size QuotaStat
Files QuotaStat
}
func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView {
q := binfo.GetQuotas()
wid := NewWebsiteIdFromBucketInfo(binfo)
size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true)
objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false)
return &WebsiteView { wid, size, objects }
}
type WebsitePatch struct {
size int64
}