guestbook!
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
7cc13887ea
commit
5080c566ac
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
22
api/dns.go
22
api/dns.go
|
@ -72,6 +72,16 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
|||
formErrors.Errors = append(formErrors.Errors, "invalid ttl")
|
||||
}
|
||||
|
||||
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 dnsRecordCount >= MAX_USER_RECORDS {
|
||||
formErrors.Errors = append(formErrors.Errors, "max records reached")
|
||||
}
|
||||
|
||||
dnsRecord := &database.DNSRecord{
|
||||
UserID: context.User.ID,
|
||||
Name: name,
|
||||
|
@ -80,17 +90,6 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
|||
TTL: ttlNum,
|
||||
Internal: internal,
|
||||
}
|
||||
|
||||
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.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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
14
api/serve.go
14
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")
|
||||
|
|
20
args/args.go
20
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }}
|
||||
|
|
Loading…
Reference in New Issue