mirror of
https://github.com/thomiceli/opengist.git
synced 2024-12-22 12:32:40 +00:00
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:
parent
4f623881ac
commit
25316d7bf2
11 changed files with 114 additions and 33 deletions
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
3
templates/base/gist_header.html
vendored
3
templates/base/gist_header.html
vendored
|
@ -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>
|
||||
|
|
2
templates/pages/all.html
vendored
2
templates/pages/all.html
vendored
|
@ -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">
|
||||
|
|
21
templates/pages/create.html
vendored
21
templates/pages/create.html
vendored
|
@ -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>
|
||||
|
|
19
templates/pages/edit.html
vendored
19
templates/pages/edit.html
vendored
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue