Co-authored-by: Elizabeth Hunt <elizabeth.hunt@simponic.xyz> Reviewed-on: #1
This commit is contained in:
parent
75ba836d60
commit
b2fc689bdc
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
74
api/auth.go
74
api/auth.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"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 {
|
func VerifySessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
||||||
return func(success Continuation, failure Continuation) ContinuationChain {
|
return func(success Continuation, failure Continuation) ContinuationChain {
|
||||||
|
authHeader := req.Header.Get("Authorization")
|
||||||
|
user, userErr := getUserFromAuthHeader(context.DBConn, authHeader)
|
||||||
|
|
||||||
sessionCookie, err := req.Cookie("session")
|
sessionCookie, err := req.Cookie("session")
|
||||||
if err != nil {
|
if err == nil {
|
||||||
resp.WriteHeader(http.StatusUnauthorized)
|
user, userErr = getUserFromSession(context.DBConn, sessionCookie.Value)
|
||||||
return failure(context, req, resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := database.GetSession(context.DBConn, sessionCookie.Value)
|
if userErr != nil || user == nil {
|
||||||
if err == nil && session.ExpireAt.Before(time.Now()) {
|
log.Println(userErr, user)
|
||||||
session = nil
|
|
||||||
database.DeleteSession(context.DBConn, sessionCookie.Value)
|
|
||||||
}
|
|
||||||
if err != nil || session == nil {
|
|
||||||
http.SetCookie(resp, &http.Cookie{
|
http.SetCookie(resp, &http.Cookie{
|
||||||
Name: "session",
|
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)
|
return failure(context, req, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
116
api/dns.go
116
api/dns.go
|
@ -3,10 +3,23 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 {
|
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 +30,108 @@ 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)
|
||||||
|
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)
|
return success(context, req, resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
api/serve.go
29
api/serve.go
|
@ -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,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)
|
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) {
|
mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
requestContext := makeRequestContext()
|
requestContext := makeRequestContext()
|
||||||
name := r.PathValue("name")
|
name := r.PathValue("name")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,12 @@ const (
|
||||||
ExpiryDuration = time.Hour * 24
|
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 {
|
type User struct {
|
||||||
ID string `json:"sub"`
|
ID string `json:"sub"`
|
||||||
Mail string `json:"email"`
|
Mail string `json:"email"`
|
||||||
|
@ -119,3 +125,60 @@ func DeleteExpiredSessions(dbConn *sql.DB) error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -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 }}
|
|
@ -35,7 +35,10 @@
|
||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
<a href="/dns">dns.</a>
|
<a href="/dns">dns.</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
<a href="/keys">api keys.</a>
|
||||||
|
<span> | </span>
|
||||||
<a href="/logout">logout, {{ .User.DisplayName }}.</a>
|
<a href="/logout">logout, {{ .User.DisplayName }}.</a>
|
||||||
|
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<a href="/login">login.</a>
|
<a href="/login">login.</a>
|
||||||
{{ end }}
|
{{ 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/connection.gif'>
|
||||||
<img width='150' height='20' src='/static/img/blinkies/eepy.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/loveuguys.gif'>
|
||||||
|
<img width='150' height='20' src='/static/img/blinkies/capitalism.gif'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -6,14 +6,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func RandomId() string {
|
func RandomId() string {
|
||||||
uuid := make([]byte, 16)
|
id := make([]byte, 16)
|
||||||
_, err := rand.Read(uuid)
|
_, err := rand.Read(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid[8] = uuid[8]&^0xc0 | 0x80
|
return fmt.Sprintf("%x", id)
|
||||||
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:])
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue