summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/api_keys.go11
-rw-r--r--api/dns.go22
-rw-r--r--api/guestbook.go143
-rw-r--r--api/serve.go14
-rw-r--r--args/args.go20
-rw-r--r--database/dns.go14
-rw-r--r--database/guestbook.go50
-rw-r--r--database/migrate.go19
-rw-r--r--database/users.go33
-rw-r--r--static/css/form.css9
-rw-r--r--static/css/guestbook.css15
-rw-r--r--static/css/styles.css1
-rw-r--r--static/js/components/formatDate.js7
-rw-r--r--static/js/script.js5
-rw-r--r--templates/api_keys.html16
-rw-r--r--templates/dns.html28
-rw-r--r--templates/guestbook.html57
17 files changed, 417 insertions, 47 deletions
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,25 +72,24 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
formErrors.Errors = append(formErrors.Errors, "invalid ttl")
}
- dnsRecord := &database.DNSRecord{
- UserID: context.User.ID,
- Name: name,
- Type: recordType,
- Content: recordContent,
- TTL: ttlNum,
- Internal: internal,
- }
-
- dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID)
+ 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 len(dnsRecords) >= MAX_USER_RECORDS {
+ if dnsRecordCount >= MAX_USER_RECORDS {
formErrors.Errors = append(formErrors.Errors, "max records reached")
}
+ dnsRecord := &database.DNSRecord{
+ UserID: context.User.ID,
+ Name: name,
+ Type: recordType,
+ Content: recordContent,
+ TTL: ttlNum,
+ Internal: internal,
+ }
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" }}
<table>
<tr>
- <th>Key</th>
- <th>Created At</th>
- <th>Revoke</th>
+ <th>key.</th>
+ <th>created at.</th>
+ <th>revoke.</th>
</tr>
{{ if (eq (len .APIKeys) 0) }}
<tr>
- <td colspan="5"><span class="blinky">No API Keys Found</span></td>
+ <td colspan="5"><span class="blinky">no api keys found</span></td>
</tr>
{{ end }}
{{ range $key := .APIKeys }}
<tr>
<td>{{ $key.Key }}</td>
- <td>{{ $key.CreatedAt }}</td>
+ <td class="time">{{ $key.CreatedAt }}</td>
<td>
<form method="POST" action="/keys/delete">
<input type="hidden" name="key" value="{{ $key.Key }}" />
- <input type="submit" value="Revoke" />
+ <input type="submit" value="revoke." />
</form>
</td>
</tr>
@@ -25,9 +25,9 @@
</table>
<br>
<form method="POST" action="/keys" class="form">
- <h2>Add An API Key</h2>
+ <h2>generate key.</h2>
<hr>
- <input type="submit" value="Generate" />
+ <input type="submit" value="generate." />
{{ 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" }}
<table>
<tr>
- <th>Type</th>
- <th>Name</th>
- <th>Content</th>
- <th>TTL</th>
- <th>Internal</th>
- <th>Delete</th>
+ <th>type.</th>
+ <th>name.</th>
+ <th>content.</th>
+ <th>ttl.</th>
+ <th>internal.</th>
+ <th>created.</th>
+ <th>delete.</th>
</tr>
{{ if (eq (len .DNSRecords) 0) }}
<tr>
- <td colspan="6"><span class="blinky">No DNS records found</span></td>
+ <td colspan="7"><span class="blinky">no dns records found.</span></td>
</tr>
{{ end }}
{{ range $record := .DNSRecords }}
@@ -20,6 +21,7 @@
<td>{{ $record.Content }}</td>
<td>{{ $record.TTL }}</td>
<td>{{ $record.Internal }}</td>
+ <td class="time">{{ $record.CreatedAt }}</td>
<td>
<form method="POST" action="/dns/delete">
<input type="hidden" name="id" value="{{ $record.ID }}" />
@@ -31,10 +33,10 @@
</table>
<br>
<form method="POST" action="/dns" class="form">
- <h2>Add DNS Records</h2>
+ <h2>add dns records.</h2>
<p>note that the name <em>must</em> be a subdomain of <em>{{ .User.Username }}</em></p>
<hr>
- <label for="type">Type</label>
+ <label for="type">type.</label>
<input type="text" name="type" placeholder="CNAME"
{{ if not .RecordForm }}
placeholder="CNAME"
@@ -42,7 +44,7 @@
value="{{ .RecordForm.Type }}"
{{ end }}
required />
- <label for="name">Name</label>
+ <label for="name">name.</label>
<input type="text" name="name"
{{ if not .RecordForm }}
placeholder="{{ .User.Username }} || endpoint.{{ .User.Username }}..."
@@ -50,7 +52,7 @@
value="{{ .RecordForm.Name }}"
{{ end }}
required/>
- <label for="content">Content</label>
+ <label for="content">content.</label>
<input type="text" name="content"
{{ if not .RecordForm }}
placeholder="{{ .User.Username }}.dev"
@@ -58,7 +60,7 @@
value="{{ .RecordForm.Content }}"
{{ end }}
required />
- <label for="ttl">TTL</label>
+ <label for="ttl">ttl.</label>
<input type="text" name="ttl"
{{ if not .RecordForm }}
placeholder="43200"
@@ -67,7 +69,7 @@
{{ end }}
required />
<label for="internal">
- Internal
+ internal.
<input style='display:inline;width:auto;' type="checkbox" name="internal"
{{ if .RecordForm.Internal }}
checked
diff --git a/templates/guestbook.html b/templates/guestbook.html
index 859daaf..f304666 100644
--- a/templates/guestbook.html
+++ b/templates/guestbook.html
@@ -1,3 +1,58 @@
{{ define "content" }}
- <h1>guestbook</h1>
+
+<form action="/guestbook" method="post" class="form">
+ <h1>sign the guestboooook!!!!</h1>
+ <p>leave a message for the world to see (keep it civil plz and no spam >:3).</p>
+ <hr>
+ <label for="name">name.</label>
+ <input type="text" id="name" name="name" class="form-control"
+ {{ if not .EntryForm }}
+ placeholder="john doe"
+ {{ else }}
+ value="{{ .EntryForm.Name }}"
+ {{ end }}
+ required>
+ <label for="message">message.</label>
+ <textarea id="message" name="message" class="form-control"
+ {{ if not .EntryForm }}
+ placeholder="hoi!"
+ {{ end }}
+ >{{ if .EntryForm }}{{ .EntryForm.Message }}{{ end }}
+ </textarea>
+
+ <div
+ class="h-captcha"
+ data-sitekey="{{ .HcaptchaArgs.SiteKey }}"
+ data-theme="dark"
+ ></div>
+ <br>
+ <button type="submit" class="btn btn-primary">sign.</button>
+ <br>
+ {{ if .FormError }}
+ {{ if (len .FormError.Errors) }}
+ {{ range $error := .FormError.Errors }}
+ <div class="error">{{ $error }}</div>
+ {{ end }}
+ {{ end }}
+ {{ end }}
+</form>
+
+<hr>
+
+{{ if (eq (len .GuestbookEntries) 0) }}
+<div class="blinky">no entries yet.</div>
+{{ else }}
+ <h1>peeps that said hi.</h1>
+ <br>
+ {{ range .GuestbookEntries }}
+ <div class="entry">
+ <div>name: <span class="entry-name">{{ .Name }}</span></div>
+ <div class="time">{{ .CreatedAt }}</div>
+ <div class="entry-message">{{ .Message }}</div>
+ </div>
+ {{ end }}
+{{ end }}
+
+<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
+
{{ end }}