summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsimponic <simponic@hatecomputers.club>2024-03-28 12:57:35 -0400
committersimponic <simponic@hatecomputers.club>2024-03-28 12:57:35 -0400
commitb2fc689bdcff28bf75c0128db19ba4730d726b4f (patch)
tree37c16d95183242516ba667aa5f441539d152c279
parent75ba836d6072235fc7a71659f8630ab3c1b210ad (diff)
downloadhatecomputers.club-b2fc689bdcff28bf75c0128db19ba4730d726b4f.tar.gz
hatecomputers.club-b2fc689bdcff28bf75c0128db19ba4730d726b4f.zip
dns api (#1)
Co-authored-by: Elizabeth Hunt <elizabeth.hunt@simponic.xyz> Reviewed-on: https://git.hatecomputers.club/hatecomputers/hatecomputers.club/pulls/1
-rw-r--r--adapters/cloudflare/cloudflare.go71
-rw-r--r--api/api_keys.go84
-rw-r--r--api/auth.go74
-rw-r--r--api/dns.go114
-rw-r--r--api/serve.go29
-rw-r--r--database/dns.go48
-rw-r--r--database/users.go63
-rw-r--r--static/css/colors.css9
-rw-r--r--static/css/form.css2
-rw-r--r--static/css/table.css7
-rw-r--r--static/img/blinkies/capitalism.gifbin0 -> 20828 bytes
-rw-r--r--templates/api_keys.html40
-rw-r--r--templates/base.html4
-rw-r--r--templates/dns.html52
-rw-r--r--utils/RandomId.go9
15 files changed, 565 insertions, 41 deletions
diff --git a/adapters/cloudflare/cloudflare.go b/adapters/cloudflare/cloudflare.go
new file mode 100644
index 0000000..40b04a5
--- /dev/null
+++ b/adapters/cloudflare/cloudflare.go
@@ -0,0 +1,71 @@
+package cloudflare
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
+)
+
+type CloudflareDNSResponse struct {
+ Result database.DNSRecord `json:"result"`
+}
+
+func CreateDNSRecord(zoneId string, apiToken string, record *database.DNSRecord) (string, error) {
+ url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneId)
+
+ reqBody := fmt.Sprintf(`{"type":"%s","name":"%s","content":"%s","ttl":%d,"proxied":false}`, record.Type, record.Name, record.Content, record.TTL)
+ payload := strings.NewReader(reqBody)
+
+ req, _ := http.NewRequest("POST", url, payload)
+
+ req.Header.Add("Authorization", "Bearer "+apiToken)
+ req.Header.Add("Content-Type", "application/json")
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", err
+ }
+
+ defer res.Body.Close()
+ body, _ := io.ReadAll(res.Body)
+
+ if res.StatusCode != 200 {
+ return "", fmt.Errorf("error creating dns record: %s", body)
+ }
+
+ var response CloudflareDNSResponse
+ err = json.Unmarshal(body, &response)
+ if err != nil {
+ return "", err
+ }
+
+ result := &response.Result
+
+ return result.ID, nil
+}
+
+func DeleteDNSRecord(zoneId string, apiToken string, id string) error {
+ url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneId, id)
+
+ req, _ := http.NewRequest("DELETE", url, nil)
+
+ req.Header.Add("Authorization", "Bearer "+apiToken)
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ defer res.Body.Close()
+ body, _ := io.ReadAll(res.Body)
+
+ if res.StatusCode != 200 {
+ return fmt.Errorf("error deleting dns record: %s", body)
+ }
+
+ return nil
+}
diff --git a/api/api_keys.go b/api/api_keys.go
new file mode 100644
index 0000000..17ed6c9
--- /dev/null
+++ b/api/api_keys.go
@@ -0,0 +1,84 @@
+package api
+
+import (
+ "log"
+ "net/http"
+
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
+)
+
+const MAX_USER_API_KEYS = 5
+
+func ListAPIKeysContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ apiKeys, err := database.ListUserAPIKeys(context.DBConn, context.User.ID)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ (*context.TemplateData)["APIKeys"] = apiKeys
+ return success(context, req, resp)
+ }
+}
+
+func CreateAPIKeyContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ formErrors := FormError{
+ Errors: []string{},
+ }
+
+ apiKeys, err := database.ListUserAPIKeys(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 {
+ formErrors.Errors = append(formErrors.Errors, "max api keys reached")
+ }
+
+ _, err = database.SaveAPIKey(context.DBConn, &database.UserApiKey{
+ UserID: context.User.ID,
+ Key: utils.RandomId(),
+ })
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ http.Redirect(resp, req, "/keys", http.StatusFound)
+ return success(context, req, resp)
+ }
+}
+
+func DeleteAPIKeyContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ key := req.FormValue("key")
+
+ apiKey, err := database.GetAPIKey(context.DBConn, key)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+ if (apiKey == nil) || (apiKey.UserID != context.User.ID) {
+ resp.WriteHeader(http.StatusUnauthorized)
+ return failure(context, req, resp)
+ }
+
+ err = database.DeleteAPIKey(context.DBConn, key)
+ if err != nil {
+ log.Println(err)
+ 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/auth.go b/api/auth.go
index 4733971..dcddf5a 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/base64"
"encoding/json"
+ "fmt"
"io"
"log"
"net/http"
@@ -116,32 +117,69 @@ func InterceptCodeContinuation(context *RequestContext, req *http.Request, resp
}
}
+func getUserFromAuthHeader(dbConn *sql.DB, bearerToken string) (*database.User, error) {
+ if bearerToken == "" {
+ return nil, nil
+ }
+
+ parts := strings.Split(bearerToken, " ")
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return nil, nil
+ }
+
+ apiKey, err := database.GetAPIKey(dbConn, parts[1])
+ if err != nil {
+ return nil, err
+ }
+ if apiKey == nil {
+ return nil, nil
+ }
+
+ user, err := database.GetUser(dbConn, apiKey.UserID)
+ if err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+func getUserFromSession(dbConn *sql.DB, sessionId string) (*database.User, error) {
+ session, err := database.GetSession(dbConn, sessionId)
+ if err != nil {
+ return nil, err
+ }
+
+ if session.ExpireAt.Before(time.Now()) {
+ session = nil
+ database.DeleteSession(dbConn, sessionId)
+ return nil, fmt.Errorf("session expired")
+ }
+
+ user, err := database.GetUser(dbConn, session.UserID)
+ if err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
func VerifySessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
return func(success Continuation, failure Continuation) ContinuationChain {
+ authHeader := req.Header.Get("Authorization")
+ user, userErr := getUserFromAuthHeader(context.DBConn, authHeader)
+
sessionCookie, err := req.Cookie("session")
- if err != nil {
- resp.WriteHeader(http.StatusUnauthorized)
- return failure(context, req, resp)
+ if err == nil {
+ user, userErr = getUserFromSession(context.DBConn, sessionCookie.Value)
}
- session, err := database.GetSession(context.DBConn, sessionCookie.Value)
- if err == nil && session.ExpireAt.Before(time.Now()) {
- session = nil
- database.DeleteSession(context.DBConn, sessionCookie.Value)
- }
- if err != nil || session == nil {
+ if userErr != nil || user == nil {
+ log.Println(userErr, user)
+
http.SetCookie(resp, &http.Cookie{
Name: "session",
- MaxAge: 0,
+ MaxAge: 0, // reset session cookie in case
})
-
- return failure(context, req, resp)
- }
-
- user, err := database.GetUser(context.DBConn, session.UserID)
- if err != nil {
- log.Println(err)
- resp.WriteHeader(http.StatusUnauthorized)
return failure(context, req, resp)
}
diff --git a/api/dns.go b/api/dns.go
index 3105f91..5123acc 100644
--- a/api/dns.go
+++ b/api/dns.go
@@ -3,10 +3,23 @@ package api
import (
"log"
"net/http"
+ "strconv"
+ "strings"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
)
+const MAX_USER_RECORDS = 20
+
+type FormError struct {
+ Errors []string
+}
+
+func userCanFuckWithDNSRecord(user *database.User, record *database.DNSRecord) bool {
+ return user.ID == record.UserID && (record.Name == user.Username || strings.HasSuffix(record.Name, "."+user.Username))
+}
+
func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
return func(success Continuation, failure Continuation) ContinuationChain {
dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID)
@@ -17,7 +30,108 @@ func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp
}
(*context.TemplateData)["DNSRecords"] = dnsRecords
+ return success(context, req, resp)
+ }
+}
+
+func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ formErrors := FormError{
+ Errors: []string{},
+ }
+
+ name := req.FormValue("name")
+ recordType := req.FormValue("type")
+ recordContent := req.FormValue("content")
+ ttl := req.FormValue("ttl")
+ ttlNum, err := strconv.Atoi(ttl)
+ if err != nil {
+ formErrors.Errors = append(formErrors.Errors, "invalid ttl")
+ }
+
+ dnsRecord := &database.DNSRecord{
+ UserID: context.User.ID,
+ Name: name,
+ Type: recordType,
+ Content: recordContent,
+ TTL: ttlNum,
+ }
+
+ 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.User, dnsRecord) {
+ formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username)
+ }
+
+ if len(formErrors.Errors) == 0 {
+ cloudflareRecordId, err := cloudflare.CreateDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, dnsRecord)
+ if err != nil {
+ log.Println(err)
+ formErrors.Errors = append(formErrors.Errors, err.Error())
+ }
+
+ dnsRecord.ID = cloudflareRecordId
+ }
+
+ if len(formErrors.Errors) == 0 {
+ _, err := database.SaveDNSRecord(context.DBConn, dnsRecord)
+ if err != nil {
+ log.Println(err)
+ formErrors.Errors = append(formErrors.Errors, "error saving record")
+ }
+ }
+
+ if len(formErrors.Errors) == 0 {
+ http.Redirect(resp, req, "/dns", http.StatusFound)
+ return success(context, req, resp)
+ }
+
+ (*context.TemplateData)["DNSRecords"] = dnsRecords
+ (*context.TemplateData)["FormError"] = &formErrors
+ (*context.TemplateData)["RecordForm"] = dnsRecord
+
+ resp.WriteHeader(http.StatusBadRequest)
+ return failure(context, req, resp)
+ }
+}
+
+func DeleteDNSRecordContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ recordId := req.FormValue("id")
+ record, err := database.GetDNSRecord(context.DBConn, recordId)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ if !userCanFuckWithDNSRecord(context.User, record) {
+ resp.WriteHeader(http.StatusUnauthorized)
+ return failure(context, req, resp)
+ }
+
+ err = cloudflare.DeleteDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, recordId)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ err = database.DeleteDNSRecord(context.DBConn, recordId)
+ if err != nil {
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+ http.Redirect(resp, req, "/dns", http.StatusFound)
return success(context, req, resp)
}
}
diff --git a/api/serve.go b/api/serve.go
index 38b65b2..d16ea99 100644
--- a/api/serve.go
+++ b/api/serve.go
@@ -70,7 +70,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
mux := http.NewServeMux()
fileServer := http.FileServer(http.Dir(argv.StaticPath))
- mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
+ mux.Handle("GET /static/", http.StripPrefix("/static/", fileServer))
makeRequestContext := func() *RequestContext {
return &RequestContext{
@@ -81,7 +81,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
}
}
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
@@ -116,6 +116,31 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(ListDNSRecordsContinuation, GoLoginContinuation)(TemplateContinuation("dns.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
+ 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)
+ })
+
+ mux.HandleFunc("POST /dns/delete", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(DeleteDNSRecordContinuation, GoLoginContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("GET /keys", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(ListAPIKeysContinuation, GoLoginContinuation)(TemplateContinuation("api_keys.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ 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)
+ })
+
+ mux.HandleFunc("POST /keys/delete", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(DeleteAPIKeyContinuation, GoLoginContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
name := r.PathValue("name")
diff --git a/database/dns.go b/database/dns.go
index 17487b7..bb5c1ef 100644
--- a/database/dns.go
+++ b/database/dns.go
@@ -8,13 +8,13 @@ import (
)
type DNSRecord struct {
- ID string
- UserID string
- Name string
- Type string
- Content string
- TTL int
- CreatedAt time.Time
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Content string `json:"content"`
+ TTL int `json:"ttl"`
+ CreatedAt time.Time `json:"created_at"`
}
func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) {
@@ -38,3 +38,37 @@ func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) {
return records, nil
}
+
+func SaveDNSRecord(db *sql.DB, record *DNSRecord) (*DNSRecord, error) {
+ log.Println("saving dns record", record)
+
+ record.CreatedAt = time.Now()
+ _, err := db.Exec("INSERT OR REPLACE INTO dns_records (id, user_id, name, type, content, ttl, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", record.ID, record.UserID, record.Name, record.Type, record.Content, record.TTL, record.CreatedAt)
+
+ if err != nil {
+ return nil, err
+ }
+ return record, nil
+}
+
+func GetDNSRecord(db *sql.DB, recordID string) (*DNSRecord, error) {
+ log.Println("getting dns record", recordID)
+
+ row := db.QueryRow("SELECT * FROM dns_records WHERE id = ?", recordID)
+ var record DNSRecord
+ err := row.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+ return &record, nil
+}
+
+func DeleteDNSRecord(db *sql.DB, recordID string) error {
+ log.Println("deleting dns record", recordID)
+
+ _, err := db.Exec("DELETE FROM dns_records WHERE id = ?", recordID)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/database/users.go b/database/users.go
index d2b4f20..f9e4436 100644
--- a/database/users.go
+++ b/database/users.go
@@ -12,6 +12,12 @@ const (
ExpiryDuration = time.Hour * 24
)
+type UserApiKey struct {
+ Key string `json:"key"`
+ UserID string `json:"user_id"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
type User struct {
ID string `json:"sub"`
Mail string `json:"email"`
@@ -119,3 +125,60 @@ func DeleteExpiredSessions(dbConn *sql.DB) error {
}
return nil
}
+
+func ListUserAPIKeys(dbConn *sql.DB, userId string) ([]*UserApiKey, error) {
+ rows, err := dbConn.Query(`SELECT key, user_id, created_at FROM api_keys WHERE user_id = ?;`, userId)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+ defer rows.Close()
+
+ var apiKeys []*UserApiKey
+ for rows.Next() {
+ var apiKey UserApiKey
+ err := rows.Scan(&apiKey.Key, &apiKey.UserID, &apiKey.CreatedAt)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ apiKeys = append(apiKeys, &apiKey)
+ }
+
+ return apiKeys, nil
+}
+
+func SaveAPIKey(dbConn *sql.DB, apiKey *UserApiKey) (*UserApiKey, error) {
+ _, err := dbConn.Exec(`INSERT OR REPLACE INTO api_keys (key, user_id) VALUES (?, ?);`, apiKey.Key, apiKey.UserID)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ apiKey.CreatedAt = time.Now()
+ return apiKey, nil
+}
+
+func GetAPIKey(dbConn *sql.DB, key string) (*UserApiKey, error) {
+ row := dbConn.QueryRow(`SELECT key, user_id, created_at FROM api_keys WHERE key = ?;`, key)
+
+ var apiKey UserApiKey
+ err := row.Scan(&apiKey.Key, &apiKey.UserID, &apiKey.CreatedAt)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ return &apiKey, nil
+}
+
+func DeleteAPIKey(dbConn *sql.DB, key string) error {
+ _, err := dbConn.Exec(`DELETE FROM api_keys WHERE key = ?;`, key)
+ if err != nil {
+ log.Println(err)
+ return err
+ }
+
+ return nil
+}
diff --git a/static/css/colors.css b/static/css/colors.css
index 69e3e4b..c68bf8e 100644
--- a/static/css/colors.css
+++ b/static/css/colors.css
@@ -5,6 +5,7 @@
--link-color-light: #d291bc;
--container-bg-light: #fff7f87a;
--border-color-light: #692fcc;
+ --error-color-light: #a83254;
--background-color-dark: #333;
--background-color-dark-2: #2c2c2c;
@@ -12,6 +13,7 @@
--link-color-dark: #b86b77;
--container-bg-dark: #424242ea;
--border-color-dark: #956ade;
+ --error-color-dark: #851736;
}
[data-theme="DARK"] {
@@ -21,6 +23,7 @@
--link-color: var(--link-color-dark);
--container-bg: var(--container-bg-dark);
--border-color: var(--border-color-dark);
+ --error-color: var(--error-color-dark);
}
[data-theme="LIGHT"] {
@@ -30,4 +33,10 @@
--link-color: var(--link-color-light);
--container-bg: var(--container-bg-light);
--border-color: var(--border-color-light);
+ --error-color: var(--error-color-light);
+}
+
+.error {
+ background-color: var(--error-color);
+ padding: 1rem;
}
diff --git a/static/css/form.css b/static/css/form.css
index 4e14b68..1378d75 100644
--- a/static/css/form.css
+++ b/static/css/form.css
@@ -1,4 +1,4 @@
-form {
+.form {
max-width: 600px;
padding: 1em;
background: var(--background-color-2);
diff --git a/static/css/table.css b/static/css/table.css
index 640ad83..75a961d 100644
--- a/static/css/table.css
+++ b/static/css/table.css
@@ -11,8 +11,13 @@ td {
border-bottom: 1px solid var(--border-color);
}
+th,
+thead {
+ background-color: var(--background-color-2);
+}
+
tbody tr:nth-child(odd) {
- background-color: var(--link-color);
+ background-color: var(--background-color);
color: var(--text-color);
}
diff --git a/static/img/blinkies/capitalism.gif b/static/img/blinkies/capitalism.gif
new file mode 100644
index 0000000..b851831
--- /dev/null
+++ b/static/img/blinkies/capitalism.gif
Binary files differ
diff --git a/templates/api_keys.html b/templates/api_keys.html
new file mode 100644
index 0000000..93eebd5
--- /dev/null
+++ b/templates/api_keys.html
@@ -0,0 +1,40 @@
+{{ define "content" }}
+ <table>
+ <tr>
+ <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>
+ </tr>
+ {{ end }}
+ {{ range $key := .APIKeys }}
+ <tr>
+ <td>{{ $key.Key }}</td>
+ <td>{{ $key.CreatedAt }}</td>
+ <td>
+ <form method="POST" action="/keys/delete">
+ <input type="hidden" name="key" value="{{ $key.Key }}" />
+ <input type="submit" value="Revoke" />
+ </form>
+ </td>
+ </tr>
+ {{ end }}
+ </table>
+ <br>
+ <form method="POST" action="/keys" class="form">
+ <h2>Add An API Key</h2>
+ <hr>
+ <input type="submit" value="Generate" />
+ </form>
+
+ {{ if .FormError }}
+ {{ if (len .FormError.Errors) }}
+ {{ range $error := .FormError.Errors }}
+ <div class="error">{{ $error }}</div>
+ {{ end }}
+ {{ end }}
+ {{ end }}
+{{ end }}
diff --git a/templates/base.html b/templates/base.html
index 79e0d12..9f5a903 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -35,7 +35,10 @@
{{ if .User }}
<a href="/dns">dns.</a>
<span> | </span>
+ <a href="/keys">api keys.</a>
+ <span> | </span>
<a href="/logout">logout, {{ .User.DisplayName }}.</a>
+
{{ else }}
<a href="/login">login.</a>
{{ end }}
@@ -74,6 +77,7 @@
<img width='150' height='20' src='/static/img/blinkies/connection.gif'>
<img width='150' height='20' src='/static/img/blinkies/eepy.gif'>
<img width='150' height='20' src='/static/img/blinkies/loveuguys.gif'>
+ <img width='150' height='20' src='/static/img/blinkies/capitalism.gif'>
</div>
</div>
</div>
diff --git a/templates/dns.html b/templates/dns.html
index 0a40cab..e317d05 100644
--- a/templates/dns.html
+++ b/templates/dns.html
@@ -5,10 +5,11 @@
<th>Name</th>
<th>Content</th>
<th>TTL</th>
+ <th>Delete</th>
</tr>
{{ if (eq (len .DNSRecords) 0) }}
<tr>
- <td colspan="4"><span class="blinky">No DNS records found</span></td>
+ <td colspan="5"><span class="blinky">No DNS records found</span></td>
</tr>
{{ end }}
{{ range $record := .DNSRecords }}
@@ -17,21 +18,60 @@
<td>{{ $record.Name }}</td>
<td>{{ $record.Content }}</td>
<td>{{ $record.TTL }}</td>
+ <td>
+ <form method="POST" action="/dns/delete">
+ <input type="hidden" name="id" value="{{ $record.ID }}" />
+ <input type="submit" value="Delete" />
+ </form>
+ </td>
</tr>
{{ end }}
</table>
<br>
- <form method="POST" action="/dns">
+ <form method="POST" action="/dns" class="form">
<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>
- <input type="text" name="type" placeholder="CNAME" required />
+ <input type="text" name="type" placeholder="CNAME"
+ {{ if not .RecordForm }}
+ placeholder="CNAME"
+ {{ else }}
+ value="{{ .RecordForm.Type }}"
+ {{ end }}
+ required />
<label for="name">Name</label>
- <input type="text" name="name" placeholder="{{ .User.Username }}" required />
+ <input type="text" name="name"
+ {{ if not .RecordForm }}
+ placeholder="{{ .User.Username }} || endpoint.{{ .User.Username }}..."
+ {{ else }}
+ value="{{ .RecordForm.Name }}"
+ {{ end }}
+ required/>
<label for="content">Content</label>
- <input type="text" name="content" placeholder="{{ .User.Username }}.dev" required />
+ <input type="text" name="content"
+ {{ if not .RecordForm }}
+ placeholder="{{ .User.Username }}.dev"
+ {{ else }}
+ value="{{ .RecordForm.Content }}"
+ {{ end }}
+ required />
<label for="ttl">TTL</label>
- <input type="text" name="ttl" placeholder="43200" required />
+ <input type="text" name="ttl"
+ {{ if not .RecordForm }}
+ placeholder="43200"
+ {{ else }}
+ value="{{ .RecordForm.TTL }}"
+ {{ end }}
+ required />
<input type="submit" value="Add" />
</form>
+
+ {{ if .FormError }}
+ {{ if (len .FormError.Errors) }}
+ {{ range $error := .FormError.Errors }}
+ <div class="error">{{ $error }}</div>
+ {{ end }}
+ {{ end }}
+ {{ end }}
{{ end }}
diff --git a/utils/RandomId.go b/utils/RandomId.go
index 09f089d..1b03ec8 100644
--- a/utils/RandomId.go
+++ b/utils/RandomId.go
@@ -6,14 +6,11 @@ import (
)
func RandomId() string {
- uuid := make([]byte, 16)
- _, err := rand.Read(uuid)
+ id := make([]byte, 16)
+ _, err := rand.Read(id)
if err != nil {
panic(err)
}
- uuid[8] = uuid[8]&^0xc0 | 0x80
- uuid[6] = uuid[6]&^0xf0 | 0x40
-
- return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
+ return fmt.Sprintf("%x", id)
}