kennel #13
			
				
			
		
		
		
	|  | @ -4,5 +4,6 @@ import "io" | |||
| 
 | ||||
| type FilesAdapter interface { | ||||
| 	CreateFile(path string, content io.Reader) (string, error) | ||||
| 	FileExists(path string) bool | ||||
| 	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 { | ||||
| 	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,238 @@ | |||
| package kennel | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"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 RingContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { | ||||
| 	return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { | ||||
| 		order := req.URL.Query().Get("order") | ||||
| 
 | ||||
| 		if order == "random" { | ||||
| 			kennelCat, err := database.GetRandomKennelCat(context.DBConn) | ||||
| 			if err != nil { | ||||
| 				log.Println(err) | ||||
| 				resp.WriteHeader(http.StatusInternalServerError) | ||||
| 				return failure(context, req, resp) | ||||
| 			} | ||||
| 			http.Redirect(resp, req, kennelCat.Link, http.StatusFound) | ||||
| 			return success(context, req, resp) | ||||
| 		} | ||||
| 
 | ||||
| 		id := req.URL.Query().Get("id") | ||||
| 		if id == "" { | ||||
| 			resp.WriteHeader(http.StatusBadRequest) | ||||
| 			return failure(context, req, resp) | ||||
| 		} | ||||
| 		if order != "random" && order != "next" && order != "prev" { | ||||
| 			kennelCat, err := database.GetKennelCat(context.DBConn, id) | ||||
| 			if err != nil { | ||||
| 				log.Println(err) | ||||
| 				resp.WriteHeader(http.StatusNotFound) | ||||
| 				return failure(context, req, resp) | ||||
| 			} | ||||
| 			http.Redirect(resp, req, kennelCat.Link, http.StatusFound) | ||||
| 			return success(context, req, resp) | ||||
| 		} | ||||
| 
 | ||||
| 		nextCat, err := database.GetNextKennelCat(context.DBConn, id, order == "next") | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			resp.WriteHeader(http.StatusInternalServerError) | ||||
| 			return failure(context, req, resp) | ||||
| 		} | ||||
| 
 | ||||
| 		http.Redirect(resp, req, nextCat.Link, http.StatusFound) | ||||
| 		return success(context, req, resp) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetKennelContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { | ||||
| 	return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { | ||||
| 		cats, err := database.GetKennel(context.DBConn) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			resp.WriteHeader(http.StatusInternalServerError) | ||||
| 			return failure(context, req, resp) | ||||
| 		} | ||||
| 		json, err := json.Marshal(cats) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			resp.WriteHeader(http.StatusInternalServerError) | ||||
| 			return failure(context, req, resp) | ||||
| 		} | ||||
| 
 | ||||
| 		resp.Header().Set("Content-Type", "application/json") | ||||
| 		resp.Write(json) | ||||
| 		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 | ||||
| } | ||||
							
								
								
									
										35
									
								
								api/serve.go
								
								
								
								
							
							
						
						
									
										35
									
								
								api/serve.go
								
								
								
								
							|  | @ -13,6 +13,7 @@ import ( | |||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/dns" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/guestbook" | ||||
| 	"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/profiles" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/template" | ||||
|  | @ -172,10 +173,38 @@ 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) | ||||
| 	}) | ||||
| 
 | ||||
