Skip to content
This repository was archived by the owner on Sep 2, 2024. It is now read-only.

Commit 65458a8

Browse files
committed
initial version of the web UI handling create app, login and simple db mgmt close #12
1 parent 689ee5a commit 65458a8

File tree

11 files changed

+573
-26
lines changed

11 files changed

+573
-26
lines changed

account.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package staticbackend
22

33
import (
4-
"context"
54
"fmt"
65
"log"
76
"math/rand"
@@ -19,7 +18,6 @@ import (
1918
"github.com/stripe/stripe-go/v71/customer"
2019
"github.com/stripe/stripe-go/v71/sub"
2120

22-
"go.mongodb.org/mongo-driver/bson"
2321
"go.mongodb.org/mongo-driver/bson/primitive"
2422
)
2523

@@ -32,20 +30,32 @@ var (
3230
type accounts struct{}
3331

3432
func (a *accounts) create(w http.ResponseWriter, r *http.Request) {
35-
email := strings.ToLower(r.URL.Query().Get("email"))
33+
var email string
34+
fromCLI := true
35+
36+
// the CLI do a GET for the account initialization, we can then
37+
// base the rest of the flow on the fact that the web UI POST data
38+
if r.Method == http.MethodPost {
39+
fromCLI = false
40+
41+
r.ParseForm()
42+
43+
email = strings.ToLower(r.Form.Get("email"))
44+
} else {
45+
email = strings.ToLower(r.URL.Query().Get("email"))
46+
}
3647
// TODO: cheap email validation
3748
if len(email) < 4 || strings.Index(email, "@") == -1 || strings.Index(email, ".") == -1 {
3849
http.Error(w, "invalid email", http.StatusBadRequest)
3950
return
4051
}
4152

4253
db := client.Database("sbsys")
43-
ctx := context.Background()
44-
count, err := db.Collection("accounts").CountDocuments(ctx, bson.M{"email": email})
54+
exists, err := internal.EmailExists(db, email)
4555
if err != nil {
4656
http.Error(w, err.Error(), http.StatusInternalServerError)
4757
return
48-
} else if count > 0 {
58+
} else if exists {
4959
http.Error(w, "Please use a different/valid email.", http.StatusInternalServerError)
5060
return
5161
}
@@ -101,11 +111,11 @@ func (a *accounts) create(w http.ResponseWriter, r *http.Request) {
101111
retry := 10
102112
dbName := randStringRunes(12)
103113
for {
104-
count, err = db.Collection("bases").CountDocuments(ctx, bson.M{"name": dbName})
114+
exists, err = internal.DatabaseExists(db, dbName)
105115
if err != nil {
106116
http.Error(w, err.Error(), http.StatusInternalServerError)
107117
return
108-
} else if count > 0 {
118+
} else if exists {
109119
retry--
110120
dbName = randStringRunes(12)
111121
continue
@@ -120,7 +130,7 @@ func (a *accounts) create(w http.ResponseWriter, r *http.Request) {
120130
Whitelist: []string{"localhost"},
121131
}
122132

123-
if _, err := db.Collection("bases").InsertOne(ctx, base); err != nil {
133+
if err := internal.CreateBase(db, base); err != nil {
124134
http.Error(w, err.Error(), http.StatusInternalServerError)
125135
return
126136
}
@@ -194,7 +204,17 @@ func (a *accounts) create(w http.ResponseWriter, r *http.Request) {
194204
return
195205
}
196206

197-
respond(w, http.StatusOK, signUpURL)
207+
if fromCLI {
208+
respond(w, http.StatusOK, signUpURL)
209+
return
210+
}
211+
212+
if strings.HasPrefix(signUpURL, "https") {
213+
http.Redirect(w, r, signUpURL, http.StatusSeeOther)
214+
return
215+
}
216+
217+
render(w, r, "login.html", nil, &Flash{Type: "sucess", Message: "We've emailed you all the information you need to get started."})
198218
}
199219

200220
func (a *accounts) auth(w http.ResponseWriter, r *http.Request) {

db/base.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,12 @@ func (b *Base) List(auth internal.Auth, db *mongo.Database, col string, params L
8181

8282
skips := params.Size * (params.Page - 1)
8383

84-
sortBy := bson.M{internal.FieldID: 1}
84+
if len(params.SortBy) == 0 || strings.EqualFold(params.SortBy, "id") {
85+
params.SortBy = internal.FieldID
86+
}
87+
sortBy := bson.M{params.SortBy: 1}
8588
if params.SortDescending {
86-
sortBy[internal.FieldID] = -1
89+
sortBy[params.SortBy] = -1
8790
}
8891

8992
opt := options.Find()
@@ -158,6 +161,9 @@ func (b *Base) Query(auth internal.Auth, db *mongo.Database, col string, filter
158161

159162
skips := params.Size * (params.Page - 1)
160163

164+
if len(params.SortBy) == 0 || strings.EqualFold(params.SortBy, "id") {
165+
params.SortBy = internal.FieldID
166+
}
161167
sortBy := bson.M{params.SortBy: 1}
162168
if params.SortDescending {
163169
sortBy[params.SortBy] = -1

internal/account.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ type Customer struct {
5757
Created time.Time `bson:"created" json:"created"`
5858
}
5959

60+
func EmailExists(db *mongo.Database, email string) (bool, error) {
61+
count, err := db.Collection("accounts").CountDocuments(ctx, bson.M{"email": email})
62+
if err != nil {
63+
return false, err
64+
}
65+
return count > 0, nil
66+
}
67+
6068
func FindToken(db *mongo.Database, id primitive.ObjectID, token string) (tok Token, err error) {
6169
sr := db.Collection("sb_tokens").FindOne(ctx, bson.M{FieldID: id, FieldToken: token})
6270
err = sr.Decode(&tok)
@@ -94,8 +102,23 @@ func CreateAccount(db *mongo.Database, cus Customer) error {
94102
return nil
95103
}
96104

105+
func CreateBase(db *mongo.Database, base BaseConfig) error {
106+
if _, err := db.Collection("bases").InsertOne(ctx, base); err != nil {
107+
return err
108+
}
109+
return nil
110+
}
111+
97112
func FindDatabase(db *mongo.Database, id primitive.ObjectID) (conf BaseConfig, err error) {
98113
sr := db.Collection("bases").FindOne(ctx, bson.M{FieldID: id})
99114
err = sr.Decode(&conf)
100115
return
101116
}
117+
118+
func DatabaseExists(db *mongo.Database, name string) (bool, error) {
119+
count, err := db.Collection("bases").CountDocuments(ctx, bson.M{"name": name})
120+
if err != nil {
121+
return false, err
122+
}
123+
return count > 0, nil
124+
}

render.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"net/http"
88
"os"
99
"strings"
10+
11+
"go.mongodb.org/mongo-driver/bson"
12+
"go.mongodb.org/mongo-driver/bson/primitive"
1013
)
1114

1215
var (
@@ -20,6 +23,8 @@ func loadTemplates() error {
2023
return err
2124
}
2225

26+
funcs := customFuncs()
27+
2328
for _, e := range entries {
2429
partials = append(partials, fmt.Sprintf("./templates/partials/%s", e.Name()))
2530
}
@@ -39,7 +44,7 @@ func loadTemplates() error {
3944

4045
cur := append([]string{name}, partials...)
4146

42-
t, err := template.New(tmpl.Name()).ParseFiles(cur...)
47+
t, err := template.New(tmpl.Name()).Funcs(funcs).ParseFiles(cur...)
4348
if err != nil {
4449
return err
4550
}
@@ -94,3 +99,24 @@ func renderErr(w http.ResponseWriter, r *http.Request, err error) {
9499
}
95100
render(w, r, "err.html", nil, nil)
96101
}
102+
103+
func customFuncs() template.FuncMap {
104+
return template.FuncMap{
105+
"getField": func(s string, doc interface{}) string {
106+
dict, ok := doc.(bson.M)
107+
if !ok {
108+
return "error converting document"
109+
}
110+
v, ok := dict[s]
111+
if !ok {
112+
return "n/a"
113+
}
114+
115+
oid, ok := v.(primitive.ObjectID)
116+
if ok {
117+
return oid.Hex()
118+
}
119+
return fmt.Sprintf("%v", v)
120+
},
121+
}
122+
}

server.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func Start(dbHost, port string) {
110110

111111
// account
112112
acct := &accounts{}
113-
http.Handle("/account/init", middleware.Chain(http.HandlerFunc(acct.create), pubWithDB...))
113+
http.HandleFunc("/account/init", acct.create)
114114
http.Handle("/account/auth", middleware.Chain(http.HandlerFunc(acct.auth), stdRoot...))
115115
http.Handle("/account/portal", middleware.Chain(http.HandlerFunc(acct.portal), stdRoot...))
116116

@@ -142,6 +142,9 @@ func Start(dbHost, port string) {
142142
webUI := ui{base: &db.Base{PublishDocument: volatile.PublishDocument}}
143143
http.HandleFunc("/ui/login", webUI.auth)
144144
http.Handle("/ui/db", middleware.Chain(http.HandlerFunc(webUI.dbCols), stdRoot...))
145+
http.Handle("/ui/db/save", middleware.Chain(http.HandlerFunc(webUI.dbSave), stdRoot...))
146+
http.Handle("/ui/db/del/", middleware.Chain(http.HandlerFunc(webUI.dbDel), stdRoot...))
147+
http.Handle("/ui/db/", middleware.Chain(http.HandlerFunc(webUI.dbDoc), stdRoot...))
145148
http.HandleFunc("/", webUI.login)
146149

147150
log.Fatal(http.ListenAndServe(":"+port, nil))
@@ -225,3 +228,11 @@ func sudoCache(w http.ResponseWriter, r *http.Request) {
225228
respond(w, http.StatusOK, true)
226229
}
227230
}
231+
232+
func getURLPart(s string, idx int) string {
233+
parts := strings.Split(s, "/")
234+
if len(parts) <= idx {
235+
return ""
236+
}
237+
return parts[idx]
238+
}

templates/db_cols.html

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,133 @@
33
<body>
44
{{template "navbar" .}}
55

6-
<div class="container p-6">
7-
<div class="columns pt-6">
8-
{{range .Data}}
9-
<p>{{.}}</p>
10-
{{end}}
11-
</div>
6+
<div class="container p-6" x-data="{showQuery: {{if .Data.Query}}true{{else}}false{{end}}}">
7+
<!-- collections and filters -->
8+
<form action="/ui/db" method="POST">
9+
<div class="columns pt-6">
10+
<div class="column is-one-sixth">
11+
<div class="field">
12+
<label class="label">Collections</label>
13+
<div class="control">
14+
<div class="select">
15+
<select name="col">
16+
{{$cur := .Data.Collection}}
17+
{{range .Data.Collections}}
18+
<option value="{{.}}" {{if eq . $cur}}selected{{end}}>
19+
{{.}}
20+
</option>
21+
{{end}}
22+
</select>
23+
</div>
24+
</div>
25+
</div>
26+
</div>
27+
<div class="column is-two-sixth">
28+
<div class="field">
29+
<label class="label">Sort</label>
30+
<div class="control">
31+
<div class="columns">
32+
<div class="column">
33+
<div class="select">
34+
<select name="sortby">
35+
{{$sortby := .Data.SortBy}}
36+
{{range .Data.Columns}}
37+
<option value="{{.}}" {{if eq . $sortby}}selected{{end}}>
38+
{{.}}
39+
</option>
40+
{{end}}
41+
</select>
42+
</div>
43+
</div>
44+
<div class="column">
45+
<div class="select">
46+
<select name="desc">
47+
<option value="0" {{if eq .Data.SortDescending "0"}}selected{{end}}>Ascending</option>
48+
<option value="1" {{if eq .Data.SortDescending "1"}}selected{{end}}>Descending</option>
49+
</select>
50+
</div>
51+
</div>
52+
</div>
53+
54+
55+
</div>
56+
</div>
57+
</div>
58+
<div class="column is-two-sixth">
59+
<div class="field">
60+
<label class="label">Fields to display (separated by ,)</label>
61+
<div class="control">
62+
<input name="fields" class="input" placeholder="i.e. email,client,invoiceId">
63+
</div>
64+
</div>
65+
</div>
66+
<div class="column is-one-sixth">
67+
<vid class="field">
68+
<label class="label">&nbsp;</label>
69+
<div class="control">
70+
<button type="submit" class="button is-primary">
71+
Refresh
72+
</button>
73+
</div>
74+
</vid>
75+
</div>
76+
</div>
77+
78+
<div class="columns pt-3">
79+
<div x-show="showQuery == false" class="column">
80+
<a @click="showQuery = true" class="button">
81+
Write a query (experimental)
82+
</a>
83+
</div>
84+
<div x-show="showQuery" class="column is-half">
85+
<textarea name="query" class="textarea" rows="9" placeholder='[["field", ">=", 3]]'>{{.Data.Query}}</textarea>
86+
</div>
87+
<div x-show="showQuery" class="column is-half">
88+
<div class="box content">
89+
<h5>How to write queries</h5>
90+
<ul>
91+
<li>Use a JSON syntax</li>
92+
<li>A clause is an array <code>["field", "operator", "value"]</code></li>
93+
<li>Use proper JavaScript type: <conde>["isActive", "=", true]</conde>
94+
</li>
95+
<li>Wrap all your clauses into a parent array: <code>[["field", "=", "value"], ["field2"...]]</code></li>
96+
<li>Available operators: =, !=, &lt;, &gt; &lt;=, &gt;=, in, !in</li>
97+
</ul>
98+
</div>
99+
</div>
100+
</div>
101+
</form>
102+
<!-- /collections and filters -->
103+
104+
<table class="table is-bordered is-striped py-6" style="overflow-x: hidden;">
105+
<thead>
106+
<tr>
107+
{{range .Data.Columns}}
108+
<th>{{.}}</th>
109+
{{end}}
110+
</tr>
111+
</thead>
112+
<tbody>
113+
{{$col := .Data.Collection}}
114+
{{$cols := .Data.Columns}}
115+
{{range .Data.Docs}}
116+
{{$doc := .}}
117+
<tr>
118+
{{range $cols}}
119+
<td>
120+
{{if eq . "id"}}
121+
<a href="/ui/db/{{getField . $doc}}?col={{$col}}">
122+
{{getField . $doc}}
123+
</a>
124+
{{else}}
125+
{{getField . $doc}}
126+
{{end}}
127+
</td>
128+
{{end}}
129+
</tr>
130+
{{end}}
131+
</tbody>
132+
</table>
12133
</div>
13134

14135
</body>

0 commit comments

Comments
 (0)