From 5080c566ac31ec622986c04f1812a1e88c88210e Mon Sep 17 00:00:00 2001 From: Lizzy Hunt Date: Fri, 29 Mar 2024 16:35:04 -0600 Subject: [PATCH] guestbook! --- api/api_keys.go | 11 ++- api/dns.go | 22 ++--- api/guestbook.go | 143 +++++++++++++++++++++++++++++ api/serve.go | 14 ++- args/args.go | 20 +++- database/dns.go | 14 ++- database/guestbook.go | 50 ++++++++++ database/migrate.go | 19 ++++ database/users.go | 33 +++++++ static/css/form.css | 9 ++ static/css/guestbook.css | 15 +++ static/css/styles.css | 1 + static/js/components/formatDate.js | 7 ++ static/js/script.js | 5 +- templates/api_keys.html | 16 ++-- templates/dns.html | 28 +++--- templates/guestbook.html | 57 +++++++++++- 17 files changed, 417 insertions(+), 47 deletions(-) create mode 100644 api/guestbook.go create mode 100644 database/guestbook.go create mode 100644 static/css/guestbook.css create mode 100644 static/js/components/formatDate.js diff --git a/api/api_keys.go b/api/api_keys.go index 17ed6c9..d636044 100644 --- a/api/api_keys.go +++ b/api/api_keys.go @@ -30,17 +30,22 @@ func CreateAPIKeyContinuation(context *RequestContext, req *http.Request, resp h Errors: []string{}, } - apiKeys, err := database.ListUserAPIKeys(context.DBConn, context.User.ID) + numKeys, err := database.CountUserAPIKeys(context.DBConn, context.User.ID) if err != nil { log.Println(err) resp.WriteHeader(http.StatusInternalServerError) return failure(context, req, resp) } - if len(apiKeys) >= MAX_USER_API_KEYS { + if numKeys >= MAX_USER_API_KEYS { formErrors.Errors = append(formErrors.Errors, "max api keys reached") } + if len(formErrors.Errors) > 0 { + (*context.TemplateData)["FormError"] = formErrors + return failure(context, req, resp) + } + _, err = database.SaveAPIKey(context.DBConn, &database.UserApiKey{ UserID: context.User.ID, Key: utils.RandomId(), @@ -50,8 +55,6 @@ func CreateAPIKeyContinuation(context *RequestContext, req *http.Request, resp h resp.WriteHeader(http.StatusInternalServerError) return failure(context, req, resp) } - - http.Redirect(resp, req, "/keys", http.StatusFound) return success(context, req, resp) } } diff --git a/api/dns.go b/api/dns.go index 0205f5d..a1739d3 100644 --- a/api/dns.go +++ b/api/dns.go @@ -72,6 +72,16 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res formErrors.Errors = append(formErrors.Errors, "invalid ttl") } + dnsRecordCount, err := database.CountUserDNSRecords(context.DBConn, context.User.ID) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + if dnsRecordCount >= MAX_USER_RECORDS { + formErrors.Errors = append(formErrors.Errors, "max records reached") + } + dnsRecord := &database.DNSRecord{ UserID: context.User.ID, Name: name, @@ -80,17 +90,6 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res TTL: ttlNum, Internal: internal, } - - dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) - if err != nil { - log.Println(err) - resp.WriteHeader(http.StatusInternalServerError) - return failure(context, req, resp) - } - if len(dnsRecords) >= MAX_USER_RECORDS { - formErrors.Errors = append(formErrors.Errors, "max records reached") - } - if !userCanFuckWithDNSRecord(context.DBConn, context.User, dnsRecord) { formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username+" or you must be a domain owner for internal domains") } @@ -122,7 +121,6 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res return success(context, req, resp) } - (*context.TemplateData)["DNSRecords"] = dnsRecords (*context.TemplateData)["FormError"] = &formErrors (*context.TemplateData)["RecordForm"] = dnsRecord diff --git a/api/guestbook.go b/api/guestbook.go new file mode 100644 index 0000000..2037e7e --- /dev/null +++ b/api/guestbook.go @@ -0,0 +1,143 @@ +package api + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + + "git.hatecomputers.club/hatecomputers/hatecomputers.club/database" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils" +) + +type HcaptchaArgs struct { + SiteKey string +} + +func validateGuestbookEntry(entry *database.GuestbookEntry) []string { + errors := []string{} + + if entry.Name == "" { + errors = append(errors, "name is required") + } + + if entry.Message == "" { + errors = append(errors, "message is required") + } + + messageLength := len(entry.Message) + if messageLength < 10 || messageLength > 500 { + errors = append(errors, "message must be between 10 and 500 characters") + } + + newLines := strings.Count(entry.Message, "\n") + if newLines > 10 { + errors = append(errors, "message cannot contain more than 10 new lines") + } + + return errors +} + +func SignGuestbookContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { + return func(success Continuation, failure Continuation) ContinuationChain { + name := req.FormValue("name") + message := req.FormValue("message") + hCaptchaResponse := req.FormValue("h-captcha-response") + + formErrors := FormError{ + Errors: []string{}, + } + + if hCaptchaResponse == "" { + formErrors.Errors = append(formErrors.Errors, "hCaptcha is required") + } + + entry := &database.GuestbookEntry{ + ID: utils.RandomId(), + Name: name, + Message: message, + } + + formErrors.Errors = append(formErrors.Errors, validateGuestbookEntry(entry)...) + + if len(formErrors.Errors) > 0 { + (*context.TemplateData)["FormError"] = formErrors + return failure(context, req, resp) + } + + err := verifyHCaptcha(context.Args.HcaptchaSecret, hCaptchaResponse) + if err != nil { + log.Println(err) + + resp.WriteHeader(http.StatusBadRequest) + return failure(context, req, resp) + } + + _, err = database.SaveGuestbookEntry(context.DBConn, entry) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + return success(context, req, resp) + } +} + +func ListGuestbookContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { + return func(success Continuation, failure Continuation) ContinuationChain { + entries, err := database.GetGuestbookEntries(context.DBConn) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + (*context.TemplateData)["GuestbookEntries"] = entries + return success(context, req, resp) + } +} + +func HcaptchaArgsContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { + return func(success Continuation, failure Continuation) ContinuationChain { + (*context.TemplateData)["HcaptchaArgs"] = HcaptchaArgs{ + SiteKey: context.Args.HcaptchaSiteKey, + } + log.Println(context.Args.HcaptchaSiteKey) + return success(context, req, resp) + } +} + +func verifyHCaptcha(secret, response string) error { + verifyURL := "https://hcaptcha.com/siteverify" + body := strings.NewReader("secret=" + secret + "&response=" + response) + + req, err := http.NewRequest("POST", verifyURL, body) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + jsonResponse := struct { + Success bool `json:"success"` + }{} + err = json.NewDecoder(resp.Body).Decode(&jsonResponse) + if err != nil { + return err + } + + if !jsonResponse.Success { + return fmt.Errorf("hcaptcha verification failed") + } + + defer resp.Body.Close() + return nil +} diff --git a/api/serve.go b/api/serve.go index d16ea99..7cef1c9 100644 --- a/api/serve.go +++ b/api/serve.go @@ -118,7 +118,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { mux.HandleFunc("POST /dns", func(w http.ResponseWriter, r *http.Request) { requestContext := makeRequestContext() - LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(CreateDNSRecordContinuation, GoLoginContinuation)(IdContinuation, TemplateContinuation("dns.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(ListDNSRecordsContinuation, GoLoginContinuation)(CreateDNSRecordContinuation, FailurePassingContinuation)(TemplateContinuation("dns.html", true), TemplateContinuation("dns.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) }) mux.HandleFunc("POST /dns/delete", func(w http.ResponseWriter, r *http.Request) { @@ -133,7 +133,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { mux.HandleFunc("POST /keys", func(w http.ResponseWriter, r *http.Request) { requestContext := makeRequestContext() - LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(CreateAPIKeyContinuation, GoLoginContinuation)(IdContinuation, TemplateContinuation("api_keys.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(CreateAPIKeyContinuation, GoLoginContinuation)(ListAPIKeysContinuation, ListAPIKeysContinuation)(TemplateContinuation("api_keys.html", true), TemplateContinuation("api_keys.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) }) mux.HandleFunc("POST /keys/delete", func(w http.ResponseWriter, r *http.Request) { @@ -141,6 +141,16 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(DeleteAPIKeyContinuation, GoLoginContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) }) + mux.HandleFunc("GET /guestbook", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(HcaptchaArgsContinuation, HcaptchaArgsContinuation)(ListGuestbookContinuation, ListGuestbookContinuation)(TemplateContinuation("guestbook.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + + mux.HandleFunc("POST /guestbook", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(HcaptchaArgsContinuation, HcaptchaArgsContinuation)(SignGuestbookContinuation, FailurePassingContinuation)(ListGuestbookContinuation, ListGuestbookContinuation)(TemplateContinuation("guestbook.html", true), TemplateContinuation("guestbook.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) { requestContext := makeRequestContext() name := r.PathValue("name") diff --git a/args/args.go b/args/args.go index 3be0abd..40dd1af 100644 --- a/args/args.go +++ b/args/args.go @@ -10,11 +10,9 @@ import ( ) type Arguments struct { - DatabasePath string - TemplatePath string - StaticPath string - CloudflareToken string - CloudflareZone string + DatabasePath string + TemplatePath string + StaticPath string Migrate bool Scheduler bool @@ -27,6 +25,12 @@ type Arguments struct { Dns bool DnsRecursion []string DnsPort int + + CloudflareToken string + CloudflareZone string + + HcaptchaSecret string + HcaptchaSiteKey string } func GetArgs() (*Arguments, error) { @@ -57,6 +61,9 @@ func GetArgs() (*Arguments, error) { oauthRedirectURI := os.Getenv("OAUTH_REDIRECT_URI") oauthUserInfoURI := os.Getenv("OAUTH_USER_INFO_URI") + hcaptchaSecret := os.Getenv("HCAPTCHA_SECRET") + hcaptchaSiteKey := os.Getenv("HCAPTCHA_SITE_KEY") + envVars := [][]string{ {cloudflareToken, "CLOUDFLARE_TOKEN"}, {cloudflareZone, "CLOUDFLARE_ZONE"}, @@ -102,6 +109,9 @@ func GetArgs() (*Arguments, error) { OauthConfig: oauthConfig, OauthUserInfoURI: oauthUserInfoURI, + + HcaptchaSecret: hcaptchaSecret, + HcaptchaSiteKey: hcaptchaSiteKey, } return arguments, nil diff --git a/database/dns.go b/database/dns.go index 568653d..fc01347 100644 --- a/database/dns.go +++ b/database/dns.go @@ -20,6 +20,18 @@ type DNSRecord struct { CreatedAt time.Time `json:"created_at"` } +func CountUserDNSRecords(db *sql.DB, userID string) (int, error) { + log.Println("counting dns records for user", userID) + + row := db.QueryRow("SELECT COUNT(*) FROM dns_records WHERE user_id = ?", userID) + var count int + err := row.Scan(&count) + if err != nil { + return 0, err + } + return count, nil +} + func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { log.Println("getting dns records for user", userID) @@ -43,7 +55,7 @@ func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { } func SaveDNSRecord(db *sql.DB, record *DNSRecord) (*DNSRecord, error) { - log.Println("saving dns record", record) + log.Println("saving dns record", record.ID) record.CreatedAt = time.Now() _, err := db.Exec("INSERT OR REPLACE INTO dns_records (id, user_id, name, type, content, ttl, internal, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", record.ID, record.UserID, record.Name, record.Type, record.Content, record.TTL, record.Internal, record.CreatedAt) diff --git a/database/guestbook.go b/database/guestbook.go new file mode 100644 index 0000000..2d4d8c9 --- /dev/null +++ b/database/guestbook.go @@ -0,0 +1,50 @@ +package database + +import ( + "database/sql" + "log" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +type GuestbookEntry struct { + ID string `json:"id"` + Name string `json:"name"` + Message string `json:"message"` + CreatedAt time.Time `json:"created_at"` +} + +func GetGuestbookEntries(db *sql.DB) ([]GuestbookEntry, error) { + log.Println("getting guest_book entries") + + rows, err := db.Query("SELECT * FROM guest_book ORDER BY created_at DESC LIMIT 200") + if err != nil { + return nil, err + } + defer rows.Close() + + var entries []GuestbookEntry + for rows.Next() { + var entry GuestbookEntry + err := rows.Scan(&entry.ID, &entry.Name, &entry.Message, &entry.CreatedAt) + if err != nil { + return nil, err + } + entries = append(entries, entry) + } + + return entries, nil +} + +func SaveGuestbookEntry(db *sql.DB, entry *GuestbookEntry) (*GuestbookEntry, error) { + log.Println("saving guest_book entry", entry.ID) + + entry.CreatedAt = time.Now() + _, err := db.Exec("INSERT OR REPLACE INTO guest_book (id, name, message, created_at) VALUES (?, ?, ?, ?)", entry.ID, entry.Name, entry.Message, entry.CreatedAt) + + if err != nil { + return nil, err + } + return entry, nil +} diff --git a/database/migrate.go b/database/migrate.go index de1db4c..1609bc2 100644 --- a/database/migrate.go +++ b/database/migrate.go @@ -102,6 +102,24 @@ func MigrateUserSessions(dbConn *sql.DB) (*sql.DB, error) { return dbConn, nil } +func MigrateGuestBook(dbConn *sql.DB) (*sql.DB, error) { + log.Println("migrating guest_book table") + + _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS guest_book ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + message TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + );`) + if err != nil { + return dbConn, err + } + + _, err = dbConn.Exec(`CREATE INDEX IF NOT EXISTS idx_guest_book_created_at ON guest_book (created_at);`) + + return dbConn, nil +} + func Migrate(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating database") @@ -111,6 +129,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) { MigrateApiKeys, MigrateDomainOwners, MigrateDNSRecords, + MigrateGuestBook, } for _, migration := range migrations { diff --git a/database/users.go b/database/users.go index f9e4436..ab48699 100644 --- a/database/users.go +++ b/database/users.go @@ -33,6 +33,8 @@ type UserSession struct { } func GetUser(dbConn *sql.DB, id string) (*User, error) { + log.Println("getting user", id) + row := dbConn.QueryRow(`SELECT id, mail, username, display_name, created_at FROM users WHERE id = ?;`, id) var user User @@ -46,6 +48,8 @@ func GetUser(dbConn *sql.DB, id string) (*User, error) { } func FindOrSaveUser(dbConn *sql.DB, user *User) (*User, error) { + log.Println("finding or saving user", user.ID) + _, err := dbConn.Exec(`INSERT OR REPLACE INTO users (id, mail, username, display_name) VALUES (?, ?, ?, ?);`, user.ID, user.Mail, user.Username, user.DisplayName) if err != nil { return nil, err @@ -55,6 +59,8 @@ func FindOrSaveUser(dbConn *sql.DB, user *User) (*User, error) { } func MakeUserSessionFor(dbConn *sql.DB, user *User) (*UserSession, error) { + log.Println("making session for user", user.ID) + expireAt := time.Now().Add(time.Hour * 12) _, err := dbConn.Exec(`INSERT OR REPLACE INTO user_sessions (id, user_id, expire_at) VALUES (?, ?, ?);`, user.ID, user.ID, time.Now().Add(ExpiryDuration)) @@ -72,6 +78,8 @@ func MakeUserSessionFor(dbConn *sql.DB, user *User) (*UserSession, error) { } func GetSession(dbConn *sql.DB, sessionId string) (*UserSession, error) { + log.Println("getting session", sessionId) + row := dbConn.QueryRow(`SELECT id, user_id, expire_at FROM user_sessions WHERE id = ?;`, sessionId) var id, userId string @@ -90,6 +98,8 @@ func GetSession(dbConn *sql.DB, sessionId string) (*UserSession, error) { } func DeleteSession(dbConn *sql.DB, sessionId string) error { + log.Println("deleting session", sessionId) + _, err := dbConn.Exec(`DELETE FROM user_sessions WHERE id = ?;`, sessionId) if err != nil { log.Println(err) @@ -126,7 +136,24 @@ func DeleteExpiredSessions(dbConn *sql.DB) error { return nil } +func CountUserAPIKeys(dbConn *sql.DB, userId string) (int, error) { + log.Println("counting api keys for user", userId) + + row := dbConn.QueryRow(`SELECT COUNT(*) FROM api_keys WHERE user_id = ?;`, userId) + + var count int + err := row.Scan(&count) + if err != nil { + log.Println(err) + return 0, err + } + + return count, nil +} + func ListUserAPIKeys(dbConn *sql.DB, userId string) ([]*UserApiKey, error) { + log.Println("listing api keys for user", userId) + rows, err := dbConn.Query(`SELECT key, user_id, created_at FROM api_keys WHERE user_id = ?;`, userId) if err != nil { log.Println(err) @@ -150,6 +177,8 @@ func ListUserAPIKeys(dbConn *sql.DB, userId string) ([]*UserApiKey, error) { } func SaveAPIKey(dbConn *sql.DB, apiKey *UserApiKey) (*UserApiKey, error) { + log.Println("saving api key", apiKey.Key) + _, err := dbConn.Exec(`INSERT OR REPLACE INTO api_keys (key, user_id) VALUES (?, ?);`, apiKey.Key, apiKey.UserID) if err != nil { log.Println(err) @@ -161,6 +190,8 @@ func SaveAPIKey(dbConn *sql.DB, apiKey *UserApiKey) (*UserApiKey, error) { } func GetAPIKey(dbConn *sql.DB, key string) (*UserApiKey, error) { + log.Println("getting api key", key) + row := dbConn.QueryRow(`SELECT key, user_id, created_at FROM api_keys WHERE key = ?;`, key) var apiKey UserApiKey @@ -174,6 +205,8 @@ func GetAPIKey(dbConn *sql.DB, key string) (*UserApiKey, error) { } func DeleteAPIKey(dbConn *sql.DB, key string) error { + log.Println("deleting api key", key) + _, err := dbConn.Exec(`DELETE FROM api_keys WHERE key = ?;`, key) if err != nil { log.Println(err) diff --git a/static/css/form.css b/static/css/form.css index 1378d75..a5dc358 100644 --- a/static/css/form.css +++ b/static/css/form.css @@ -28,3 +28,12 @@ input[type="submit"] { border: 0; cursor: pointer; } + +textarea { + display: block; + width: 100%; + padding: 0.5em; + margin: 0 0 1em; + border: 1px solid var(--border-color); + background: var(--container-bg); +} diff --git a/static/css/guestbook.css b/static/css/guestbook.css new file mode 100644 index 0000000..0fb7a16 --- /dev/null +++ b/static/css/guestbook.css @@ -0,0 +1,15 @@ +.entry { + margin-bottom: 10px; + border: 1px solid var(--border-color); + + padding: 10px; +} + +.entry-name { + font-weight: bold; +} + +.entry-message { + margin-left: 20px; + white-space: pre-wrap; +} diff --git a/static/css/styles.css b/static/css/styles.css index b3babe7..7486016 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -2,6 +2,7 @@ @import "/static/css/blinky.css"; @import "/static/css/table.css"; @import "/static/css/form.css"; +@import "/static/css/guestbook.css"; @font-face { font-family: "ComicSans"; diff --git a/static/js/components/formatDate.js b/static/js/components/formatDate.js new file mode 100644 index 0000000..a12f04f --- /dev/null +++ b/static/js/components/formatDate.js @@ -0,0 +1,7 @@ +const timeElements = document.querySelectorAll(".time"); +timeElements.forEach((timeElement) => { + const dateStr = timeElement.textContent.split(" ").slice(0, 3).join(" "); + const date = new Date(dateStr); + + timeElement.textContent = date.toLocaleString(); +}); diff --git a/static/js/script.js b/static/js/script.js index 459383e..56233e3 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1,2 +1,5 @@ -const scripts = ["/static/js/components/themeSwitcher.js"]; +const scripts = [ + "/static/js/components/themeSwitcher.js", + "/static/js/components/formatDate.js", +]; requirejs(scripts); diff --git a/templates/api_keys.html b/templates/api_keys.html index 0aa3094..cd4d274 100644 --- a/templates/api_keys.html +++ b/templates/api_keys.html @@ -1,23 +1,23 @@ {{ define "content" }} - - - + + + {{ if (eq (len .APIKeys) 0) }} - + {{ end }} {{ range $key := .APIKeys }} - + @@ -25,9 +25,9 @@
KeyCreated AtRevokekey.created at.revoke.
No API Keys Foundno api keys found
{{ $key.Key }}{{ $key.CreatedAt }}{{ $key.CreatedAt }}
- +

-

Add An API Key

+

generate key.


- + {{ if .FormError }} {{ if (len .FormError.Errors) }} {{ range $error := .FormError.Errors }} diff --git a/templates/dns.html b/templates/dns.html index a794789..d16ed89 100644 --- a/templates/dns.html +++ b/templates/dns.html @@ -1,16 +1,17 @@ {{ define "content" }} - - - - - - + + + + + + + {{ if (eq (len .DNSRecords) 0) }} - + {{ end }} {{ range $record := .DNSRecords }} @@ -20,6 +21,7 @@ +
TypeNameContentTTLInternalDeletetype.name.content.ttl.internal.created.delete.
No DNS records foundno dns records found.
{{ $record.Content }} {{ $record.TTL }} {{ $record.Internal }}{{ $record.CreatedAt }} @@ -31,10 +33,10 @@

-

Add DNS Records

+

add dns records.

note that the name must be a subdomain of {{ .User.Username }}


- + - + - + - +