| 	mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) { | ||||
| 	mux.HandleFunc("GET /kennel/", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		name := r.PathValue("name") | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(template.TemplateContinuation(name+".html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 		LogRequestContinuation(requestContext, r, w)(kennel.GetKennelContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	mux.HandleFunc("GET /kennel/cat", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		LogRequestContinuation(requestContext, r, w)(kennel.RingContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	mux.HandleFunc("GET /kennel/cats", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		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/cats/delete", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		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) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		templateFile := r.PathValue("template") | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(template.TemplateContinuation(templateFile+".html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	return &http.Server{ | ||||
|  |  | |||
|  | @ -0,0 +1,159 @@ | |||
| package database | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	_ "github.com/mattn/go-sqlite3" | ||||
| 	"log" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type KennelCat struct { | ||||
| 	ID          string    `json:"id"` | ||||
| 	UserID      string    `json:"user_id"` | ||||
| 	Name        string    `json:"name"` | ||||
| 	Link        string    `json:"link"` | ||||
| 	Description string    `json:"description"` | ||||
| 	Spritesheet string    `json:"spritesheet"` | ||||
| 	CreatedAt   time.Time `json:"created_at"` | ||||
| } | ||||
| 
 | ||||
| type KennelState struct { | ||||
| 	At           time.Time `json:"at"` | ||||
| 	EncodedState string    `json:"state"` | ||||
| } | ||||
| 
 | ||||
| func CountUserKennelCats(db *sql.DB, userID string) (int, error) { | ||||
| 	log.Println("counting kennel cats for user", userID) | ||||
| 
 | ||||
| 	row := db.QueryRow("SELECT COUNT(*) FROM kennel_cat WHERE user_id = ?", userID) | ||||
| 	var count int | ||||
| 	err := row.Scan(&count) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return count, nil | ||||
| } | ||||
| 
 | ||||
| func GetUserKennelCats(db *sql.DB, userID string) ([]KennelCat, error) { | ||||
| 	log.Println("getting kennel cats for user", userID) | ||||
| 
 | ||||
| 	rows, err := db.Query("SELECT * FROM kennel_cat WHERE user_id = ?", userID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
| 
 | ||||
| 	var cats []KennelCat | ||||
| 	for rows.Next() { | ||||
| 		var cat KennelCat | ||||
| 		err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		cats = append(cats, cat) | ||||
| 	} | ||||
| 
 | ||||
| 	return cats, nil | ||||
| } | ||||
| 
 | ||||
| func SaveKennelCat(db *sql.DB, cat *KennelCat) (*KennelCat, error) { | ||||
| 	log.Println("saving kennel cat", cat.ID) | ||||
| 
 | ||||
| 	if (cat.CreatedAt == time.Time{}) { | ||||
| 		cat.CreatedAt = time.Now() | ||||
| 	} | ||||
| 
 | ||||
| 	_, 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 { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return cat, nil | ||||
| } | ||||
| 
 | ||||
| func GetKennelCat(db *sql.DB, catID string) (*KennelCat, error) { | ||||
| 	log.Println("getting kennel cat", catID) | ||||
| 
 | ||||
| 	row := db.QueryRow("SELECT * FROM kennel_cat WHERE id = ?", catID) | ||||
| 	var cat KennelCat | ||||
| 	err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &cat, nil | ||||
| } | ||||
| 
 | ||||
| func DeleteKennelCat(db *sql.DB, catID string) error { | ||||
| 	log.Println("deleting kennel cat", catID) | ||||
| 
 | ||||
| 	_, err := db.Exec("DELETE FROM kennel_cat WHERE id = ?", catID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func GetRandomKennelCat(dbConn *sql.DB) (*KennelCat, error) { | ||||
| 	log.Println("getting random kennel cat") | ||||
| 
 | ||||
| 	row := dbConn.QueryRow("SELECT * FROM kennel_cat ORDER BY RANDOM() LIMIT 1") | ||||
| 	var cat KennelCat | ||||
| 	err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &cat, nil | ||||
| } | ||||
| 
 | ||||
| func GetNextKennelCat(dbConn *sql.DB, lastID string, next bool) (*KennelCat, error) { | ||||
| 	log.Println("getting next kennel cat") | ||||
| 
 | ||||
| 	operation := ">" | ||||
| 	sorted := "ASC" | ||||
| 	if !next { | ||||
| 		operation = "<" | ||||
| 		sorted = "DESC" | ||||
| 	} | ||||
| 
 | ||||
| 	row := dbConn.QueryRow("SELECT * FROM kennel_cat WHERE id "+operation+" ? ORDER BY id "+sorted+" LIMIT 1", lastID) | ||||
| 
 | ||||
| 	var cat KennelCat | ||||
| 	err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 	if err != nil { | ||||
| 		if next { | ||||
| 			// loop "back" to the first in the ring
 | ||||
| 			row = dbConn.QueryRow("SELECT * FROM kennel_cat ORDER BY id ASC LIMIT 1") | ||||
| 		} else { | ||||
| 			// loop "forward" to the first in the ring
 | ||||
| 			row = dbConn.QueryRow("SELECT * FROM kennel_cat ORDER BY id DESC LIMIT 1") | ||||
| 		} | ||||
| 		err = row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &cat, nil | ||||
| } | ||||
| 
 | ||||
| func GetKennel(dbConn *sql.DB) ([]KennelCat, error) { | ||||
| 	log.Println("getting kennel") | ||||
| 
 | ||||
| 	rows, err := dbConn.Query("SELECT * FROM kennel_cat") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
| 
 | ||||
| 	var cats []KennelCat | ||||
| 	for rows.Next() { | ||||
| 		var cat KennelCat | ||||
| 		err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		cats = append(cats, cat) | ||||
| 	} | ||||
| 
 | ||||
| 	return cats, nil | ||||
| } | ||||
|  | @ -162,6 +162,26 @@ func MigrateProfiles(dbConn *sql.DB) (*sql.DB, error) { | |||
| 	return dbConn, nil | ||||
| } | ||||
| 
 | ||||
| func MigrateKennel(dbConn *sql.DB) (*sql.DB, error) { | ||||
| 	log.Println("migrating kennel tables") | ||||
| 
 | ||||
| 	_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS kennel_cat ( | ||||
|                 id TEXT PRIMARY KEY, | ||||
|                 name TEXT NOT NULL, | ||||
| 		user_id INTEGER NOT NULL, | ||||
|                 link TEXT NOT NULL, | ||||
|                 description TEXT NOT NULL, | ||||
|                 spritesheet TEXT NOT NULL, | ||||
| 		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
| 		FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE | ||||
| 	);`) | ||||
| 	if err != nil { | ||||
| 		return dbConn, err | ||||
| 	} | ||||
| 
 | ||||
| 	return dbConn, nil | ||||
| } | ||||
| 
 | ||||
| func Migrate(dbConn *sql.DB) (*sql.DB, error) { | ||||
| 	log.Println("migrating database") | ||||
| 
 | ||||
|  | @ -173,6 +193,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) { | |||
| 		MigrateDNSRecords, | ||||
| 		MigrateGuestBook, | ||||
| 		MigrateProfiles, | ||||
| 		MigrateKennel, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, migration := range migrations { | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
|   --container-bg-light: #fff7f87a; | ||||
|   --border-color-light: #692fcc; | ||||
|   --error-color-light: #a83254; | ||||
|   --tr-color-light: #8bcefa; | ||||
| 
 | ||||
|   --background-color-dark: #333; | ||||
|   --background-color-dark-2: #2c2c2c; | ||||
|  | @ -16,6 +17,7 @@ | |||
|   --container-bg-dark: #424242ea; | ||||
|   --border-color-dark: #956ade; | ||||
|   --error-color-dark: #851736; | ||||
|   --tr-color-dark: #212a6a; | ||||
| } | ||||
| 
 | ||||
| [data-theme="DARK"] { | ||||
|  | @ -27,6 +29,7 @@ | |||
|   --border-color: var(--border-color-dark); | ||||
|   --error-color: var(--error-color-dark); | ||||
|   --confirm-color: var(--confirm-color-dark); | ||||
|   --tr-color: var(--tr-color-dark); | ||||
| } | ||||
| 
 | ||||
| [data-theme="LIGHT"] { | ||||
|  | @ -38,6 +41,7 @@ | |||
|   --border-color: var(--border-color-light); | ||||
|   --error-color: var(--error-color-light); | ||||
|   --confirm-color: var(--confirm-color-light); | ||||
|   --tr-color: var(--tr-color-light); | ||||
| } | ||||
| 
 | ||||
| .error { | ||||
|  |  | |||
|  | @ -26,6 +26,6 @@ tbody tr { | |||
| } | ||||
| 
 | ||||
| tbody tr:hover { | ||||
|   background-color: #ff47daa0; | ||||
|   background-color: var(--tr-color); | ||||
|   color: #2a2a2a; | ||||
| } | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 6.4 KiB | 
|  | @ -35,6 +35,8 @@ | |||
| 	<span> | </span> | ||||
| 	<a href="/keys">api keys.</a> | ||||
| 	<span> | </span> | ||||
|         <a href="/kennel/cats">kennel.</a> | ||||
| 	<span> | </span> | ||||
| 	<a href="/profile">{{ .User.DisplayName }}.</a> | ||||
| 	<span> | </span> | ||||
| 	<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> | ||||
|     <hr> | ||||
|     <label for="type">type.</label> | ||||
|     <input type="text" name="type" placeholder="CNAME" | ||||
|     <input type="text" name="type" | ||||
| 	   {{ if not .RecordForm }} | ||||
| 	   placeholder="CNAME" | ||||
| 	   {{ else }} | ||||
|  |  | |||
|  | @ -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 }} | ||||
		Loading…
	
		Reference in New Issue