Added private visibility

* Changed gist type and added HTML button on creation

* Adapted label and edit button

* Changed rules for git HTTP and SSH

* Adapt Readme features
This commit is contained in:
Thomas Miceli 2023-09-02 03:58:37 +02:00 committed by GitHub
parent 4f623881ac
commit 25316d7bf2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 114 additions and 33 deletions

View file

@ -22,7 +22,7 @@ A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomic
## Features
* Create public or unlisted snippets
* Create public, unlisted or private snippets
* Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Revisions history
* Syntax highlighting ; markdown & CSV support

View file

@ -15,7 +15,7 @@ type Gist struct {
Preview string
PreviewFilename string
Description string
Private bool
Private int // 0: public, 1: unlisted, 2: private
UserID uint
User User
NbFiles int
@ -89,7 +89,7 @@ func GetAllGists(offset int) ([]*Gist, error) {
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId).
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%").
Limit(11).
Offset(offset * 10).
@ -101,7 +101,7 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId).
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}
@ -124,7 +124,7 @@ func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId).
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("likes.user_id = ?", fromUserId).
Joins("join likes on gists.id = likes.gist_id").
Joins("join users on likes.user_id = users.id")
@ -147,7 +147,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId).
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.user_id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}
@ -243,7 +243,7 @@ func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
var gists []*Gist
err := db.Model(&gist).Preload("User").
Where("forked_id = ?", gist.ID).
Where("(gists.private = 0) or (gists.private = 1 and gists.user_id = ?)", currentUserId).
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
Limit(11).
Offset(offset * 10).
Order("updated_at desc").
@ -379,7 +379,7 @@ func (gist *Gist) UpdatePreviewAndCount() error {
type GistDTO struct {
Title string `validate:"max=50" form:"title"`
Description string `validate:"max=150" form:"description"`
Private bool `form:"private"`
Private int `validate:"number,min=0,max=2" form:"private"`
Files []FileDTO `validate:"min=1,dive"`
}

View file

@ -42,12 +42,21 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
return errors.New("internal server error")
}
if verb == "receive-pack" || requireLogin == "1" {
// Check for the key if :
// - user wants to push the gist
// - user wants to clone a private gist
// - gist is not found (obfuscation)
// - admin setting to require login is set to true
if verb == "receive-pack" ||
gist.Private == 2 ||
gist.ID == 0 ||
requireLogin == "1" {
pubKey, err := models.SSHKeyExistsForUser(key, gist.UserID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
return errors.New("unauthorized")
return errors.New("gist not found")
}
errorSsh("Failed to get user by SSH key id", err)
return errors.New("internal server error")

View file

@ -80,7 +80,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
setData(ctx, "hasLiked", hasLiked)
}
if gist.Private {
if gist.Private > 0 {
setData(ctx, "NoIndex", true)
}
@ -88,6 +88,22 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
}
}
// gistSoftInit try to load a gist (same as gistInit) but does not return a 404 if the gist is not found
// useful for git clients using HTTP to obfuscate the existence of a private gist
func gistSoftInit(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
userName := ctx.Param("user")
gistName := ctx.Param("gistname")
gistName = strings.TrimSuffix(gistName, ".git")
gist, _ := models.GetGist(userName, gistName)
setData(ctx, "gist", gist)
return next(ctx)
}
}
func allGists(ctx echo.Context) error {
var err error
var urlPage string
@ -400,7 +416,7 @@ func processCreate(ctx echo.Context) error {
func toggleVisibility(ctx echo.Context) error {
var gist = getData(ctx, "gist").(*models.Gist)
gist.Private = !gist.Private
gist.Private = (gist.Private + 1) % 3
if err := gist.Update(); err != nil {
return errorRes(500, "Error updating this gist", err)
}

View file

@ -47,16 +47,23 @@ func gitHttp(ctx echo.Context) error {
gist := getData(ctx, "gist").(*models.Gist)
// Shows basic auth if :
// - user wants to push the gist
// - user wants to clone a private gist
// - gist is not found (obfuscation)
// - admin setting to require login is set to true
noAuth := (ctx.QueryParam("service") == "git-upload-pack" ||
strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") ||
ctx.Request().Method == "GET") &&
gist.Private != 2 &&
gist.ID != 0 &&
!getData(ctx, "RequireLogin").(bool)
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
if _, err := os.Stat(repositoryPath); os.IsNotExist(err) {
if err != nil {
return errorRes(500, "Repository does not exist", err)
return errorRes(404, "Repository directory does not exist", err)
}
}
@ -82,12 +89,16 @@ func gitHttp(ctx echo.Context) error {
return basicAuth(ctx)
}
if gist.ID == 0 {
return errorRes(404, "Not found", nil)
}
if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
if err != nil {
return errorRes(500, "Cannot verify password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return errorRes(403, "Unauthorized", nil)
return errorRes(404, "Not found", nil)
}
return route.handler(ctx)

View file

@ -30,11 +30,11 @@ var re = regexp.MustCompile("[^a-z0-9]+")
var fm = template.FuncMap{
"split": strings.Split,
"indexByte": strings.IndexByte,
"toInt": func(i string) int64 {
val, _ := strconv.ParseInt(i, 10, 64)
"toInt": func(i string) int {
val, _ := strconv.Atoi(i)
return val
},
"inc": func(i int64) int64 {
"inc": func(i int) int {
return i + 1
},
"splitGit": func(i string) []string {
@ -88,6 +88,20 @@ var fm = template.FuncMap{
return config.C.ExternalUrl + "/" + manifestEntries[jsfile].File
},
"defaultAvatar": defaultAvatar,
"visibilityStr": func(visibility int, lowercase bool) string {
s := "Public"
switch visibility {
case 1:
s = "Unlisted"
case 2:
s = "Private"
}
if lowercase {
return strings.ToLower(s)
}
return s
},
}
var EmbedFS fs.FS
@ -226,7 +240,7 @@ func Start() {
debugStr := ""
// Git HTTP routes
if config.C.HttpGit {
e.Any("/:user/:gistname/*", gitHttp, gistInit)
e.Any("/:user/:gistname/*", gitHttp, gistSoftInit)
debugStr = " (with Git over HTTP)"
}

View file

@ -183,4 +183,20 @@ document.addEventListener('DOMContentLoaded', () => {
});
};
});
const gistmenuvisibility = document.getElementById('gist-menu-visibility');
if (gistmenuvisibility) {
let submitgistbutton = (document.getElementById('submit-gist') as HTMLInputElement);
document.getElementById('gist-visibility-menu-button')!.onclick = () => {
console.log("z");
gistmenuvisibility!.classList.toggle('hidden');
}
Array.from(document.querySelectorAll('.gist-visibility-option')).forEach((el) => {
(el as HTMLElement).onclick = () => {
submitgistbutton.textContent = "Create " + el.textContent.toLowerCase() + " gist";
submitgistbutton!.value = (el as HTMLElement).dataset.visibility || '0';
gistmenuvisibility!.classList.add('hidden');
}
});
}
});

View file

@ -92,8 +92,7 @@
<p class="mt-1 max-w-2xl text-sm text-slate-500">Forked from <a href="{{ $.c.ExternalUrl }}/{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Uuid }}">{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}</a></p>
{{ end }}
<p class="mt-1 max-w-2xl text-sm text-slate-500">Last active <span class="moment-timestamp"> {{ .gist.UpdatedAt }} </span>
{{ if .gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> Unlisted </span>{{ end }}
{{ if .gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr .gist.Private false }} </span>{{ end }}
</p>
<p class="mt-3 max-w-2xl text-slate-700 dark:text-slate-300">{{ .gist.Description }}</p>
</header>

View file

@ -137,7 +137,7 @@
</div>
<h5 class="text-sm text-slate-500 pb-1">Last active <span class="moment-timestamp">{{ $gist.UpdatedAt }}</span>
{{ if $gist.Forked }} • Forked from <a href="{{ $.c.ExternalUrl }}/{{ $gist.Forked.User.Username }}/{{ $gist.Forked.Uuid }}">{{ $gist.Forked.User.Username }}/{{ $gist.Forked.Title }}</a> {{ end }}
{{ if $gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> Unlisted </span>{{ end }}</h5>
{{ if $gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr $gist.Private false }} </span>{{ end }}</h5>
<h5 class="text-xs text-slate-700 dark:text-slate-300 py-1">{{ $gist.Description }}</h5>
<a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Uuid }}" class="text-slate-700 dark:text-slate-300">
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto hover:border-primary-600">

View file

@ -56,8 +56,25 @@
<div class="flex">
<button type="button" id="add-file" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-white bg-gray-100 dark:bg-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">Add file</button>
<button type="submit" name="private" value="1" class="ml-auto inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-white bg-gray-100 dark:bg-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">Create unlisted gist</button>
<button type="submit" name="private" value="0" class="ml-2 inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">Create public gist</button>
<div class="ml-auto inline-flex ">
<button id="submit-gist" type="submit" name="private" value="0" class="ml-2 items-center px-4 py-2 border border-transparent border-primary-200 dark:border-primary-700 text-sm font-medium rounded-l-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 z-20">Create public gist</button>
<div class="relative -ml-px block">
<button type="button" class="relative inline-flex items-center rounded-r-md bg-primary-500 hover:bg-primary-600 px-2 py-2 text-gray-400 border border-transparent border-primary-200 dark:border-primary-700 focus:z-10" id="gist-visibility-menu-button">
<span class="sr-only">Open options</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="white" aria-hidden="true">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>
</button>
<div id="gist-menu-visibility" class="hidden absolute right-0 z-10 mt-2 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="gist-visibility-menu-button">
<div class="rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none" role="none">
<span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-visibility="0" role="menuitem">Public</span>
<span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-visibility="1" role="menuitem">Unlisted</span>
<span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-visibility="2" role="menuitem">Private</span>
</div>
</div>
</div>
</div>
</div>
{{ .csrfHtml }}
</form>

View file

@ -11,18 +11,17 @@
<form id="visibility" class="flex items-center whitespace-nowrap" method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/visibility">
{{ .csrfHtml }}
<button type="submit" class="ml-auto relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
{{ if .gist.Private }}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Make public
{{ if eq .gist.Private 2 }}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
{{ else }}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
Make unlisted
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
{{ end }}
Make {{ visibilityStr (inc .gist.Private) true }}
</button>
</form>
<form id="delete" onsubmit="return confirm('Are you sure you want to delete this gist ?')" class="ml-2 flex items-center" method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/delete">