dns api #1
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
@ -19,7 +18,6 @@ func CreateDNSRecord(zoneId string, apiToken string, record *database.DNSRecord)
|
|||
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)
|
||||
|
|
|
@ -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"
|
||||
"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)
|
||||
}
|
||||
|
||||
|
|
20
api/dns.go
20
api/dns.go
|
@ -10,6 +10,8 @@ import (
|
|||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
||||
)
|
||||
|
||||
const MAX_USER_RECORDS = 20
|
||||
|
||||
type FormError struct {
|
||||
Errors []string
|
||||
}
|
||||
|
@ -43,6 +45,9 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
|||
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,
|
||||
|
@ -52,8 +57,14 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
|||
TTL: ttlNum,
|
||||
}
|
||||
|
||||
dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID)
|
||||
if err != nil {
|
||||
formErrors.Errors = append(formErrors.Errors, "invalid ttl")
|
||||
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) {
|
||||
|
@ -83,13 +94,6 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
|||
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
|
||||
|
|
15
api/serve.go
15
api/serve.go
|
@ -126,6 +126,21 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
|
|||
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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }}
|
||||
<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 }}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue