kennel #13
|
@ -4,5 +4,6 @@ import "io"
|
||||||
|
|
||||||
type FilesAdapter interface {
|
type FilesAdapter interface {
|
||||||
CreateFile(path string, content io.Reader) (string, error)
|
CreateFile(path string, content io.Reader) (string, error)
|
||||||
|
FileExists(path string) bool
|
||||||
DeleteFile(path string) error
|
DeleteFile(path string) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,3 +35,8 @@ func (f *FilesystemAdapter) CreateFile(path string, content io.Reader) (string,
|
||||||
func (f *FilesystemAdapter) DeleteFile(path string) error {
|
func (f *FilesystemAdapter) DeleteFile(path string) error {
|
||||||
return os.Remove(f.BasePath + path)
|
return os.Remove(f.BasePath + path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FilesystemAdapter) FileExists(path string) bool {
|
||||||
|
_, err := os.Stat(f.BasePath + path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
package kennel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MaxCatSize = 1024 * 100 // 60KB
|
||||||
|
const CatsPath = "cats/"
|
||||||
|
const CatsPrefix = "/uploads/cats/"
|
||||||
|
const DefaultCatSpritesheet = "/static/img/cat_spritesheets/default.gif"
|
||||||
|
const MaxUserCats = 15
|
||||||
|
|
||||||
|
func ListUserCatsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
|
||||||
|
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
|
||||||
|
userID := context.User.ID
|
||||||
|
|
||||||
|
cats, err := database.GetUserKennelCats(context.DBConn, userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
resp.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
(*context.TemplateData)["Cats"] = cats
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCatContinuation(fileAdapter files.FilesAdapter, maxUserCats int, maxCatSize int, catsPath string, catsPrefix string, defaultCatSpritesheet string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
|
||||||
|
return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
|
||||||
|
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
|
||||||
|
formErrors := types.BannerMessages{
|
||||||
|
Messages: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
numCats, err := database.CountUserKennelCats(context.DBConn, context.User.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
resp.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
if numCats >= maxUserCats {
|
||||||
|
formErrors.Messages = append(formErrors.Messages, "max cats reached for user")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = req.ParseMultipartForm(int64(maxCatSize))
|
||||||
|
if err != nil {
|
||||||
|
formErrors.Messages = append(formErrors.Messages, "cat spritesheet too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
catID := utils.RandomId()
|
||||||
|
spritesheetPath := catsPrefix + catID
|
||||||
|
|
||||||
|
if len(formErrors.Messages) == 0 {
|
||||||
|
file, _, err := req.FormFile("spritesheet")
|
||||||
|
if file != nil && err != nil {
|
||||||
|
formErrors.Messages = append(formErrors.Messages, "error uploading spritesheet")
|
||||||
|
} else if file != nil {
|
||||||
|
defer file.Close()
|
||||||
|
reader := http.MaxBytesReader(resp, file, int64(maxCatSize))
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
_, err = fileAdapter.CreateFile(catsPath+catID, reader)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
formErrors.Messages = append(formErrors.Messages, "error saving spritesheet (is it too big?)")
|
||||||
|
}
|
||||||
|
} else if file == nil && err != nil {
|
||||||
|
spritesheetPath = defaultCatSpritesheet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link := req.FormValue("link")
|
||||||
|
description := req.FormValue("description")
|
||||||
|
name := req.FormValue("name")
|
||||||
|
|
||||||
|
cat := &database.KennelCat{
|
||||||
|
ID: catID,
|
||||||
|
UserID: context.User.ID,
|
||||||
|
Name: name,
|
||||||
|
Link: link,
|
||||||
|
Description: description,
|
||||||
|
Spritesheet: spritesheetPath,
|
||||||
|
}
|
||||||
|
formErrors.Messages = append(formErrors.Messages, validateCat(cat)...)
|
||||||
|
if len(formErrors.Messages) == 0 {
|
||||||
|
_, err := database.SaveKennelCat(context.DBConn, cat)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
formErrors.Messages = append(formErrors.Messages, "failed to save cat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(formErrors.Messages) > 0 {
|
||||||
|
(*context.TemplateData)["Error"] = formErrors
|
||||||
|
(*context.TemplateData)["CatForm"] = cat
|
||||||
|
resp.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
formSuccess := types.BannerMessages{
|
||||||
|
Messages: []string{"cat added."},
|
||||||
|
}
|
||||||
|
(*context.TemplateData)["Success"] = formSuccess
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveCatContinuation(fileAdapter files.FilesAdapter, catsPath string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
|
||||||
|
return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
|
||||||
|
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
|
||||||
|
catID := req.FormValue("id")
|
||||||
|
|
||||||
|
cat, err := database.GetKennelCat(context.DBConn, catID)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
resp.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
if cat == nil || cat.UserID != context.User.ID {
|
||||||
|
resp.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = database.DeleteKennelCat(context.DBConn, catID)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
resp.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fileAdapter.DeleteFile(catsPath + catID)
|
||||||
|
if err != nil && fileAdapter.FileExists(catsPath+catID) {
|
||||||
|
log.Println(err)
|
||||||
|
resp.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCatStateAtContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
|
||||||
|
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
|
||||||
|
atLong, err := strconv.ParseInt(req.FormValue("at"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
resp.WriteHeader(http.StatusBadRequest)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
cats, err := database.GetKennelStateAt(context.DBConn, atLong)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
resp.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
(*context.TemplateData)["EncodedState"] = cats.EncodedState
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCat(cat *database.KennelCat) []string {
|
||||||
|
errors := []string{}
|
||||||
|
|
||||||
|
if cat.Name == "" {
|
||||||
|
errors = append(errors, "name is required")
|
||||||
|
}
|
||||||
|
if cat.Link == "" {
|
||||||
|
errors = append(errors, "link is required")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(cat.Link, "http://") && !strings.HasPrefix(cat.Link, "https://") {
|
||||||
|
errors = append(errors, "link must be a valid URL")
|
||||||
|
}
|
||||||
|
if cat.Description == "" {
|
||||||
|
errors = append(errors, "description is required")
|
||||||
|
}
|
||||||
|
if len(cat.Description) > 100 {
|
||||||
|
errors = append(errors, "description must be less than 100 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
34
api/serve.go
34
api/serve.go
|
@ -13,6 +13,7 @@ import (
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/dns"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/dns"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/guestbook"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/guestbook"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/hcaptcha"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/hcaptcha"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/kennel"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/keys"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/keys"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/profiles"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/profiles"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/template"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/template"
|
||||||
|
@ -172,23 +173,28 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
|
||||||
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(hcaptcha.CaptchaVerificationContinuation, hcaptcha.CaptchaVerificationContinuation)(guestbook.SignGuestbookContinuation, FailurePassingContinuation)(guestbook.ListGuestbookContinuation, guestbook.ListGuestbookContinuation)(hcaptcha.CaptchaArgsContinuation, hcaptcha.CaptchaArgsContinuation)(template.TemplateContinuation("guestbook.html", true), template.TemplateContinuation("guestbook.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(hcaptcha.CaptchaVerificationContinuation, hcaptcha.CaptchaVerificationContinuation)(guestbook.SignGuestbookContinuation, FailurePassingContinuation)(guestbook.ListGuestbookContinuation, guestbook.ListGuestbookContinuation)(hcaptcha.CaptchaArgsContinuation, hcaptcha.CaptchaArgsContinuation)(template.TemplateContinuation("guestbook.html", true), template.TemplateContinuation("guestbook.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
mux.HandleFunc("GET /kennel", func(w http.ResponseWriter, r *http.Request) {
|
||||||
mux.HandleFunc("GET /kennel", func(w http.ResponseWriter, r *http.Request) {
|
requestContext := makeRequestContext()
|
||||||
requestContext := makeRequestContext()
|
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(kennel.GetCatStateAtContinuation, FailurePassingContinuation)(template.TemplateContinuation("kennel_enc.json", false), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
||||||
LogRequestContinuation(requestContext, r, w)(kennel.GetKennelStateContinuation, kennel.GetKennelStateContinuation)(template.TemplateContinuation("kennel.json", true), template.TemplateContinuation("kennel.json", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
})
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("GET /kennel/cats", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("GET /kennel/cats", func(w http.ResponseWriter, r *http.Request) {
|
||||||
requestContext := makeRequestContext()
|
requestContext := makeRequestContext()
|
||||||
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(kennel.ListUserCats, auth.GoLoginContinuation)(template.TemplateContinuation("kennel_cats.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, auth.GoLoginContinuation)(template.TemplateContinuation("kennel_cats.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("POST /kennel/cats", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestContext := makeRequestContext()
|
||||||
|
createCatContinuation := kennel.CreateCatContinuation(uploadAdapter, kennel.MaxUserCats, kennel.MaxCatSize, kennel.CatsPath, kennel.CatsPrefix, kennel.DefaultCatSpritesheet)
|
||||||
|
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(createCatContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, kennel.ListUserCatsContinuation)(template.TemplateContinuation("kennel_cats.html", true), template.TemplateContinuation("kennel_cats.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
||||||
|
})
|
||||||
|
|
||||||
mux.HandleFunc("POST /kennel", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("POST /kennel/cats/delete", func(w http.ResponseWriter, r *http.Request) {
|
||||||
requestContext := makeRequestContext()
|
requestContext := makeRequestContext()
|
||||||
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(hcaptcha.CaptchaVerificationContinuation, hcaptcha.CaptchaVerificationContinuation)(guestbook.SignGuestbookContinuation, FailurePassingContinuation)(guestbook.ListGuestbookContinuation, guestbook.ListGuestbookContinuation)(hcaptcha.CaptchaArgsContinuation, hcaptcha.CaptchaArgsContinuation)(template.TemplateContinuation("guestbook.html", true), template.TemplateContinuation("guestbook.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
deleteCatContinuation := kennel.RemoveCatContinuation(uploadAdapter, kennel.CatsPath)
|
||||||
})
|
|
||||||
*/
|
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(deleteCatContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, FailurePassingContinuation)(template.TemplateContinuation("kennel_cats.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
||||||
|
})
|
||||||
|
|
||||||
mux.HandleFunc("GET /{template}", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("GET /{template}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
requestContext := makeRequestContext()
|
requestContext := makeRequestContext()
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
type KennelCat struct {
|
type KennelCat struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
Link string `json:"link"`
|
Link string `json:"link"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Spritesheet string `json:"spritesheet"`
|
Spritesheet string `json:"spritesheet"`
|
||||||
|
@ -45,7 +46,7 @@ func GetUserKennelCats(db *sql.DB, userID string) ([]KennelCat, error) {
|
||||||
var cats []KennelCat
|
var cats []KennelCat
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var cat KennelCat
|
var cat KennelCat
|
||||||
err := rows.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
|
err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -62,7 +63,7 @@ func SaveKennelCat(db *sql.DB, cat *KennelCat) (*KennelCat, error) {
|
||||||
cat.CreatedAt = time.Now()
|
cat.CreatedAt = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Exec("INSERT OR REPLACE INTO kennel_cat (id, user_id, link, description, spritesheet, created_at) VALUES (?, ?, ?, ?, ?, ?)", cat.ID, cat.UserID, cat.Link, cat.Description, cat.Spritesheet, cat.CreatedAt)
|
_, err := db.Exec("INSERT OR REPLACE INTO kennel_cat (id, user_id, name, link, description, spritesheet, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", cat.ID, cat.UserID, cat.Name, cat.Link, cat.Description, cat.Spritesheet, cat.CreatedAt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -75,7 +76,7 @@ func GetKennelCat(db *sql.DB, catID string) (*KennelCat, error) {
|
||||||
|
|
||||||
row := db.QueryRow("SELECT * FROM kennel_cat WHERE id = ?", catID)
|
row := db.QueryRow("SELECT * FROM kennel_cat WHERE id = ?", catID)
|
||||||
var cat KennelCat
|
var cat KennelCat
|
||||||
err := row.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
|
err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -104,7 +105,7 @@ func GetKennel(dbConn *sql.DB) ([]KennelCat, error) {
|
||||||
var cats []KennelCat
|
var cats []KennelCat
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var cat KennelCat
|
var cat KennelCat
|
||||||
err := rows.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
|
err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -114,10 +115,22 @@ func GetKennel(dbConn *sql.DB) ([]KennelCat, error) {
|
||||||
return cats, nil
|
return cats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetKennelState(dbConn *sql.DB) (*KennelState, error) {
|
func GetLatestKennelState(dbConn *sql.DB) (*KennelState, error) {
|
||||||
log.Println("getting kennel state")
|
log.Println("getting kennel state")
|
||||||
|
|
||||||
row := dbConn.QueryRow("SELECT * FROM kennel_state")
|
row := dbConn.QueryRow("SELECT * FROM kennel_state ORDER BY at DESC LIMIT 1")
|
||||||
|
var state KennelState
|
||||||
|
err := row.Scan(&state.At, &state.EncodedState)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetKennelStateAt(dbConn *sql.DB, at int64) (*KennelState, error) {
|
||||||
|
log.Println("getting kennel state at", at)
|
||||||
|
|
||||||
|
row := dbConn.QueryRow("SELECT * FROM kennel_state WHERE at = ?", at)
|
||||||
var state KennelState
|
var state KennelState
|
||||||
err := row.Scan(&state.At, &state.EncodedState)
|
err := row.Scan(&state.At, &state.EncodedState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -167,6 +167,7 @@ func MigrateKennel(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
|
|
||||||
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS kennel_cat (
|
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS kennel_cat (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
link TEXT NOT NULL,
|
link TEXT NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
--container-bg-light: #fff7f87a;
|
--container-bg-light: #fff7f87a;
|
||||||
--border-color-light: #692fcc;
|
--border-color-light: #692fcc;
|
||||||
--error-color-light: #a83254;
|
--error-color-light: #a83254;
|
||||||
|
--tr-color-light: #8bcefa;
|
||||||
|
|
||||||
--background-color-dark: #333;
|
--background-color-dark: #333;
|
||||||
--background-color-dark-2: #2c2c2c;
|
--background-color-dark-2: #2c2c2c;
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
--container-bg-dark: #424242ea;
|
--container-bg-dark: #424242ea;
|
||||||
--border-color-dark: #956ade;
|
--border-color-dark: #956ade;
|
||||||
--error-color-dark: #851736;
|
--error-color-dark: #851736;
|
||||||
|
--tr-color-dark: #212a6a;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="DARK"] {
|
[data-theme="DARK"] {
|
||||||
|
@ -27,6 +29,7 @@
|
||||||
--border-color: var(--border-color-dark);
|
--border-color: var(--border-color-dark);
|
||||||
--error-color: var(--error-color-dark);
|
--error-color: var(--error-color-dark);
|
||||||
--confirm-color: var(--confirm-color-dark);
|
--confirm-color: var(--confirm-color-dark);
|
||||||
|
--tr-color: var(--tr-color-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="LIGHT"] {
|
[data-theme="LIGHT"] {
|
||||||
|
@ -38,6 +41,7 @@
|
||||||
--border-color: var(--border-color-light);
|
--border-color: var(--border-color-light);
|
||||||
--error-color: var(--error-color-light);
|
--error-color: var(--error-color-light);
|
||||||
--confirm-color: var(--confirm-color-light);
|
--confirm-color: var(--confirm-color-light);
|
||||||
|
--tr-color: var(--tr-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
|
|
@ -68,3 +68,104 @@ hr {
|
||||||
.info:hover {
|
.info:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
@import "/static/css/colors.css";
|
||||||
|
@import "/static/css/blinky.css";
|
||||||
|
@import "/static/css/table.css";
|
||||||
|
@import "/static/css/form.css";
|
||||||
|
@import "/static/css/guestbook.css";
|
||||||
|
@import "/static/css/club.css";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "ComicSans";
|
||||||
|
src: url("/static/font/comicsans.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: "ComicSans", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* i just cannot get this to look good on firefox... */
|
||||||
|
@supports not (-moz-appearance: none) {
|
||||||
|
* {
|
||||||
|
cursor: url("/static/img/cursor-2.png"), auto;
|
||||||
|
-webkit-animation: cursor 400ms infinite;
|
||||||
|
animation: cursor 400ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes cursor {
|
||||||
|
0% {
|
||||||
|
cursor: url("/static/img/cursor-2.png"), auto;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
cursor: url("/static/img/cursor-1.png"), auto;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
cursor: url("/static/img/cursor-2.png"), auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cursor {
|
||||||
|
0% {
|
||||||
|
cursor: url("/static/img/cursor-2.png"), auto;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
cursor: url("/static/img/cursor-1.png"), auto;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
cursor: url("/static/img/cursor-2.png"), auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
background-image: url("/static/img/stars.gif");
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: auto;
|
||||||
|
background-color: var(--container-bg);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--text-color);
|
||||||
|
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkies {
|
||||||
|
display: flex;
|
||||||
|
justify-content: left;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 900px;
|
||||||
|
gap: 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
max-width: 600px;
|
||||||
|
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,6 @@ tbody tr {
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:hover {
|
tbody tr:hover {
|
||||||
background-color: #ff47daa0;
|
background-color: var(--tr-color);
|
||||||
color: #2a2a2a;
|
color: #2a2a2a;
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
|
@ -35,6 +35,8 @@
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="/keys">api keys.</a>
|
<a href="/keys">api keys.</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
<a href="/kennel/cats">kennel.</a>
|
||||||
|
<span> | </span>
|
||||||
<a href="/profile">{{ .User.DisplayName }}.</a>
|
<a href="/profile">{{ .User.DisplayName }}.</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="/logout">logout.</a>
|
<a href="/logout">logout.</a>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<p>note that the name <em>must</em> be a subdomain of <em>{{ .User.Username }}</em></p>
|
<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"
|
<input type="text" name="type"
|
||||||
{{ if not .RecordForm }}
|
{{ if not .RecordForm }}
|
||||||
placeholder="CNAME"
|
placeholder="CNAME"
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
{{ define "content" }}
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<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="7"><span class="blinky">no dns records found.</span></td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
{{ range $record := .DNSRecords }}
|
|
||||||
<tr>
|
|
||||||
<td>{{ $record.Type }}</td>
|
|
||||||
<td>{{ $record.Name }}</td>
|
|
||||||
<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 }}" />
|
|
||||||
<input type="submit" value="Delete" />
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</table>
|
|
||||||
<br>
|
|
||||||
<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"
|
|
||||||
{{ if not .RecordForm }}
|
|
||||||
placeholder="CNAME"
|
|
||||||
{{ else }}
|
|
||||||
value="{{ .RecordForm.Type }}"
|
|
||||||
{{ end }}
|
|
||||||
required />
|
|
||||||
<label for="name">name.</label>
|
|
||||||
<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"
|
|
||||||
{{ if not .RecordForm }}
|
|
||||||
placeholder="{{ .User.Username }}.dev"
|
|
||||||
{{ else }}
|
|
||||||
value="{{ .RecordForm.Content }}"
|
|
||||||
{{ end }}
|
|
||||||
required />
|
|
||||||
<label for="ttl">ttl.</label>
|
|
||||||
<input type="text" name="ttl"
|
|
||||||
{{ if not .RecordForm }}
|
|
||||||
placeholder="43200"
|
|
||||||
{{ else }}
|
|
||||||
value="{{ .RecordForm.TTL }}"
|
|
||||||
{{ end }}
|
|
||||||
required />
|
|
||||||
<label for="internal">
|
|
||||||
internal.
|
|
||||||
<input style='display:inline;width:auto;' type="checkbox" name="internal"
|
|
||||||
{{ if .RecordForm.Internal }}
|
|
||||||
checked
|
|
||||||
{{ end }}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<input type="submit" value="add." />
|
|
||||||
</form>
|
|
||||||
{{ end }}
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>name.</th>
|
||||||
|
<th>link.</th>
|
||||||
|
<th>description.</th>
|
||||||
|
<th>spritesheet.</th>
|
||||||
|
<th>created at.</th>
|
||||||
|
<th>remove.</th>
|
||||||
|
</tr>
|
||||||
|
{{ if (eq (len .Cats) 0) }}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6"><span class="blinky">no cats found</span></td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
{{ range $cat := .Cats }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ $cat.Name }}</td>
|
||||||
|
<td><a href="{{ $cat.Link }}">{{ $cat.Link }}</a></td>
|
||||||
|
<td>{{ $cat.Description }}</td>
|
||||||
|
<td><a href="{{ $cat.Spritesheet }}"><img width="100" src="{{ $cat.Spritesheet }}"></a></td>
|
||||||
|
<td class="time">{{ $cat.CreatedAt }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="/kennel/cats/delete">
|
||||||
|
<input type="hidden" name="id" value="{{ $cat.ID }}" />
|
||||||
|
<input type="submit" value="remove." />
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
|
<form method="POST" action="/kennel/cats" class="form" enctype="multipart/form-data">
|
||||||
|
<h2>add cat.</h2>
|
||||||
|
<hr>
|
||||||
|
<label for="name">name.</label>
|
||||||
|
<input type="text" name="name" id="name"
|
||||||
|
{{ if not .CatForm }}
|
||||||
|
placeholder="wallace."
|
||||||
|
{{ else }}
|
||||||
|
value="{{ .CatForm.Name }}"
|
||||||
|
{{ end }}
|
||||||
|
/>
|
||||||
|
<label for="description">description.</label>
|
||||||
|
<input type="text" name="description" id="description"
|
||||||
|
{{ if not .CatForm }}
|
||||||
|
placeholder="a cat."
|
||||||
|
{{ else }}
|
||||||
|
value="{{ .CatForm.Description }}"
|
||||||
|
{{ end }}
|
||||||
|
/>
|
||||||
|
<label for="link">link.</label>
|
||||||
|
<input type="text" name="link" id="link"
|
||||||
|
{{ if not .CatForm }}
|
||||||
|
placeholder="https://hatecomputers.club"
|
||||||
|
{{ else }}
|
||||||
|
value="{{ .CatForm.Link }}"
|
||||||
|
{{ end }}/>
|
||||||
|
|
||||||
|
<label for="spritesheet" style="margin:0">spritesheet.</label>
|
||||||
|
<h6>if not specified, will use <a href="/static/img/cat_spritesheets/default.gif">the default</a>. check it out for the format we expect. max 50KB.</h6>
|
||||||
|
<input type="file" name="spritesheet" id="spritesheet" />
|
||||||
|
|
||||||
|
<input type="submit" value="mrow." />
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ define "content" }}{{ .EncodedState }}{{ end }}
|
Loading…
Reference in New Issue