mirror of
https://github.com/thomiceli/opengist.git
synced 2025-06-13 22:07:11 +02:00
Add custom urls for gists (#183)
This commit is contained in:
@ -48,6 +48,7 @@ type Gist struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Uuid string
|
||||
Title string
|
||||
URL string
|
||||
Preview string
|
||||
PreviewFilename string
|
||||
Description string
|
||||
@ -83,7 +84,7 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
||||
func GetGist(user string, gistUuid string) (*Gist, error) {
|
||||
gist := new(Gist)
|
||||
err := db.Preload("User").Preload("Forked.User").
|
||||
Where("gists.uuid = ? AND users.username like ?", gistUuid, user).
|
||||
Where("(gists.uuid = ? OR gists.url = ?) AND users.username like ?", gistUuid, gistUuid, user).
|
||||
Joins("join users on gists.user_id = users.id").
|
||||
First(&gist).Error
|
||||
|
||||
@ -460,11 +461,19 @@ func (gist *Gist) VisibilityStr() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (gist *Gist) Identifier() string {
|
||||
if gist.URL != "" {
|
||||
return gist.URL
|
||||
}
|
||||
return gist.Uuid
|
||||
}
|
||||
|
||||
// -- DTO -- //
|
||||
|
||||
type GistDTO struct {
|
||||
Title string `validate:"max=250" form:"title"`
|
||||
Description string `validate:"max=1000" form:"description"`
|
||||
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
|
||||
Private Visibility `validate:"number,min=0,max=2" form:"private"`
|
||||
Files []FileDTO `validate:"min=1,dive"`
|
||||
Name []string `form:"name"`
|
||||
@ -481,11 +490,13 @@ func (dto *GistDTO) ToGist() *Gist {
|
||||
Title: dto.Title,
|
||||
Description: dto.Description,
|
||||
Private: dto.Private,
|
||||
URL: dto.URL,
|
||||
}
|
||||
}
|
||||
|
||||
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
|
||||
gist.Title = dto.Title
|
||||
gist.Description = dto.Description
|
||||
gist.URL = dto.URL
|
||||
return gist
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ gist.no-content: No content
|
||||
gist.new.new_gist: New gist
|
||||
gist.new.title: Title
|
||||
gist.new.description: Description
|
||||
gist.new.url: URL
|
||||
gist.new.filename-with-extension: Filename with extension
|
||||
gist.new.indent-mode: Indent mode
|
||||
gist.new.indent-mode-space: Space
|
||||
|
@ -255,7 +255,7 @@ func allGists(ctx echo.Context) error {
|
||||
for _, gist := range gists {
|
||||
rendered, err := render.HighlightGistPreview(gist)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error rendering gist preview for " + gist.Uuid + " - " + gist.PreviewFilename)
|
||||
log.Warn().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
|
||||
}
|
||||
renderedFiles = append(renderedFiles, &rendered)
|
||||
}
|
||||
@ -329,14 +329,15 @@ func gistJson(ctx echo.Context) error {
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
jsUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), gist.User.Username, gist.Uuid+".js")
|
||||
jsUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), gist.User.Username, gist.Identifier()+".js")
|
||||
if err != nil {
|
||||
return errorRes(500, "Error joining url", err)
|
||||
}
|
||||
|
||||
return ctx.JSON(200, map[string]interface{}{
|
||||
"owner": gist.User.Username,
|
||||
"id": gist.Uuid,
|
||||
"id": gist.Identifier(),
|
||||
"uuid": gist.Uuid,
|
||||
"title": gist.Title,
|
||||
"description": gist.Description,
|
||||
"created_at": time.Unix(gist.CreatedAt, 0).Format(time.RFC3339),
|
||||
@ -388,7 +389,7 @@ document.write('%s')
|
||||
func revisions(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
userName := gist.User.Username
|
||||
gistName := gist.Uuid
|
||||
gistName := gist.Identifier()
|
||||
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
@ -546,7 +547,7 @@ func processCreate(ctx echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
return redirect(ctx, "/"+user.Username+"/"+gist.Uuid)
|
||||
return redirect(ctx, "/"+user.Username+"/"+gist.Identifier())
|
||||
}
|
||||
|
||||
func toggleVisibility(ctx echo.Context) error {
|
||||
@ -558,7 +559,7 @@ func toggleVisibility(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
addFlash(ctx, "Gist visibility has been changed", "success")
|
||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
|
||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
|
||||
}
|
||||
|
||||
func deleteGist(ctx echo.Context) error {
|
||||
@ -591,7 +592,7 @@ func like(ctx echo.Context) error {
|
||||
return errorRes(500, "Error liking/dislking this gist", err)
|
||||
}
|
||||
|
||||
redirectTo := "/" + gist.User.Username + "/" + gist.Uuid
|
||||
redirectTo := "/" + gist.User.Username + "/" + gist.Identifier()
|
||||
if r := ctx.QueryParam("redirecturl"); r != "" {
|
||||
redirectTo = r
|
||||
}
|
||||
@ -609,11 +610,11 @@ func fork(ctx echo.Context) error {
|
||||
|
||||
if gist.User.ID == currentUser.ID {
|
||||
addFlash(ctx, "Unable to fork own gists", "error")
|
||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
|
||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
|
||||
}
|
||||
|
||||
if alreadyForked.ID != 0 {
|
||||
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Uuid)
|
||||
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Identifier())
|
||||
}
|
||||
|
||||
uuidGist, err := uuid.NewRandom()
|
||||
@ -646,7 +647,7 @@ func fork(ctx echo.Context) error {
|
||||
|
||||
addFlash(ctx, "Gist has been forked", "success")
|
||||
|
||||
return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Uuid)
|
||||
return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Identifier())
|
||||
}
|
||||
|
||||
func rawFile(ctx echo.Context) error {
|
||||
@ -736,7 +737,7 @@ func downloadZip(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
ctx.Response().Header().Set("Content-Type", "application/zip")
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Uuid+".zip")
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Identifier()+".zip")
|
||||
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes())))
|
||||
_, err = ctx.Response().Write(zipFile.Bytes())
|
||||
if err != nil {
|
||||
@ -755,7 +756,7 @@ func likes(ctx echo.Context) error {
|
||||
return errorRes(500, "Error getting users who liked this gist", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Uuid+"/likes", 1); err != nil {
|
||||
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
}
|
||||
|
||||
@ -779,7 +780,7 @@ func forks(ctx echo.Context) error {
|
||||
return errorRes(500, "Error getting users who liked this gist", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Uuid+"/forks", 2); err != nil {
|
||||
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
}
|
||||
|
||||
|
@ -398,7 +398,7 @@ func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
gist := getData(ctx, "gist")
|
||||
user := getUserLogged(ctx)
|
||||
if !gist.(*db.Gist).CanWrite(user) {
|
||||
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Uuid)
|
||||
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
|
@ -199,3 +199,59 @@ func TestLikeFork(t *testing.T) {
|
||||
require.Equal(t, gist1db.Private, gist2db.Private)
|
||||
require.Equal(t, user2.Username, gist2db.User.Username)
|
||||
}
|
||||
|
||||
func TestCustomUrl(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
URL: "my-gist",
|
||||
Description: "my first gist",
|
||||
Private: 0,
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), gist1db.ID)
|
||||
require.Equal(t, gist1.Title, gist1db.Title)
|
||||
require.Equal(t, gist1.Description, gist1db.Description)
|
||||
require.Regexp(t, "[a-f0-9]{32}", gist1db.Uuid)
|
||||
require.Equal(t, gist1.URL, gist1db.URL)
|
||||
require.Equal(t, user1.Username, gist1db.User.Username)
|
||||
|
||||
gist1dbUuid, err := db.GetGist(user1.Username, gist1db.Uuid)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db, gist1dbUuid)
|
||||
|
||||
gist1dbUrl, err := db.GetGist(user1.Username, gist1.URL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db, gist1dbUrl)
|
||||
|
||||
require.Equal(t, gist1.URL, gist1db.Identifier())
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "gist2",
|
||||
Description: "my second gist",
|
||||
Private: 0,
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
}
|
||||
err = s.request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, gist2db.Uuid, gist2db.Identifier())
|
||||
require.NotEqual(t, gist2db.URL, gist2db.Identifier())
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"golang.org/x/crypto/argon2"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@ -135,6 +136,8 @@ type OpengistValidator struct {
|
||||
func NewValidator() *OpengistValidator {
|
||||
v := validator.New()
|
||||
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
|
||||
_ = v.RegisterValidation("alphanumdash", validateAlphaNumDash)
|
||||
_ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty)
|
||||
return &OpengistValidator{v}
|
||||
}
|
||||
|
||||
@ -158,6 +161,9 @@ func validationMessages(err *error) string {
|
||||
messages[i] = e.Field() + " should not include a sub directory"
|
||||
case "alphanum":
|
||||
messages[i] = e.Field() + " should only contain alphanumeric characters"
|
||||
case "alphanumdash":
|
||||
case "alphanumdashorempty":
|
||||
messages[i] = e.Field() + " should only contain alphanumeric characters and dashes"
|
||||
case "min":
|
||||
messages[i] = "Not enough " + e.Field()
|
||||
case "notreserved":
|
||||
@ -181,6 +187,14 @@ func validateReservedKeywords(fl validator.FieldLevel) bool {
|
||||
return !ok
|
||||
}
|
||||
|
||||
func validateAlphaNumDash(fl validator.FieldLevel) bool {
|
||||
return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
|
||||
}
|
||||
|
||||
func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool {
|
||||
return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
|
||||
}
|
||||
|
||||
func getPage(ctx echo.Context) int {
|
||||
page := ctx.QueryParam("page")
|
||||
if page == "" {
|
||||
|
Reference in New Issue
Block a user