dns api #1

Merged
simponic merged 3 commits from dns_api into main 2024-03-28 12:57:36 -04:00
8 changed files with 299 additions and 18 deletions
Showing only changes of commit 243bb8e35b - Show all commits

View File

@ -0,0 +1,73 @@
package cloudflare
import (
"encoding/json"
"fmt"
"io"
"log"
"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)
log.Println(reqBody)
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
}

View File

@ -3,10 +3,21 @@ package api
import ( import (
"log" "log"
"net/http" "net/http"
"strconv"
"strings"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database" "git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
) )
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 { func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
return func(success Continuation, failure Continuation) ContinuationChain { return func(success Continuation, failure Continuation) ContinuationChain {
dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID)
@ -17,7 +28,106 @@ func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp
} }
(*context.TemplateData)["DNSRecords"] = dnsRecords (*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)
dnsRecord := &database.DNSRecord{
UserID: context.User.ID,
Name: name,
Type: recordType,
Content: recordContent,
TTL: ttlNum,
}
if err != nil {
formErrors.Errors = append(formErrors.Errors, "invalid ttl")
}
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)
}
dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID)
if err != nil {
log.Println(err)
resp.WriteHeader(http.StatusInternalServerError)
return failure(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) return success(context, req, resp)
} }
} }

View File

@ -70,7 +70,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
mux := http.NewServeMux() mux := http.NewServeMux()
fileServer := http.FileServer(http.Dir(argv.StaticPath)) 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 { makeRequestContext := func() *RequestContext {
return &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() requestContext := makeRequestContext()
LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
}) })
@ -116,6 +116,16 @@ 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) 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 /{name}", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext() requestContext := makeRequestContext()
name := r.PathValue("name") name := r.PathValue("name")

View File

@ -8,13 +8,13 @@ import (
) )
type DNSRecord struct { type DNSRecord struct {
ID string ID string `json:"id"`
UserID string UserID string `json:"user_id"`
Name string Name string `json:"name"`
Type string Type string `json:"type"`
Content string Content string `json:"content"`
TTL int TTL int `json:"ttl"`
CreatedAt time.Time CreatedAt time.Time `json:"created_at"`
} }
func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { 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 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
}

View File

@ -5,6 +5,7 @@
--link-color-light: #d291bc; --link-color-light: #d291bc;
--container-bg-light: #fff7f87a; --container-bg-light: #fff7f87a;
--border-color-light: #692fcc; --border-color-light: #692fcc;
--error-color-light: #a83254;
--background-color-dark: #333; --background-color-dark: #333;
--background-color-dark-2: #2c2c2c; --background-color-dark-2: #2c2c2c;
@ -12,6 +13,7 @@
--link-color-dark: #b86b77; --link-color-dark: #b86b77;
--container-bg-dark: #424242ea; --container-bg-dark: #424242ea;
--border-color-dark: #956ade; --border-color-dark: #956ade;
--error-color-dark: #851736;
} }
[data-theme="DARK"] { [data-theme="DARK"] {
@ -21,6 +23,7 @@
--link-color: var(--link-color-dark); --link-color: var(--link-color-dark);
--container-bg: var(--container-bg-dark); --container-bg: var(--container-bg-dark);
--border-color: var(--border-color-dark); --border-color: var(--border-color-dark);
--error-color: var(--error-color-dark);
} }
[data-theme="LIGHT"] { [data-theme="LIGHT"] {
@ -30,4 +33,10 @@
--link-color: var(--link-color-light); --link-color: var(--link-color-light);
--container-bg: var(--container-bg-light); --container-bg: var(--container-bg-light);
--border-color: var(--border-color-light); --border-color: var(--border-color-light);
--error-color: var(--error-color-light);
}
.error {
background-color: var(--error-color);
padding: 1rem;
} }

View File

@ -1,4 +1,4 @@
form { .form {
max-width: 600px; max-width: 600px;
padding: 1em; padding: 1em;
background: var(--background-color-2); background: var(--background-color-2);

View File

@ -11,8 +11,13 @@ td {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
th,
thead {
background-color: var(--background-color-2);
}
tbody tr:nth-child(odd) { tbody tr:nth-child(odd) {
background-color: var(--link-color); background-color: var(--background-color);
color: var(--text-color); color: var(--text-color);
} }

View File

@ -5,10 +5,11 @@
<th>Name</th> <th>Name</th>
<th>Content</th> <th>Content</th>
<th>TTL</th> <th>TTL</th>
<th>Delete</th>
</tr> </tr>
{{ if (eq (len .DNSRecords) 0) }} {{ if (eq (len .DNSRecords) 0) }}
<tr> <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> </tr>
{{ end }} {{ end }}
{{ range $record := .DNSRecords }} {{ range $record := .DNSRecords }}
@ -17,21 +18,60 @@
<td>{{ $record.Name }}</td> <td>{{ $record.Name }}</td>
<td>{{ $record.Content }}</td> <td>{{ $record.Content }}</td>
<td>{{ $record.TTL }}</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> </tr>
{{ end }} {{ end }}
</table> </table>
<br> <br>
<form method="POST" action="/dns"> <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> <hr>
<label for="type">Type</label> <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> <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> <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> <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" /> <input type="submit" value="Add" />
</form> </form>
{{ if .FormError }}
{{ if (len .FormError.Errors) }}
{{ range $error := .FormError.Errors }}
<div class="error">{{ $error }}</div>
{{ end }}
{{ end }}
{{ end }}
{{ end }} {{ end }}