Compare commits
9 Commits
v1.12.0
...
fix/gitea-
| Author | SHA1 | Date | |
|---|---|---|---|
|
a22475d692
|
|||
|
|
4d29a50e64 | ||
|
|
3a4602d412 | ||
|
|
2e10c1732a | ||
|
|
fe04c03acb | ||
|
|
2a1554d063 | ||
|
|
b7dbdde66b | ||
|
|
b7278b60ab | ||
|
|
84c6a41340 |
30
.github/workflows/docs.yml
vendored
30
.github/workflows/docs.yml
vendored
@@ -28,20 +28,16 @@ jobs:
|
||||
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
|
||||
npm run docs:build
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
source: "docs/.vitepress/dist/*"
|
||||
target: ${{ secrets.SERVER_PATH }}
|
||||
|
||||
- name: Update remote docs
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
${{ secrets.UPDATE_DOCS }}
|
||||
- name: Push to docs repository
|
||||
run: |
|
||||
git clone https://${{ secrets.STATIC_REPO_TOKEN }}@github.com/${{ secrets.STATIC_REPO }}.git target-repo
|
||||
rm -rf target-repo/srv/opengist
|
||||
mkdir -p target-repo/srv/opengist
|
||||
cp -r docs/.vitepress/dist/* target-repo/srv/opengist/
|
||||
cd target-repo
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "Deploy docs from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||
git pull --rebase
|
||||
git push
|
||||
|
||||
30
.github/workflows/helm.yml
vendored
30
.github/workflows/helm.yml
vendored
@@ -34,20 +34,16 @@ jobs:
|
||||
helm repo index --url https://helm.opengist.io --merge index.yaml .
|
||||
fi
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
source: "./helm/*.tgz,./helm/index.yaml"
|
||||
target: ${{ secrets.HELM_SERVER_PATH }}
|
||||
|
||||
- name: Update remote helm repository
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
${{ secrets.UPDATE_HELM_REPO }}
|
||||
- name: Push to docs repository
|
||||
run: |
|
||||
git clone https://${{ secrets.DOCS_REPO_TOKEN }}@github.com/${{ secrets.DOCS_REPO }}.git target-repo
|
||||
mkdir -p target-repo/helm
|
||||
cp helm/*.tgz target-repo/helm/
|
||||
cp helm/index.yaml target-repo/helm/
|
||||
cd target-repo
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "Deploy helm chart from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||
git pull --rebase
|
||||
git push
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [1.12.1](https://github.com/thomiceli/opengist/compare/v1.12.0...v1.12.1) - 2026-02-03
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
### Added
|
||||
- More translation strings (#605)
|
||||
|
||||
### Fixed
|
||||
- Allow Access Tokens with Required Login (#611)
|
||||
- Make text files renderable with mimetypes different than text/plain (#612)
|
||||
- Improve security on raw files endpoint (#613)
|
||||
|
||||
> Admins of Opengist instances may want to run "Synchronize all gists previews" in the admin panel.
|
||||
|
||||
## [1.12.0](https://github.com/thomiceli/opengist/compare/v1.11.1...v1.12.0) - 2026-01-27
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
|
||||
@@ -77,9 +77,9 @@ Download the archive for your system from the release page [here](https://github
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.0/opengist1.12.0-linux-amd64.tar.gz
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.0-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
@@ -4,9 +4,9 @@ Download the archive for your system from the release page [here](https://github
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.0/opengist1.12.0-linux-amd64.tar.gz
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.0-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
@@ -10,7 +10,7 @@ Requirements:
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
cd opengist
|
||||
|
||||
git checkout v1.12.0 # optional, to checkout the latest release
|
||||
git checkout v1.12.1 # optional, to checkout the latest release
|
||||
|
||||
make
|
||||
./opengist
|
||||
|
||||
@@ -27,9 +27,9 @@ Stop the running instance; then like your first installation of Opengist, downlo
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.0/opengist1.12.0-linux-amd64.tar.gz
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.0-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
29
helm/opengist/CHANGELOG.md
Normal file
29
helm/opengist/CHANGELOG.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Helm Chart Changelog
|
||||
|
||||
## 0.6.0 - 2026-02-03
|
||||
|
||||
- Bump Opengist image to 1.12.1
|
||||
|
||||
## 0.5.0 - 2026-01-27
|
||||
|
||||
- Bump Opengist image to 1.12.0
|
||||
- Add StatefulSet support
|
||||
- Add Prometheus ServiceMonitor support if Opengist metrics are enabled
|
||||
- New service for metrics endpoint, dissociated from the main service
|
||||
- Use existing pvc claim of provided
|
||||
|
||||
## 0.4.0 - 2025-09-30
|
||||
|
||||
- Bump Opengist image to 1.11.1
|
||||
|
||||
## 0.3.0 - 2025-09-21
|
||||
|
||||
- Bump Opengist image to 1.11.0
|
||||
|
||||
## 0.2.0 - 2025-05-10
|
||||
|
||||
- Add `deployment.env[]` in values
|
||||
|
||||
## 0.1.0 - 2025-04-06
|
||||
|
||||
- Initial release, with Opengist image 1.10.0
|
||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: opengist
|
||||
description: Opengist Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.5.0
|
||||
appVersion: 1.12.0
|
||||
version: 0.6.0
|
||||
appVersion: 1.12.1
|
||||
home: https://opengist.io
|
||||
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg
|
||||
sources:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Opengist Helm Chart
|
||||
|
||||
 
|
||||
 
|
||||
|
||||
Opengist Helm chart for Kubernetes. Check [CHANGELOG.md](CHANGELOG.md) for release notes.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ configExistingSecret: ""
|
||||
image:
|
||||
repository: ghcr.io/thomiceli/opengist
|
||||
pullPolicy: Always
|
||||
tag: "1.12.0"
|
||||
tag: "1.12.1"
|
||||
digest: ""
|
||||
imagePullSecrets: []
|
||||
# - name: "image-pull-secret"
|
||||
|
||||
@@ -2,16 +2,15 @@ package oauth
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
gojson "encoding/json"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/gitea"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type GiteaProvider struct {
|
||||
@@ -80,34 +79,7 @@ func (p *GiteaCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
|
||||
|
||||
func (p *GiteaCallbackProvider) UpdateUserDB(user *db.User) {
|
||||
user.GiteaID = p.User.UserID
|
||||
|
||||
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", p.User.UserID))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get user from Gitea")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot read Gitea response body")
|
||||
return
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = gojson.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot unmarshal Gitea response body")
|
||||
return
|
||||
}
|
||||
|
||||
field, ok := result["avatar_url"]
|
||||
if !ok {
|
||||
log.Error().Msg("Field 'avatar_url' not found in Gitea JSON response")
|
||||
return
|
||||
}
|
||||
|
||||
user.AvatarURL = field.(string)
|
||||
user.AvatarURL = p.User.AvatarURL
|
||||
}
|
||||
|
||||
func NewGiteaCallbackProvider(user *goth.User) CallbackProvider {
|
||||
|
||||
@@ -73,6 +73,7 @@ type Gist struct {
|
||||
URL string
|
||||
Preview string
|
||||
PreviewFilename string
|
||||
PreviewMimeType string
|
||||
Description string
|
||||
Private Visibility // 0: public, 1: unlisted, 2: private
|
||||
UserID uint
|
||||
@@ -551,6 +552,7 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
|
||||
if len(filesStr) == 0 {
|
||||
gist.Preview = ""
|
||||
gist.PreviewFilename = ""
|
||||
gist.PreviewMimeType = ""
|
||||
} else {
|
||||
for _, fileStr := range filesStr {
|
||||
file, err := gist.File("HEAD", fileStr, true)
|
||||
@@ -562,6 +564,7 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
|
||||
}
|
||||
gist.Preview = ""
|
||||
gist.PreviewFilename = file.Filename
|
||||
gist.PreviewMimeType = file.MimeType.ContentType
|
||||
|
||||
if !file.MimeType.CanBeEdited() {
|
||||
continue
|
||||
|
||||
@@ -2,43 +2,45 @@ package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
)
|
||||
|
||||
type MimeType struct {
|
||||
ContentType string
|
||||
extension string
|
||||
ContentType string
|
||||
extension string
|
||||
golangContentType string // json, m3u, etc. still renderable as text
|
||||
}
|
||||
|
||||
func (mt MimeType) IsText() bool {
|
||||
return strings.Contains(mt.ContentType, "text/")
|
||||
return strings.HasPrefix(mt.ContentType, "text/") || strings.HasPrefix(mt.golangContentType, "text/")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsCSV() bool {
|
||||
return strings.Contains(mt.ContentType, "text/csv") &&
|
||||
return strings.HasPrefix(mt.ContentType, "text/csv") &&
|
||||
(strings.HasSuffix(mt.extension, ".csv"))
|
||||
}
|
||||
|
||||
func (mt MimeType) IsImage() bool {
|
||||
return strings.Contains(mt.ContentType, "image/")
|
||||
return strings.HasPrefix(mt.ContentType, "image/")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsSVG() bool {
|
||||
return strings.Contains(mt.ContentType, "image/svg+xml")
|
||||
return strings.HasPrefix(mt.ContentType, "image/svg+xml")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsPDF() bool {
|
||||
return strings.Contains(mt.ContentType, "application/pdf")
|
||||
return strings.HasPrefix(mt.ContentType, "application/pdf")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsAudio() bool {
|
||||
return strings.Contains(mt.ContentType, "audio/")
|
||||
return strings.HasPrefix(mt.ContentType, "audio/")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsVideo() bool {
|
||||
return strings.Contains(mt.ContentType, "video/")
|
||||
return strings.HasPrefix(mt.ContentType, "video/")
|
||||
}
|
||||
|
||||
func (mt MimeType) CanBeHighlighted() bool {
|
||||
@@ -87,5 +89,5 @@ func (mt MimeType) RenderType() string {
|
||||
}
|
||||
|
||||
func DetectMimeType(data []byte, extension string) MimeType {
|
||||
return MimeType{mimetype.Detect(data).String(), extension}
|
||||
return MimeType{mimetype.Detect(data).String(), extension, http.DetectContentType(data)}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ gist.edit.save: Сохранить
|
||||
|
||||
gist.list.joined: Зарегистрирован
|
||||
gist.list.all: Все фрагменты
|
||||
gist.list.search-results: Результаты поиска
|
||||
gist.list.search-results: Результаты поиска
|
||||
gist.list.sort: Сортировка
|
||||
gist.list.sort-by-created: по дате создания
|
||||
gist.list.sort-by-updated: по дате обновления
|
||||
@@ -159,19 +159,19 @@ admin.created_at: Создан
|
||||
admin.config-link: Эти настройки могут быть %s файлом конфигурации YAML и/или переменными окружения.
|
||||
admin.config-link-overriden: перекрыты
|
||||
admin.disable-signup: Запретить регистрацию
|
||||
admin.disable-signup_help: Запретить создание новых доступов
|
||||
admin.disable-signup_help: Запретить создание новых доступов.
|
||||
admin.require-login: Требовать авторизацию
|
||||
admin.require-login_help: Запретить просмотр фрагментов без авторизации.
|
||||
admin.disable-login: Запретить авторизацию по паролю
|
||||
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
|
||||
admin.disable-gravatar: Запретить Gravatar
|
||||
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.allow-gists-without-login: Разрешить доступ к отдельным фрагментам без авторизации
|
||||
admin.allow-gists-without-login_help: Разрешает просматривать и скачивать отдельные фрагменты без входа, но требует авторизацию для поиска фрагментов.
|
||||
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
|
||||
|
||||
admin.gists.title: Название
|
||||
admin.gists.private: Приватный
|
||||
admin.gists.private: Приватный?
|
||||
admin.gists.nb-files: Файлов
|
||||
admin.gists.nb-likes: Понравилось
|
||||
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?
|
||||
@@ -183,7 +183,7 @@ gist.list.all-liked-by: 'Все фрагменты, понравившиеся %
|
||||
gist.list.all-forked-by: 'Все фрагменты, ответвлённые %s'
|
||||
gist.list.all-from: 'Все фрагменты от %s'
|
||||
gist.search.found: 'фрагментов найдено'
|
||||
gist.search.no-results: 'Не найден ни один фрагмент'
|
||||
gist.search.no-results: 'Фрагменты не найдены'
|
||||
gist.search.help.user: 'фрагментов создано пользователем'
|
||||
gist.search.help.title: 'фрагментов с указанным заголовком'
|
||||
gist.search.help.filename: 'фрагменты содержащие файлы с указанным именем'
|
||||
@@ -196,67 +196,67 @@ settings.link-gitlab-account: 'Привязать учётную запись Gi
|
||||
settings.unlink-gitlab-account: 'Отвязать учётную запись GitHub'
|
||||
settings.change-username: 'Сменить имя пользователя'
|
||||
settings.create-password: 'Создать пароль'
|
||||
settings.create-password-help: ''
|
||||
settings.change-password: ''
|
||||
settings.change-password-help: ''
|
||||
settings.password-label-title: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.actions.sync-previews: ''
|
||||
admin.actions.reset-hooks: ''
|
||||
admin.actions.index-gists: ''
|
||||
validation.should-not-be-empty: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
settings.create-password-help: 'Создайте пароль для входа в Opengist по HTTP'
|
||||
settings.change-password: 'Сменить пароль'
|
||||
settings.change-password-help: 'Смените пароль для входа в Opengist по HTTP'
|
||||
settings.password-label-title: 'Пароль'
|
||||
error.page-not-found: 'Страница не найдена'
|
||||
error.bad-request: 'Неверный запрос'
|
||||
error.signup-disabled: 'Регистрация недоступна'
|
||||
error.signup-disabled-form: 'Регистрация через форму недоступна'
|
||||
error.login-disabled-form: 'Авторизация через форму недоступна'
|
||||
error.complete-oauth-login: 'Не удалось завершить авторизацию пользователя: %s'
|
||||
error.oauth-unsupported: 'Провайдер OAuth не поддерживается'
|
||||
error.cannot-bind-data: 'Не удалось обработать данные'
|
||||
error.invalid-number: 'Некорректное числовое значение'
|
||||
error.invalid-character-unescaped: 'Обнаружен неверный неэкранированный символ'
|
||||
admin.invitations: 'Инвайты'
|
||||
admin.invitations.create: 'Создать инвайт'
|
||||
admin.actions.sync-previews: 'Обновить предпросмотры всех фрагментов'
|
||||
admin.actions.reset-hooks: 'Сбросить хуки Git-сервера для всех репозиториев'
|
||||
admin.actions.index-gists: 'Проиндексировать все фрагменты'
|
||||
validation.should-not-be-empty: 'Поле %s не должно быть пустым'
|
||||
admin.invitations.help: 'Инвайты используются для создания аккаунта, даже когда регистрация запрещена.'
|
||||
admin.invitations.max_uses: 'Максимальное количество использований'
|
||||
admin.invitations.expires_at: 'Истекает'
|
||||
admin.invitations.code: 'Код'
|
||||
admin.invitations.copy_link: 'Скопировать ссылку'
|
||||
admin.invitations.uses: 'Количество использований'
|
||||
admin.invitations.expired: 'Истёк'
|
||||
flash.admin.user-deleted: 'Пользователь удалён'
|
||||
flash.admin.gist-deleted: 'Фрагмент удалён'
|
||||
flash.admin.invitation-created: 'Приглашение создано'
|
||||
flash.admin.invitation-deleted: 'Приглашение удалено'
|
||||
flash.admin.sync-fs: 'Выполняется синхронизация репозиториев с файловой системой…'
|
||||
flash.admin.sync-db: 'Выполняется синхронизация репозиториев с базой данных…'
|
||||
flash.admin.git-gc: 'Сборка мусора в репозиториях…'
|
||||
flash.admin.sync-previews: 'Обновление предпросмотров фрагментов…'
|
||||
flash.admin.reset-hooks: 'Пересоздание Git-хуков для всех репозиториев…'
|
||||
flash.admin.index-gists: 'Выполняется индексация фрагментов…'
|
||||
flash.auth.username-exists: 'Такое имя пользователя уже занято'
|
||||
flash.auth.invalid-credentials: 'Некорректные данные для входа'
|
||||
flash.auth.account-linked-oauth: 'Учётная запись связана с %s'
|
||||
flash.auth.account-unlinked-oauth: 'Учётная запись отключена от %s'
|
||||
flash.auth.user-sshkeys-not-retrievable: 'Не удалось получить SSH-ключи пользователя'
|
||||
flash.auth.user-sshkeys-not-created: 'Не удалось создать SSH-ключ'
|
||||
flash.auth.must-be-logged-in: 'Для доступа к фрагментам необходимо войти в аккаунт'
|
||||
flash.gist.visibility-changed: 'Видимость фрагмента изменена'
|
||||
flash.gist.deleted: 'Фрагмент удалён'
|
||||
flash.gist.fork-own-gist: 'Нельзя создать форк собственного фрагмента'
|
||||
flash.gist.forked: 'Фрагмент создан как форк'
|
||||
flash.user.email-updated: 'Адрес электронной почты обновлён'
|
||||
flash.user.invalid-ssh-key: 'Неверный SSH-ключ'
|
||||
flash.user.ssh-key-added: 'SSH-ключ добавлен'
|
||||
flash.user.ssh-key-deleted: 'SSH-ключ удалён'
|
||||
flash.user.password-updated: 'Пароль обновлён'
|
||||
flash.user.username-updated: 'Имя пользователя обновлено'
|
||||
validation.is-too-long: 'Поле %s слишком длинное'
|
||||
validation.should-not-include-sub-directory: 'Поле %s не должно содержать подкаталоги'
|
||||
validation.should-only-contain-alphanumeric-characters: 'Поле %s должно содержать только буквы и цифры'
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: 'Поле %s должно содержать только буквы, цифры и дефисы'
|
||||
validation.not-enough: 'Недостаточно %s'
|
||||
validation.invalid: 'Неверный %s'
|
||||
html.title.admin-panel: 'Панель администратора'
|
||||
settings.ssh-key-exists: SSH-ключ уже существует
|
||||
gist.file-binary-edit: Этот файл является бинарным.
|
||||
gist.preview-non-available: Предпросмотр недоступен
|
||||
@@ -278,3 +278,80 @@ gist.new.any-file-type: Поддерживаются файлы любого т
|
||||
gist.delete.confirm: Вы уверены, что хотите удалить этот gist?
|
||||
gist.list.topic-results: Все фрагменты с этой темой
|
||||
gist.revision.binary-file-changes: Изменения в бинарных файлах не отображаются
|
||||
admin.actions.sync-gist-languages: Обновить языки всех фрагментов
|
||||
admin.invitations.delete_confirm: Вы хотите удалить это приглашение?
|
||||
flash.auth.passkey-deleted: Ключ доступа удалён
|
||||
flash.auth.passkey-registred: Ключ доступа %s зарегистрирован
|
||||
validation.invalid-gist-topics: 'Некорректные темы фрагмента: они должны начинаться с буквы или цифры, быть не длиннее 50 символов и могут содержать дефисы'
|
||||
settings.header.tokens: Токены доступа
|
||||
settings.style.removed-lines-color: Цвет удалённых строк
|
||||
settings.style.added-lines-color: Цвет добавленных строк
|
||||
settings.style.git-lines-color: Цвет git-строк
|
||||
settings.style.save-style: Сохранить оформление
|
||||
settings.create-token: Создать токен доступа
|
||||
settings.create-token-help: Токены доступа используются для доступа к API
|
||||
settings.token-name: Название
|
||||
settings.delete-token-confirm: Подтвердите удаление токена доступа
|
||||
settings.token-permissions: Права доступа
|
||||
settings.token-gist-permission: Фрагменты
|
||||
settings.token-permission-none: Нет доступа
|
||||
settings.token-permission-read: Чтение
|
||||
settings.token-permission-read-write: Чтение и запись
|
||||
settings.delete-token: Удалить
|
||||
settings.token-created-at: Создан
|
||||
settings.token-never-used: Не использовался
|
||||
settings.token-expiration: Срок действия
|
||||
settings.token-expiration-help: Оставьте пустым, чтобы срок действия не ограничивался
|
||||
settings.token-expires-at: Истекает
|
||||
settings.token-expired: истёк
|
||||
settings.token-deleted: Токен доступа удалён
|
||||
auth.mfa.delete-passkey-confirm: Подтвердите удаление ключа доступа
|
||||
auth.mfa.use-passkey: Использовать ключ доступа
|
||||
auth.mfa.bind-passkey: Добавить ключ доступа
|
||||
auth.mfa.login-with-passkey: Войти с помощью ключа доступа
|
||||
auth.mfa.waiting-for-passkey-input: Ожидание подтверждения в браузере…
|
||||
auth.mfa.use-passkey-to-finish: Используйте ключ доступа для завершения аутентификации
|
||||
auth.mfa.passkeys-help: Добавьте ключ доступа для входа в аккаунт и использования в качестве MFA.
|
||||
auth.mfa.passkey-name: Название
|
||||
auth.mfa.delete-passkey: Удалить
|
||||
auth.mfa.passkey-added-at: Добавлен
|
||||
auth.mfa.passkey-never-used: Никогда не использовался
|
||||
auth.mfa.passkey-last-used: Последнее использование
|
||||
auth.totp.already-enabled: TOTP уже включён
|
||||
auth.totp.invalid-secret: Некорректный секретный ключ TOTP
|
||||
auth.totp.invalid-code: Некорректный одноразовый код
|
||||
auth.totp.code-used: Код восстановления %s уже был использован и больше недействителен. Вы можете отключить MFA или сгенерировать новые коды.
|
||||
auth.totp.disabled: Двухфакторная аутентификация TOTP отключена
|
||||
auth.totp.disable: Отключить TOTP
|
||||
auth.totp.enter-code: Введите код из приложения Authenticator
|
||||
auth.totp.submit: Подтвердить
|
||||
auth.totp.proceed: Продолжить
|
||||
auth.totp.scan-qr-code: Отсканируйте QR-код ниже в приложении-аутентификаторе для включения двухфакторной аутентификации или введите указанную строку и подтвердите кодом.
|
||||
error.not-in-mfa-session: Пользователь не находится в MFA-сессии
|
||||
error.no-file-uploaded: Файл не загружен
|
||||
error.cannot-open-file: Не удалось открыть загруженный файл
|
||||
auth.totp.help: TOTP — это метод двухфакторной аутентификации, использующий общий секрет для генерации одноразового пароля.
|
||||
auth.totp.use: Использовать TOTP
|
||||
auth.totp.regenerate-recovery-codes: Сгенерировать коды восстановления заново
|
||||
auth.totp: Одноразовый пароль по времени (TOTP)
|
||||
flash.admin.sync-gist-languages: Обновление языков фрагментов…
|
||||
settings.token-created: Токен создан, обязательно сохраните его, повторно он показан не будет!
|
||||
settings.token-last-used: Последнее использование
|
||||
settings.token-no-expiration: Бессрочно
|
||||
auth.totp.save-recovery-codes: Сохраните коды восстановления в безопасном месте. Они понадобятся для восстановления доступа к аккаунту при утере приложения-аутентификатора.
|
||||
auth.totp.enter-recovery-key: или код восстановления, если вы потеряли устройство
|
||||
settings.style.theme: Тема
|
||||
settings.style.theme-light: Светлая тема
|
||||
settings.style.theme-dark: Тёмная тема
|
||||
settings.style.theme-auto: Авто
|
||||
auth.mfa: Регистрация отключена администратором
|
||||
auth.mfa.passkey: Вход
|
||||
auth.mfa.passkeys: Ключи доступа
|
||||
auth.totp.code: Код
|
||||
settings.header.account: Аккаунт
|
||||
settings.header.mfa: Двухфакторная аутентификация (MFA)
|
||||
settings.header.ssh: SSH
|
||||
settings.header.style: Тема оформления
|
||||
settings.style.gist-code: Код фрагмента
|
||||
settings.style.no-soft-wrap: Без переносов строк
|
||||
settings.style.soft-wrap: Перенос строк
|
||||
|
||||
@@ -27,8 +27,9 @@ func (r HighlightedFile) InternalType() string {
|
||||
|
||||
type RenderedGist struct {
|
||||
*db.Gist
|
||||
Lines []string
|
||||
HTML string
|
||||
Lines []string
|
||||
HTML string
|
||||
PreviewMimeType *git.MimeType
|
||||
}
|
||||
|
||||
func highlightFile(file *git.File) (HighlightedFile, error) {
|
||||
@@ -76,6 +77,18 @@ func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
|
||||
Gist: gist,
|
||||
}
|
||||
|
||||
if gist.PreviewMimeType != "" {
|
||||
mt := &git.MimeType{ContentType: gist.PreviewMimeType}
|
||||
if mt.CanBeEmbedded() {
|
||||
rendered.PreviewMimeType = mt
|
||||
return rendered, nil
|
||||
}
|
||||
}
|
||||
|
||||
if gist.Preview == "" {
|
||||
return rendered, nil
|
||||
}
|
||||
|
||||
style := newStyle()
|
||||
lexer := newLexer(gist.PreviewFilename)
|
||||
if lexer.Config().Name == "markdown" {
|
||||
|
||||
@@ -3,6 +3,7 @@ package gist
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
@@ -19,8 +20,23 @@ func RawFile(ctx *context.Context) error {
|
||||
if file == nil {
|
||||
return ctx.NotFound("File not found")
|
||||
}
|
||||
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
|
||||
ctx.Response().Header().Set("Content-Disposition", "inline; filename=\""+file.Filename+"\"")
|
||||
|
||||
if file.MimeType.IsSVG() {
|
||||
ctx.Response().Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
} else if file.MimeType.IsPDF() {
|
||||
ctx.Response().Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
||||
}
|
||||
|
||||
if file.MimeType.CanBeEmbedded() {
|
||||
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
|
||||
} else if file.MimeType.IsText() {
|
||||
ctx.Response().Header().Set("Content-Type", "text/plain")
|
||||
} else {
|
||||
ctx.Response().Header().Set("Content-Type", "application/octet-stream")
|
||||
}
|
||||
|
||||
ctx.Response().Header().Set("Content-Disposition", "inline; filename=\""+url.PathEscape(file.Filename)+"\"")
|
||||
ctx.Response().Header().Set("X-Content-Type-Options", "nosniff")
|
||||
return ctx.PlainText(200, file.Content)
|
||||
}
|
||||
|
||||
@@ -36,8 +52,9 @@ func DownloadFile(ctx *context.Context) error {
|
||||
}
|
||||
|
||||
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename=\""+url.PathEscape(file.Filename)+"\"")
|
||||
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
|
||||
ctx.Response().Header().Set("X-Content-Type-Options", "nosniff")
|
||||
_, err = ctx.Response().Write([]byte(file.Content))
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error downloading the file", err)
|
||||
|
||||
@@ -206,6 +206,9 @@ func makeCheckRequireLogin(isSingleGistAccess bool) Middleware {
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
if getUserByToken(ctx) != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(handlers.ContextAuthInfo{Context: ctx}, isSingleGistAccess)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
|
||||
@@ -354,7 +357,6 @@ func getUserByToken(ctx *context.Context) *db.User {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update last used timestamp
|
||||
_ = accessToken.UpdateLastUsed()
|
||||
|
||||
return &accessToken.User
|
||||
|
||||
@@ -359,3 +359,90 @@ func TestAccessTokenLastUsedUpdate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, int64(0), tokenFromDB.LastUsedAt)
|
||||
}
|
||||
|
||||
func TestAccessTokenWithRequireLogin(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
admin := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, admin)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "private-gist",
|
||||
Description: "my private gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"file.txt"},
|
||||
Content: []string{"content"},
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "public-gist",
|
||||
Description: "my public gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PublicVisibility,
|
||||
},
|
||||
Name: []string{"public.txt"},
|
||||
Content: []string{"public content"},
|
||||
}
|
||||
err = s.Request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
token := &db.AccessToken{
|
||||
Name: "read-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
plainToken, err := token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = token.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
err = s.Request("GET", "/thomas/"+gist1db.Uuid, nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/thomas/"+gist2db.Uuid, nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
headers := map[string]string{"Authorization": "Token " + plainToken}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist2db.Uuid, nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid+"/raw/HEAD/file.txt", nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidHeaders := map[string]string{"Authorization": "Token invalid_token"}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 302, invalidHeaders)
|
||||
require.NoError(t, err)
|
||||
|
||||
noPermToken := &db.AccessToken{
|
||||
Name: "no-perm-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.NoPermission,
|
||||
}
|
||||
noPermPlain, err := noPermToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = noPermToken.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
noPermHeaders := map[string]string{"Authorization": "Token " + noPermPlain}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 302, noPermHeaders)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import '../ts/ipynb.ts';
|
||||
import PDFObject from 'pdfobject';
|
||||
|
||||
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
|
||||
el.addEventListener('click', event => {
|
||||
@@ -76,8 +75,4 @@ if (document.getElementById('gist').dataset.own) {
|
||||
checkboxes.forEach((el: HTMLButtonElement) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".pdf").forEach((el) => {
|
||||
PDFObject.embed(el.dataset.src || "", el);
|
||||
})
|
||||
}
|
||||
@@ -2,9 +2,14 @@ import '../css/tailwind.css';
|
||||
import '../img/favicon-32.png';
|
||||
import '../img/opengist.svg';
|
||||
import jdenticon from 'jdenticon/standalone';
|
||||
import PDFObject from 'pdfobject';
|
||||
|
||||
jdenticon.update("[data-jdenticon-value]")
|
||||
|
||||
document.querySelectorAll(".pdf").forEach((el) => {
|
||||
PDFObject.embed(el.dataset.src || "", el);
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('user-btn')?.addEventListener("click" , () => {
|
||||
document.getElementById('user-menu')!.classList.toggle('hidden');
|
||||
|
||||
32
templates/partials/_gist_preview.html
vendored
32
templates/partials/_gist_preview.html
vendored
@@ -60,7 +60,33 @@
|
||||
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto hover:border-primary-600">
|
||||
<div class="code overflow-auto">
|
||||
{{ if .gist.PreviewFilename }}
|
||||
{{ if .gist.Preview }}
|
||||
{{ if .gist.PreviewMimeType }}
|
||||
{{ if .gist.PreviewMimeType.IsSVG }}
|
||||
<div class="p-8 flex justify-center">
|
||||
<img src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}" alt="{{ .gist.PreviewFilename }}" class="max-h-80 object-contain">
|
||||
</div>
|
||||
{{ else if .gist.PreviewMimeType.IsImage }}
|
||||
<div class="p-8 flex justify-center">
|
||||
<img src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}" alt="{{ .gist.PreviewFilename }}" class="max-h-80 object-contain">
|
||||
</div>
|
||||
{{ else if .gist.PreviewMimeType.IsAudio }}
|
||||
<div class="p-8 flex justify-center">
|
||||
<audio controls class="w-full max-w-lg">
|
||||
<source src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}" type="{{ .gist.PreviewMimeType.ContentType }}">
|
||||
</audio>
|
||||
</div>
|
||||
{{ else if .gist.PreviewMimeType.IsVideo }}
|
||||
<div class="p-8 flex justify-center">
|
||||
<video controls class="max-h-80 max-w-full">
|
||||
<source src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}" type="{{ .gist.PreviewMimeType.ContentType }}">
|
||||
</video>
|
||||
</div>
|
||||
{{ else if .gist.PreviewMimeType.IsPDF }}
|
||||
<div class="pdf max-h-80" data-src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}"></div>
|
||||
{{ else }}
|
||||
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.preview-non-available" }}</p></div>
|
||||
{{ end }}
|
||||
{{ else if .gist.Preview }}
|
||||
{{ if isMarkdown .gist.PreviewFilename }}
|
||||
<div class="chroma preview markdown markdown-body p-8">{{ .gist.HTML | safe }}</div>
|
||||
{{ else }}
|
||||
@@ -80,8 +106,8 @@
|
||||
</table>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.preview-non-available" }}</p></div>
|
||||
{{ end }}
|
||||
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.preview-non-available" }}</p></div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.no-content" }}</p></div>
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user