aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Schanzenbach <schanzen@gnunet.org>2023-12-06 14:52:13 +0100
committerMartin Schanzenbach <schanzen@gnunet.org>2023-12-06 14:52:13 +0100
commita84002ca1ad4f70cc7f6c5105424f7a8ab1af6d8 (patch)
tree00563ebaf7e6c17876d5b6904e090ab78215905c
parent86fb4f92dd0a6624e3cd7e7f25781c011483c642 (diff)
downloadgnunet-gns-registrar-a84002ca1ad4f70cc7f6c5105424f7a8ab1af6d8.tar.gz
gnunet-gns-registrar-a84002ca1ad4f70cc7f6c5105424f7a8ab1af6d8.zip
Towards paying with the taler demo
-rw-r--r--gns-registrar.conf4
-rw-r--r--go.mod3
-rw-r--r--pkg/rest/gnsregistrar.go233
-rw-r--r--web/templates/buy.html22
-rw-r--r--web/templates/name.html12
-rw-r--r--web/templates/names.html20
6 files changed, 207 insertions, 87 deletions
diff --git a/gns-registrar.conf b/gns-registrar.conf
index e929f1b..6b12eaa 100644
--- a/gns-registrar.conf
+++ b/gns-registrar.conf
@@ -9,8 +9,8 @@ default_doc_lang = en
9default_tos_path = terms/ 9default_tos_path = terms/
10default_pp_path = privacy/ 10default_pp_path = privacy/
11supported_doc_filetypes = text/html application/pdf application/epub application/xml text/plain 11supported_doc_filetypes = text/html application/pdf application/epub application/xml text/plain
12merchant_baseurl_private = http://merchant.taldir/instances/myInstance 12merchant_baseurl_private = https://backend.demo.taler.net
13merchant_token = superSecretToken 13merchant_token = sandbox
14registrar_landing = web/templates/landing.html 14registrar_landing = web/templates/landing.html
15registrar_name = web/templates/name.html 15registrar_name = web/templates/name.html
16registration_failed = templates/registration_failed.html 16registration_failed = templates/registration_failed.html
diff --git a/go.mod b/go.mod
index e68908e..ec487dd 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.18
4 4
5require ( 5require (
6 github.com/gorilla/mux v1.8.0 // indirect 6 github.com/gorilla/mux v1.8.0 // indirect
7 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
7 gopkg.in/ini.v1 v1.67.0 // indirect 8 gopkg.in/ini.v1 v1.67.0 // indirect
8 taler.net/taler-go.git v0.0.0-20220719135513-36eb87bf37a3 // indirect 9 taler.net/taler-go.git v0.0.0-20231206131418-346a54653b41 // indirect
9) 10)
diff --git a/pkg/rest/gnsregistrar.go b/pkg/rest/gnsregistrar.go
index b243e3e..7d3f6c4 100644
--- a/pkg/rest/gnsregistrar.go
+++ b/pkg/rest/gnsregistrar.go
@@ -20,7 +20,9 @@ package gnsregistrar
20 20
21import ( 21import (
22 "bytes" 22 "bytes"
23 "encoding/base64"
23 "encoding/json" 24 "encoding/json"
25 "errors"
24 "fmt" 26 "fmt"
25 "html/template" 27 "html/template"
26 "io" 28 "io"
@@ -28,15 +30,19 @@ import (
28 "os" 30 "os"
29 "time" 31 "time"
30 32
33 "github.com/skip2/go-qrcode"
31 "github.com/gorilla/mux" 34 "github.com/gorilla/mux"
32 "gopkg.in/ini.v1" 35 "gopkg.in/ini.v1"
33 "taler.net/taler-go.git/pkg/merchant" 36 "taler.net/taler-go.git/pkg/merchant"
37 talerutil "taler.net/taler-go.git/pkg/util"
34) 38)
35 39
36 40
37type RegistrationMetadata struct { 41type RegistrationMetadata struct {
38 Expiration uint64 `json:"expiration"` 42 Expiration uint64 `json:"expiration"`
39 Paid bool `json:"paid"` 43 Paid bool `json:"paid"`
44 OrderID string `json:"order_id"`
45 NeedsPaymentUntil time.Time `json:"needs_payment_until"`
40} 46}
41 47
42type IdentityInfo struct { 48type IdentityInfo struct {
@@ -85,8 +91,8 @@ type Registrar struct {
85 // name page 91 // name page
86 NameTpl *template.Template 92 NameTpl *template.Template
87 93
88 // all names page 94 // buy names page
89 AllNamesTpl *template.Template 95 BuyTpl *template.Template
90 96
91 // Merchant object 97 // Merchant object
92 Merchant merchant.Merchant 98 Merchant merchant.Merchant
@@ -97,6 +103,9 @@ type Registrar struct {
97 // Registration expiration (NOT record expiration!) 103 // Registration expiration (NOT record expiration!)
98 RelativeRegistrationExpiration time.Duration 104 RelativeRegistrationExpiration time.Duration
99 105
106 // Payment expiration (time you have to pay for registration)
107 PaymentExpiration time.Duration
108
100 // Name of our root zone 109 // Name of our root zone
101 RootZoneName string 110 RootZoneName string
102 111
@@ -113,10 +122,8 @@ type Registrar struct {
113 RegistrationPolicy string 122 RegistrationPolicy string
114 123
115 // Cost for a registration 124 // Cost for a registration
116 RegistrationCost uint64 125 RegistrationCost *talerutil.Amount
117 126
118 // Currency to use
119 RegistrationCostCurrency string
120} 127}
121 128
122type VersionResponse struct { 129type VersionResponse struct {
@@ -149,7 +156,7 @@ func (t *Registrar) landingPage(w http.ResponseWriter, r *http.Request) {
149func (t *Registrar) registerName(w http.ResponseWriter, r *http.Request) { 156func (t *Registrar) registerName(w http.ResponseWriter, r *http.Request) {
150 vars := mux.Vars(r) 157 vars := mux.Vars(r)
151 var namestoreRequest NamestoreRecord 158 var namestoreRequest NamestoreRecord
152 var delegationRecord RecordData 159 var delegationRecord RecordData
153 var metadataRecord RecordData 160 var metadataRecord RecordData
154 var gnunetError GnunetError 161 var gnunetError GnunetError
155 var registrationMetadata RegistrationMetadata 162 var registrationMetadata RegistrationMetadata
@@ -173,45 +180,193 @@ func (t *Registrar) registerName(w http.ResponseWriter, r *http.Request) {
173 Paid: false, 180 Paid: false,
174 Expiration: uint64(time.Now().Add(t.RelativeRegistrationExpiration).UnixMicro()), 181 Expiration: uint64(time.Now().Add(t.RelativeRegistrationExpiration).UnixMicro()),
175 } 182 }
176 metadataRecordValue, err := json.Marshal(registrationMetadata) 183 metadataRecordValue, err := json.Marshal(registrationMetadata)
177 if nil != err { 184 if nil != err {
178 http.Redirect(w, r, "/name?label="+vars["label"] + "&error=Registration failed", http.StatusSeeOther) 185 http.Redirect(w, r, "/name?label="+vars["label"] + "&error=Registration failed", http.StatusSeeOther)
179 return 186 return
180 } 187 }
181 metadataRecord.Value = string(metadataRecordValue) 188 metadataRecord.Value = string(metadataRecordValue)
182 namestoreRequest.RecordName = vars["label"] 189 namestoreRequest.RecordName = vars["label"]
183 namestoreRequest.Records = []RecordData{delegationRecord,metadataRecord} 190 namestoreRequest.Records = []RecordData{delegationRecord,metadataRecord}
184 reqString, _ := json.Marshal(namestoreRequest) 191 reqString, _ := json.Marshal(namestoreRequest)
185 // FIXME handle errors here 192 // FIXME handle errors here
186 fmt.Println(namestoreRequest) 193 fmt.Println(namestoreRequest)
187 resp, err := http.Post(t.GnunetUrl+"/namestore/" + t.RootZoneName, "application/json", bytes.NewBuffer(reqString)) 194 resp, err := http.Post(t.GnunetUrl+"/namestore/" + t.RootZoneName, "application/json", bytes.NewBuffer(reqString))
188 resp.Body.Close() 195 resp.Body.Close()
189 if http.StatusNoContent != resp.StatusCode { 196 if http.StatusNoContent != resp.StatusCode {
190 fmt.Printf("Got error: %d\n", resp.StatusCode) 197 fmt.Printf("Got error: %d\n", resp.StatusCode)
191 json.NewDecoder(resp.Body).Decode(&gnunetError) 198 json.NewDecoder(resp.Body).Decode(&gnunetError)
192 fmt.Println(gnunetError.Code) 199 fmt.Println(gnunetError.Code)
193 fmt.Println(err) 200 fmt.Println(err)
194 http.Redirect(w, r, "/name?label="+vars["label"] + "&error=" + gnunetError.Description, http.StatusSeeOther) 201 http.Redirect(w, r, "/name?label="+vars["label"] + "&error=" + gnunetError.Description, http.StatusSeeOther)
195 return 202 return
196 } 203 }
197 http.Redirect(w, r, "/name/"+ vars["label"] + "?registered=true", http.StatusSeeOther) 204 http.Redirect(w, r, "/name/"+ vars["label"] + "?registered=true", http.StatusSeeOther)
198 return 205 return
199} 206}
200 207
201
202func (t *Registrar) searchPage(w http.ResponseWriter, r *http.Request) { 208func (t *Registrar) searchPage(w http.ResponseWriter, r *http.Request) {
203 w.Header().Set("Content-Type", "text/html; charset=utf-8") 209 w.Header().Set("Content-Type", "text/html; charset=utf-8")
204 http.Redirect(w, r, "/name/"+r.URL.Query().Get("label"), http.StatusSeeOther) 210 http.Redirect(w, r, "/name/"+r.URL.Query().Get("label"), http.StatusSeeOther)
205 return 211 return
206} 212}
207 213
214func (t *Registrar) expireRegistration(label string) (error) {
215 var gnunetError GnunetError
216 client := &http.Client{}
217 req, _ := http.NewRequest(http.MethodDelete,t.GnunetUrl+"/namestore/" + t.RootZoneName + "/" + label, nil)
218 resp, err := client.Do(req)
219 resp.Body.Close()
220 if err != nil {
221 return err
222 }
223 if http.StatusNotFound == resp.StatusCode {
224 return nil
225 }
226 if http.StatusNoContent != resp.StatusCode {
227 fmt.Printf("Got error: %d\n", resp.StatusCode)
228 err = json.NewDecoder(resp.Body).Decode(&gnunetError)
229 return errors.New("GNUnet REST API error: " + gnunetError.Description)
230 }
231 return nil
232}
233
234func (t *Registrar) setupRegistrationMetadataBeforePayment(label string, zkey string, orderId string, paymentUntil time.Time) (error) {
235 var namestoreRequest NamestoreRecord
236 var delegationRecord RecordData
237 var metadataRecord RecordData
238 var gnunetError GnunetError
239 var registrationMetadata RegistrationMetadata
240 delegationRecord.IsPrivate = true // Private until payment is through
241 delegationRecord.IsRelativeExpiration = true
242 delegationRecord.IsSupplemental = false
243 delegationRecord.IsMaintenance = false
244 delegationRecord.IsShadow = false
245 delegationRecord.RecordType = "PKEY" // FIXME get from value
246 delegationRecord.RelativeExpiration = uint64(t.RelativeDelegationExpiration.Microseconds())
247 delegationRecord.Value = zkey
248 metadataRecord.IsPrivate = true
249 metadataRecord.IsRelativeExpiration = true
250 metadataRecord.IsSupplemental = false
251 metadataRecord.IsMaintenance = true
252 metadataRecord.IsShadow = false
253 metadataRecord.RecordType = "TXT" // FIXME use new recory type
254 metadataRecord.RelativeExpiration = uint64(t.RelativeDelegationExpiration.Microseconds())
255 registrationMetadata = RegistrationMetadata{
256 Paid: false,
257 OrderID: orderId,
258 NeedsPaymentUntil: paymentUntil,
259 Expiration: uint64(time.Now().Add(t.RelativeRegistrationExpiration).UnixMicro()),
260 }
261 metadataRecordValue, err := json.Marshal(registrationMetadata)
262 if nil != err {
263 return err
264 }
265 metadataRecord.Value = string(metadataRecordValue)
266 namestoreRequest.RecordName = label
267 namestoreRequest.Records = []RecordData{delegationRecord,metadataRecord}
268 reqString, _ := json.Marshal(namestoreRequest)
269 // FIXME handle errors here
270 fmt.Println(namestoreRequest)
271 resp, err := http.Post(t.GnunetUrl+"/namestore/" + t.RootZoneName, "application/json", bytes.NewBuffer(reqString))
272 resp.Body.Close()
273 if http.StatusNoContent != resp.StatusCode {
274 fmt.Printf("Got error: %d\n", resp.StatusCode)
275 err = json.NewDecoder(resp.Body).Decode(&gnunetError)
276 return errors.New("GNUnet REST API error: " + gnunetError.Description)
277 }
278 return nil
279}
280
208func (t *Registrar) buyPage(w http.ResponseWriter, r *http.Request) { 281func (t *Registrar) buyPage(w http.ResponseWriter, r *http.Request) {
209 vars := mux.Vars(r) 282 vars := mux.Vars(r)
210 if t.RegistrationCost == 0 { 283 /*if t.RegistrationCost == 0 {
211 http.Redirect(w, r, "/name/"+vars["label"]+"/register?zkey="+r.URL.Query().Get("zkey"), http.StatusSeeOther) 284 http.Redirect(w, r, "/name/"+vars["label"]+"/register?zkey="+r.URL.Query().Get("zkey"), http.StatusSeeOther)
212 return 285 return
286 }*/
287 var metadataResponse RegistrationMetadata
288 var namestoreResponse NamestoreRecord
289 w.Header().Set("Content-Type", "text/html; charset=utf-8")
290 var metadataExists = false
291 // FIXME redirect back if label empty
292 resp, err := http.Get(t.GnunetUrl + "/namestore/" + t.RootZoneName + "/" + vars["label"] + "?include_maintenance=yes")
293 if err != nil {
294 fmt.Printf("Failed to get zone contents")
295 return
296 }
297 defer resp.Body.Close()
298 if http.StatusOK == resp.StatusCode {
299 respData, err := io.ReadAll(resp.Body)
300 if err != nil {
301 fmt.Printf("Failed to get zone contents" + err.Error())
302 return
303 }
304 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse)
305 if err != nil {
306 fmt.Printf("Failed to get zone contents" + err.Error())
307 return
308 }
309 for _, record := range namestoreResponse.Records {
310 if record.RecordType == "PKEY" {
311 continue
312 }
313 if record.RecordType == "TXT" {
314 err = json.Unmarshal([]byte(record.Value), &metadataResponse)
315 if err != nil {
316 fmt.Printf("Failed to get zone contents" + err.Error())
317 return
318 }
319 metadataExists = true
320 }
321 }
322 } else {
323 fmt.Printf("Failed to get zone contents" + err.Error())
324 return
325 }
326 var errorMsg = ""
327 // FIXME check for order expiration
328 if metadataExists {
329 if metadataResponse.Paid {
330 http.Redirect(w, r, "/name/" +vars["label"] + "?error=Registration failed: Already paid", http.StatusSeeOther)
331 return
332 }
333 if !time.Now().After(metadataResponse.NeedsPaymentUntil) {
334 http.Redirect(w, r, "/name/" + vars["label"] + "?error=Registration failed: Name is being paid", http.StatusSeeOther)
335 return
336 }
337 t.expireRegistration(vars["label"])
338 }
339 orderID, newOrderErr := t.Merchant.AddNewOrder(*t.RegistrationCost, "GNS registrar name registration", "/name/" + vars["label"])
340 if newOrderErr != nil {
341 fmt.Println(newOrderErr)
342 http.Redirect(w, r, "/name/"+vars["label"] + "?error=Registration failed: Unable to create order", http.StatusSeeOther)
343 return
213 } 344 }
214 http.Redirect(w, r, "/name/"+vars["label"]+"?error=Not implemented", http.StatusSeeOther) 345 metadataResponse.OrderID = orderID
346
347 payto, paytoErr := t.Merchant.IsOrderPaid(metadataResponse.OrderID)
348 if paytoErr != nil {
349 http.Redirect(w, r, "/name/"+vars["label"] + "?error=Registration failed: Error getting payment data", http.StatusSeeOther)
350 return
351 }
352 qrPng, qrErr := qrcode.Encode(payto, qrcode.Medium, 256)
353 if qrErr != nil {
354 http.Redirect(w, r, "/name/"+vars["label"] + "?error=Registration failed: Error generating QR code", http.StatusSeeOther)
355 return
356 }
357 paymentUntil := time.Now().Add(t.PaymentExpiration)
358 err = t.setupRegistrationMetadataBeforePayment(vars["label"], vars["zkey"], orderID, paymentUntil)
359 encodedPng := base64.StdEncoding.EncodeToString(qrPng)
360 fullData := map[string]interface{}{
361 "qrCode": template.URL("data:image/png;base64," + encodedPng),
362 "payto": template.URL(payto),
363 "fulfillmentUrl": template.URL("/name/" + vars["label"]),
364 "label": vars["label"],
365 "error": errorMsg,
366 "cost": t.RegistrationCost,
367 "suffixHint": t.SuffixHint,
368 }
369 t.BuyTpl.Execute(w, fullData)
215 return 370 return
216} 371}
217 372
@@ -264,7 +419,6 @@ func (t *Registrar) namePage(w http.ResponseWriter, r *http.Request) {
264 "label": vars["label"], 419 "label": vars["label"],
265 "error": r.URL.Query().Get("error"), 420 "error": r.URL.Query().Get("error"),
266 "cost": t.RegistrationCost, 421 "cost": t.RegistrationCost,
267 "currency": t.RegistrationCostCurrency,
268 "available": available, 422 "available": available,
269 "currentValue": value, 423 "currentValue": value,
270 "suffixHint": t.SuffixHint, 424 "suffixHint": t.SuffixHint,
@@ -276,34 +430,6 @@ func (t *Registrar) namePage(w http.ResponseWriter, r *http.Request) {
276 return 430 return
277} 431}
278 432
279func (t *Registrar) allNamesPage(w http.ResponseWriter, r *http.Request) {
280 var namestoreResponse []NamestoreRecord
281 w.Header().Set("Content-Type", "text/html; charset=utf-8")
282 resp, err := http.Get(t.GnunetUrl + "/namestore/" + t.RootZoneName)
283 if err != nil {
284 fmt.Printf("Failed to get zone contents")
285 return
286 }
287 defer resp.Body.Close()
288 if http.StatusOK != resp.StatusCode {
289 fmt.Printf("Failed to get zone contents. Retcode=%d", resp.StatusCode)
290 return
291 }
292 respData, err := io.ReadAll(resp.Body)
293 if err != nil {
294 fmt.Printf("Failed to get zone contents" + err.Error())
295 return
296 }
297 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse)
298 if err != nil {
299 fmt.Printf("Failed to get zone contents" + err.Error())
300 return
301 }
302 fmt.Println(namestoreResponse)
303 t.AllNamesTpl.Execute(w, namestoreResponse)
304 return
305}
306
307func (t *Registrar) setupHandlers() { 433func (t *Registrar) setupHandlers() {
308 t.Router = mux.NewRouter().StrictSlash(true) 434 t.Router = mux.NewRouter().StrictSlash(true)
309 435
@@ -312,7 +438,6 @@ func (t *Registrar) setupHandlers() {
312 t.Router.HandleFunc("/name/{label}/buy", t.buyPage).Methods("GET") 438 t.Router.HandleFunc("/name/{label}/buy", t.buyPage).Methods("GET")
313 t.Router.HandleFunc("/name/{label}/register", t.registerName).Methods("GET") 439 t.Router.HandleFunc("/name/{label}/register", t.registerName).Methods("GET")
314 t.Router.HandleFunc("/search", t.searchPage).Methods("GET") 440 t.Router.HandleFunc("/search", t.searchPage).Methods("GET")
315 t.Router.HandleFunc("/names", t.allNamesPage).Methods("GET")
316 441
317 /* ToS API */ 442 /* ToS API */
318 // t.Router.HandleFunc("/terms", t.termsResponse).Methods("GET") 443 // t.Router.HandleFunc("/terms", t.termsResponse).Methods("GET")
@@ -347,21 +472,21 @@ func (t *Registrar) Initialize(cfgfile string) {
347 if err != nil { 472 if err != nil {
348 fmt.Println(err) 473 fmt.Println(err)
349 } 474 }
350 allNamesTplFile := t.Cfg.Section("gns-registrar").Key("registrar_all_names").MustString("web/templates/names.html") 475 buyTplFile := t.Cfg.Section("gns-registrar").Key("buy_template").MustString("web/templates/buy.html")
351 t.AllNamesTpl, err = template.ParseFiles(allNamesTplFile) 476 t.BuyTpl, err = template.ParseFiles(buyTplFile)
352 if err != nil { 477 if err != nil {
353 fmt.Println(err) 478 fmt.Println(err)
354 } 479 }
355 t.RegistrationCost = t.Cfg.Section("gns-registrar").Key("registration_cost").MustUint64(0) 480 paymentExp := t.Cfg.Section("gns-registrar").Key("payment_required_expiration").MustString("48h")
356 recordExp := t.Cfg.Section("gns-registrar").Key("relative_delegation_expiration").MustString("24h") 481 recordExp := t.Cfg.Section("gns-registrar").Key("relative_delegation_expiration").MustString("24h")
357 registrationExp := t.Cfg.Section("gns-registrar").Key("registration_expiration").MustString("8760h") 482 registrationExp := t.Cfg.Section("gns-registrar").Key("registration_expiration").MustString("8760h")
358 fmt.Println(recordExp)
359 fmt.Println(registrationExp)
360 t.RelativeRegistrationExpiration, _ = time.ParseDuration(registrationExp) 483 t.RelativeRegistrationExpiration, _ = time.ParseDuration(registrationExp)
361 t.RelativeDelegationExpiration, _ = time.ParseDuration(recordExp) 484 t.RelativeDelegationExpiration, _ = time.ParseDuration(recordExp)
485 t.PaymentExpiration, _ = time.ParseDuration(paymentExp)
362 fmt.Println(t.RelativeDelegationExpiration) 486 fmt.Println(t.RelativeDelegationExpiration)
363 fmt.Println(t.RelativeRegistrationExpiration) 487 fmt.Println(t.RelativeRegistrationExpiration)
364 t.RegistrationCostCurrency = t.Cfg.Section("gns-registrar").Key("registration_cost_currency").MustString("EUR") 488 costStr := t.Cfg.Section("gns-registrar").Key("registration_cost").MustString("KUDOS:13")
489 t.RegistrationCost, err = talerutil.ParseAmount(costStr)
365 t.SuffixHint = t.Cfg.Section("gns-registrar").Key("suffix_hint").MustString("example.alt") 490 t.SuffixHint = t.Cfg.Section("gns-registrar").Key("suffix_hint").MustString("example.alt")
366 t.RootZoneName = t.Cfg.Section("gns-registrar").Key("root_zone_name").MustString("test") 491 t.RootZoneName = t.Cfg.Section("gns-registrar").Key("root_zone_name").MustString("test")
367 t.GnunetUrl = t.Cfg.Section("gns-registrar").Key("gnunet_baseurl_private").MustString("http://localhost:7776") 492 t.GnunetUrl = t.Cfg.Section("gns-registrar").Key("gnunet_baseurl_private").MustString("http://localhost:7776")
@@ -390,8 +515,8 @@ func (t *Registrar) Initialize(cfgfile string) {
390 fmt.Printf("Failed to get zone contents" + err.Error()) 515 fmt.Printf("Failed to get zone contents" + err.Error())
391 os.Exit(1) 516 os.Exit(1)
392 } 517 }
393 merchURL := t.Cfg.Section("gns-registrar").Key("merchant_baseurl_private").MustString("http://merchant.gnsregistrar/instances/myInstance") 518 merchURL := t.Cfg.Section("gns-registrar").Key("merchant_baseurl_private").MustString("https://backend.demo.taler.net")
394 merchToken := t.Cfg.Section("gns-registrar").Key("merchant_token").MustString("secretAccessToken") 519 merchToken := t.Cfg.Section("gns-registrar").Key("merchant_token").MustString("sandbox")
395 t.Merchant = merchant.NewMerchant(merchURL, merchToken) 520 t.Merchant = merchant.NewMerchant(merchURL, merchToken)
396 t.setupHandlers() 521 t.setupHandlers()
397} 522}
diff --git a/web/templates/buy.html b/web/templates/buy.html
new file mode 100644
index 0000000..e6d953b
--- /dev/null
+++ b/web/templates/buy.html
@@ -0,0 +1,22 @@
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <!-- Required meta tags -->
5 <meta charset="utf-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7 <link href="/css/bootstrap.min.css" rel="stylesheet">
8 <title>Buy</title>
9 </head>
10 <body>
11 <div class ="container text-center">
12 <h1 class="mb-3">To register your name <i class="text-primary">{{.label}}</i>.<i class="text-secondary">{{.suffixHint}}</i> please pay here:</h1>
13
14 <a href="{{.payto}}" class="btn btn-success mb-3">Pay with TALER</a><br/>
15 Alternatively, you can pay using your mobile wallet by scanning the QR code below:<br/>
16 <a href="{{.fulfillmentUrl}}">
17 <img class="qr" src="{{.qrCode}}"/>
18 </a><br/>
19 After you pay with a mobile wallet, please click on the QR code to finalize your registration.
20 </div>
21 </body>
22</html>
diff --git a/web/templates/name.html b/web/templates/name.html
index 2b780f4..5307668 100644
--- a/web/templates/name.html
+++ b/web/templates/name.html
@@ -31,14 +31,10 @@
31 <h1 class="mb-5"><i class="text-primary">{{.label}}</i> is still <span class="text-success">available</span> for registration.</h1> 31 <h1 class="mb-5"><i class="text-primary">{{.label}}</i> is still <span class="text-success">available</span> for registration.</h1>
32 <form action="/name/{{.label}}/buy" method="get" class="align-items-center"> 32 <form action="/name/{{.label}}/buy" method="get" class="align-items-center">
33 <div class="input-group mb-2 w-75"> 33 <div class="input-group mb-2 w-75">
34 <span class="input-group-text" id="reg-prefix"><i class="text-primary">{{.label}}</i>.<i class="text-secondary">{{.suffixHint}}</i>: </span> 34 <span class="input-group-text" id="reg-prefix"><i class="text-primary">{{.label}}</i>.<i class="text-secondary">{{.suffixHint}}</i>: </span>
35 <input type="hidden" name="label" value="{{.label}}"> 35 <input type="hidden" name="label" value="{{.label}}">
36 <input name="zkey" class="form-control form-control-lg" maxlength="63" type="text" placeholder="Enter your zone key here!" required autofocus> 36 <input name="zkey" class="form-control form-control-lg" maxlength="63" type="text" placeholder="Enter your zone key here!" required autofocus>
37 {{if eq .cost 0}} 37 <input class="btn btn-primary" type="submit" value="Register for {{.cost}}">
38 <input class="btn btn-primary" type="submit" value="Register for free">
39 {{else}}
40 <input class="btn btn-primary" type="submit" value="Register for {{.cost}} {{.currency}}">
41 {{end}}
42 </div> 38 </div>
43 </form> 39 </form>
44 {{else}} 40 {{else}}
@@ -54,10 +50,6 @@
54 </form> 50 </form>
55 <form action="/renew" method="get" class="align-items-center"> 51 <form action="/renew" method="get" class="align-items-center">
56 <span>Registration valid until: <i>{{.registeredUntil}}</i></span><br/> 52 <span>Registration valid until: <i>{{.registeredUntil}}</i></span><br/>
57 {{if .modificationAllowed}}
58 <span>Registration extension cost: <i>23 EUR</i></span><br/>
59 <input class="btn btn-primary" type="submit" value="Extend registration">
60 {{end}}
61 </form> 53 </form>
62 {{end}} 54 {{end}}
63 <a class="btn btn-primary mt-5" href="/">Back</a> 55 <a class="btn btn-primary mt-5" href="/">Back</a>
diff --git a/web/templates/names.html b/web/templates/names.html
deleted file mode 100644
index f41ddf2..0000000
--- a/web/templates/names.html
+++ /dev/null
@@ -1,20 +0,0 @@
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <!-- Required meta tags -->
5 <meta charset="utf-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7 <link href="/css/style.css" rel="stylesheet">
8 <title>All Names Overview</title>
9 </head>
10 <body>
11 <div class="names">
12 <!-- FIXME: Probably make this a table -->
13 <ul>
14 {{range $val := .}}
15 <li>{{$val.RecordName}}: {{(index $val.Records 0).Value}}</li>
16 {{end}}
17 </ul>
18 </div>
19 </body>
20</html>