Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b9eb8e127 | ||
|
|
05523f6bb1 | ||
|
|
30ca090e74 | ||
|
|
fa8e068e24 | ||
|
|
72275e7573 | ||
|
|
5b278e2e86 | ||
|
|
6c450c6f3b | ||
|
|
dd050bb6a0 | ||
|
|
c7a6b05c6d | ||
|
|
35297a287a | ||
|
|
85b51bf3c9 | ||
|
|
9dff67f003 | ||
|
|
a5ea522e45 | ||
|
|
61e274e56d | ||
|
|
c20ed60913 | ||
|
|
b31d95c7f6 | ||
|
|
689fd21afa | ||
|
|
be3580f7b1 | ||
|
|
6085471b81 | ||
|
|
3943b53163 | ||
|
|
fe674ac88b | ||
|
|
9c29e86222 | ||
|
|
933ba2da0d | ||
|
|
4d0b75ed0e | ||
|
|
1dcb900cf3 | ||
|
|
46dea89b41 | ||
|
|
977fc9db28 | ||
|
|
3e83700fc2 | ||
|
|
0d7305d9ba | ||
|
|
d4eed91130 | ||
|
|
ffafde2b3e | ||
|
|
a7b346d8df | ||
|
|
25316d7bf2 | ||
|
|
4f623881ac | ||
|
|
b5cd49db4c | ||
|
|
89685bfac6 | ||
|
|
319a89387a | ||
|
|
24fc6dd8e4 | ||
|
|
cc6110bb4e | ||
|
|
2890c60124 | ||
|
|
5bb5886770 | ||
|
|
038d81df2d | ||
|
|
7515e82d34 | ||
|
|
add0299442 | ||
|
|
06b752f567 | ||
|
|
a35b64455a | ||
|
|
9470622125 | ||
|
|
7b5d035a32 | ||
|
|
98c5cd1794 | ||
|
|
0936cbc455 | ||
|
|
fc421a68b5 | ||
|
|
62711ff491 | ||
|
|
da19e486f2 | ||
|
|
98c85de3d6 | ||
|
|
af3aab21e3 | ||
|
|
fb407bcbce | ||
|
|
3366cde385 | ||
|
|
b2a56fe5a0 | ||
|
|
24e3de8fc1 | ||
|
|
c517c2d9c9 | ||
|
|
8880e00f48 | ||
|
|
da970d7272 | ||
|
|
62f91c5ed2 | ||
|
|
0a542431c9 | ||
|
|
cecc06b332 | ||
|
|
4a75a50370 | ||
|
|
7cc77d80dc | ||
|
|
df22506bdd | ||
|
|
026bb7304c | ||
|
|
0cae152e03 | ||
|
|
5fe84164a5 | ||
|
|
089d321898 | ||
|
|
1f74affde4 | ||
|
|
49807d04c7 | ||
|
|
67a06cea0a | ||
|
|
d866a6f2f2 | ||
|
|
3cf5bc8b76 | ||
|
|
2782655545 | ||
|
|
0362cd8a6a | ||
|
|
58d40e211f | ||
|
|
713b5d623e | ||
|
|
5040a2fe3c | ||
|
|
5ba90af04c | ||
|
|
c45b418bd0 | ||
|
|
2b3043ad5d | ||
|
|
333efeacbf | ||
|
|
64d0818c9f | ||
|
|
fe19e64dd4 | ||
|
|
91db4dcd30 | ||
|
|
09507500a9 | ||
|
|
14dac703c8 | ||
|
|
fddc50e3cc | ||
|
|
5bc1697774 | ||
|
|
7807110845 | ||
|
|
c7866e18e4 | ||
|
|
4c43f1aa8a | ||
|
|
485f209319 | ||
|
|
9d20465eb9 | ||
|
|
884195aed4 | ||
|
|
6a0fd92516 | ||
|
|
2d42cf798c | ||
|
|
4008b7ce38 | ||
|
|
a6c5696ceb | ||
|
|
47cbf5e7ef | ||
|
|
8534a4880b | ||
|
|
dba3f4be44 | ||
|
|
dbdd38694d | ||
|
|
19db4c223b | ||
|
|
31aae8a305 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
||||
templates/**/* linguist-vendored
|
||||
public/**/*.css linguist-vendored
|
||||
public/**/*.scss linguist-vendored
|
||||
*.config.js linguist-vendored
|
||||
|
||||
63
.github/workflows/go.yml
vendored
Normal file
63
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: "Go CI"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 'dev-*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.20
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.54
|
||||
skip-pkg-cache: true
|
||||
args: --out-format=colored-line-number --timeout=20m
|
||||
|
||||
- name: Format
|
||||
run: make fmt check_changes
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.20
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Check
|
||||
run: make go_mod check_changes
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||
go: ["1.20", "1.21"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Run tests
|
||||
run: make test
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
name: Docker
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
binaries-build-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.20
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Cross compile build
|
||||
run: make all_crosscompile
|
||||
|
||||
- name: Upload Release Assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
build/*.tar.gz
|
||||
build/*.zip
|
||||
build/checksums.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker-build-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ gist.db
|
||||
public/assets/*
|
||||
public/manifest.json
|
||||
opengist
|
||||
build/
|
||||
|
||||
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,9 +1,130 @@
|
||||
# Changelog
|
||||
|
||||
## [1.0.1](https://github.com/thomiceli/opengist/compare/v1.0.0...v1.0.1) - [2023-04-12]
|
||||
## [1.5.0](https://github.com/thomiceli/opengist/compare/v1.4.2...v1.5.0) - 2023-09-26
|
||||
### Added
|
||||
- Private Gist visibility (#87)
|
||||
- Create gists from a special Git HTTP server remote URL (#95)
|
||||
- OIDC provider integration (#98)
|
||||
- Translation system (#104)
|
||||
- Run `git gc` on all repositories as admin (#90)
|
||||
- Unit and integration tests (#97)
|
||||
- Documentation (#110, #111)
|
||||
- New logo (#103)
|
||||
|
||||
### Changed
|
||||
- Use Non-CGO SQLite instead of CGO SQLite (#100)
|
||||
- Various UI changes (#84, #93)
|
||||
- Improved CI/CD pipeline (#99, #113)
|
||||
- Improved git http semantics and repo obfuscation (#94)
|
||||
- Updated Go deps (#102)
|
||||
|
||||
### Fixed
|
||||
- Find command for Windows users (#89)
|
||||
- Retain visibility when editing a gist (#83)
|
||||
- Typo on admin index page (#85)
|
||||
- ViteJS dev server (#91)
|
||||
- Bugs (#105)
|
||||
|
||||
### Breaking changes
|
||||
- Removed CONFIG env var
|
||||
- Removed TLS server (#101)
|
||||
|
||||
## [1.4.2](https://github.com/thomiceli/opengist/compare/v1.4.1...v1.4.2) - 2023-07-17
|
||||
### Added
|
||||
- External url to HTML links & redirects (#75)
|
||||
- Make unlisted gists not SEO crawlable (#78)
|
||||
- Warning message on OAuth unlink (#79)
|
||||
|
||||
### Changed
|
||||
- Redirect to `/all` when not logged in (#76)
|
||||
- Removed Dev Docker image (#80)
|
||||
|
||||
## [1.4.1](https://github.com/thomiceli/opengist/compare/v1.4.0...v1.4.1) - 2023-06-25
|
||||
### ⚠️ Docker users ⚠️
|
||||
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
|
||||
`docker-compose.yml` file or any other Docker related configuration.
|
||||
|
||||
Please make a backup of your Opengist data directory before updating.
|
||||
|
||||
### Fixed
|
||||
- Git message remote: `warning: unable to access '/root/.config/git/attributes': Permission denied` (#71)
|
||||
|
||||
## [1.4.0](https://github.com/thomiceli/opengist/compare/v1.3.0...v1.4.0) - 2023-06-23
|
||||
### ⚠️ Docker users ⚠️
|
||||
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
|
||||
`docker-compose.yml` file or any other Docker related configuration.
|
||||
|
||||
Please make a backup of your Opengist data directory before updating.
|
||||
|
||||
### Added
|
||||
- Search gists, browse users snippets, likes and forks (#68)
|
||||
- SQLite WAL journal mode by default (#54)
|
||||
- Change SQLite journal mode via configuration (#54)
|
||||
- Configuration via environment variables (#50)
|
||||
- Docker dev image (#56)
|
||||
- Choose Docker container/volumes owner via UID/GID (#63)
|
||||
|
||||
### Changed
|
||||
- Docker volume changed from `/root/.opengist` to `/opengist` (#63)
|
||||
- `DEV` environment variable renamed to `OG_DEV` (#64)
|
||||
- Use `npx` in Makefile instead of `./node_modules/.bin` (#66)
|
||||
- DEPRECATED: `OG_CONFIG` environment variable (#64)
|
||||
|
||||
### Fixed
|
||||
- Gitea URL joins (#43, #61)
|
||||
- Dark mode flickering (#44)
|
||||
- Typos (#42)
|
||||
|
||||
## [1.3.0](https://github.com/thomiceli/opengist/compare/v1.2.0...v1.3.0) - 2023-05-27
|
||||
### Added
|
||||
- Disable login form via admin panel
|
||||
- Syntax highlighting in Markdown code block (#29)
|
||||
- Better UI for admin settings (#30)
|
||||
- Disable Gravatar (#37)
|
||||
- Swap between dark and light theme (#38)
|
||||
|
||||
### Changed
|
||||
- Logs are now also appended to stdout
|
||||
- Golang module name is now `github.com/thomiceli/opengist`
|
||||
|
||||
### Fixed
|
||||
- First account registering with OAuth is now admin
|
||||
- Fix HTML entities escaping in Markdown code block (#29)
|
||||
|
||||
## [1.2.0](https://github.com/thomiceli/opengist/compare/v1.1.1...v1.2.0) - 2023-05-01
|
||||
### Added
|
||||
- Restrict or unrestrict snippets visibility to anonymous users (#19)
|
||||
- Go CI with Staticcheck
|
||||
|
||||
### Changed
|
||||
- Filenames are now trimmed when creating a snippet (#20)
|
||||
- SSH public key comments are now trimmed when adding a new key (#22)
|
||||
|
||||
### Fixed
|
||||
- Respect ExternalUrl for OAuth (#21)
|
||||
- SSH public key detection (#22)
|
||||
|
||||
## [1.1.1](https://github.com/thomiceli/opengist/compare/v1.1.0...v1.1.1) - 2023-04-20
|
||||
### Fixed
|
||||
- Git processes are now correctly killed
|
||||
|
||||
## [1.1.0](https://github.com/thomiceli/opengist/compare/v1.0.1...v1.1.0) - 2023-04-18
|
||||
### Added
|
||||
- GitHub and Gitea OAuth2 login
|
||||
- Database migration system
|
||||
|
||||
### Changed
|
||||
- Admin panel route from `/admin` route to `/admin-panel`
|
||||
- Moved disable signup option to admin panel
|
||||
|
||||
### Fixed
|
||||
- Truncate raw file (#4)
|
||||
- Fix SSH key table constraints on user delete
|
||||
|
||||
## [1.0.1](https://github.com/thomiceli/opengist/compare/v1.0.0...v1.0.1) - 2023-04-12
|
||||
### Changed
|
||||
- Updated base footer
|
||||
- Changed redirections when not logged in
|
||||
|
||||
## 1.0.0 - [2023-04-10]
|
||||
## 1.0.0 - 2023-04-10
|
||||
- Initial release
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@@ -7,7 +7,7 @@ RUN apk update && \
|
||||
musl-dev \
|
||||
libstdc++
|
||||
|
||||
COPY --from=golang:1.19-alpine /usr/local/go/ /usr/local/go/
|
||||
COPY --from=golang:1.20-alpine /usr/local/go/ /usr/local/go/
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
|
||||
COPY --from=node:18-alpine /usr/local/ /usr/local/
|
||||
@@ -21,10 +21,11 @@ COPY . .
|
||||
RUN make
|
||||
|
||||
|
||||
FROM alpine:3.17
|
||||
FROM alpine:3.17 as run
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
shadow \
|
||||
openssl \
|
||||
openssh \
|
||||
curl \
|
||||
@@ -36,10 +37,14 @@ RUN apk update && \
|
||||
musl-dev \
|
||||
libstdc++
|
||||
|
||||
WORKDIR /opengist
|
||||
RUN addgroup -S opengist && \
|
||||
adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' opengist
|
||||
|
||||
COPY --from=build /opengist/opengist .
|
||||
WORKDIR /app/opengist
|
||||
|
||||
COPY --from=build --chown=opengist:opengist /opengist/opengist .
|
||||
COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
|
||||
|
||||
EXPOSE 6157 2222
|
||||
VOLUME /root/.opengist
|
||||
CMD ["./opengist"]
|
||||
VOLUME /opengist
|
||||
ENTRYPOINT ["./docker/entrypoint.sh"]
|
||||
|
||||
33
Makefile
33
Makefile
@@ -1,9 +1,11 @@
|
||||
.PHONY: all install build_frontend build_backend build build_docker watch_frontend watch_backend watch clean clean_docker
|
||||
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test
|
||||
|
||||
# Specify the name of your Go binary output
|
||||
BINARY_NAME := opengist
|
||||
|
||||
all: install build
|
||||
all: clean install build
|
||||
|
||||
all_crosscompile: clean install build_frontend build_crosscompile
|
||||
|
||||
install:
|
||||
@echo "Installing NPM dependencies..."
|
||||
@@ -13,7 +15,7 @@ install:
|
||||
|
||||
build_frontend:
|
||||
@echo "Building frontend assets..."
|
||||
./node_modules/.bin/vite build
|
||||
npx vite build
|
||||
|
||||
build_backend:
|
||||
@echo "Building Opengist binary..."
|
||||
@@ -21,26 +23,43 @@ build_backend:
|
||||
|
||||
build: build_frontend build_backend
|
||||
|
||||
build_crosscompile:
|
||||
@bash ./scripts/build-all.sh
|
||||
|
||||
build_docker:
|
||||
@echo "Building Docker image..."
|
||||
docker build -t $(BINARY_NAME):latest .
|
||||
|
||||
watch_frontend:
|
||||
@echo "Building frontend assets..."
|
||||
./node_modules/.bin/vite dev --port 16157
|
||||
npx vite dev --port 16157
|
||||
|
||||
watch_backend:
|
||||
@echo "Building Opengist binary..."
|
||||
DEV=1 ./node_modules/.bin/nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run . --config config.yml'
|
||||
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run . --config config.yml'
|
||||
|
||||
watch:
|
||||
@bash ./watch.sh
|
||||
@bash ./scripts/watch.sh
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up build artifacts..."
|
||||
@rm -f $(BINARY_NAME) public/manifest.json
|
||||
@rm -rf public/assets
|
||||
@rm -rf public/assets build
|
||||
|
||||
clean_docker:
|
||||
@echo "Cleaning up Docker image..."
|
||||
@docker rmi $(BINARY_NAME)
|
||||
|
||||
check_changes:
|
||||
@echo "Checking for changes..."
|
||||
@git --no-pager diff --exit-code || (echo "There are unstaged changes detected." && exit 1)
|
||||
|
||||
go_mod:
|
||||
@go mod download
|
||||
@go mod tidy
|
||||
|
||||
fmt:
|
||||
@go fmt ./...
|
||||
|
||||
test:
|
||||
@go test ./... -p 1
|
||||
|
||||
164
README.md
164
README.md
@@ -1,54 +1,41 @@
|
||||
# Opengist
|
||||
|
||||
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/a9dd531f676d01b93bb6bd70751a69382ca563b0/public/opengist.svg" alt="Opengist" align="right" />
|
||||
|
||||
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
||||
read and/or modified using standard Git commands, or with the web interface.
|
||||
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
|
||||
|
||||
[Documentation](/docs) • [Demo](https://opengist.thomice.li)
|
||||
|
||||
|
||||

|
||||

|
||||
[](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
|
||||
[](https://goreportcard.com/report/github.com/thomiceli/opengist)
|
||||
|
||||
A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomice.li).
|
||||
|
||||
* [Features](#features)
|
||||
* [Install](#install)
|
||||
* [With Docker](#with-docker)
|
||||
* [From source](#from-source)
|
||||
* [Configuration](#configuration)
|
||||
* [Administration](#administration)
|
||||
* [Use Nginx as a reverse proxy](#use-nginx-as-a-reverse-proxy)
|
||||
* [Use Fail2ban](#use-fail2ban)
|
||||
* [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
* Create public or unlisted snippets
|
||||
* Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Create public, unlisted or private snippets
|
||||
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Revisions history
|
||||
* Syntax highlighting ; markdown & CSV support
|
||||
* Like / Fork snippets
|
||||
* Search for all snippets or for certain users snippets
|
||||
* Editor with indentation mode & size ; drag and drop files
|
||||
* Search for snippets ; browse users snippets, likes and forks
|
||||
* Download raw files or as a ZIP archive
|
||||
* Avatars
|
||||
* Responsive UI
|
||||
* Enable or disable signups
|
||||
* Admin panel : delete users/gists; clean database/filesystem by syncing gists
|
||||
* SQLite database
|
||||
* Logging
|
||||
* OAuth2 login with GitHub, Gitea, and OpenID Connect
|
||||
* Restrict or unrestrict snippets visibility to anonymous users
|
||||
* Docker support
|
||||
* [More...](/docs/index.md#features)
|
||||
|
||||
#### Todo
|
||||
|
||||
- [ ] Light mode
|
||||
- [ ] Tests
|
||||
- [ ] Search for snippets
|
||||
- [ ] Embed snippets
|
||||
- [ ] Filesystem/Redis support for user sessions
|
||||
- [ ] Have a cool logo
|
||||
|
||||
## Install
|
||||
## Quick start
|
||||
|
||||
### With Docker
|
||||
|
||||
A Docker [image](https://github.com/users/thomiceli/packages/container/package/opengist), available for each release, can be pulled
|
||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
||||
|
||||
```
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1
|
||||
```
|
||||
|
||||
@@ -70,15 +57,37 @@ services:
|
||||
- "6157:6157" # HTTP port
|
||||
- "2222:2222" # SSH port, can be removed if you don't use SSH
|
||||
volumes:
|
||||
- "$HOME/.opengist:/root/.opengist"
|
||||
- "$HOME/.opengist:/opengist"
|
||||
```
|
||||
|
||||
You can define which user/group should run the container and own the files by setting the `UID` and `GID` environment variables :
|
||||
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
CONFIG: |
|
||||
log-level: info
|
||||
UID: 1001
|
||||
GID: 1001
|
||||
```
|
||||
|
||||
### Via binary
|
||||
|
||||
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.5.0/opengist1.5.0-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.5.0-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
Requirements : [Git](https://git-scm.com/downloads) (2.20+), [Go](https://go.dev/doc/install) (1.19+), [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
Requirements : [Git](https://git-scm.com/downloads) (2.20+), [Go](https://go.dev/doc/install) (1.20+), [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
|
||||
```shell
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
@@ -89,87 +98,12 @@ make
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
## Configuration
|
||||
|
||||
Opengist can be configured using YAML. The full configuration file is [config.yml](config.yml), each default key/value
|
||||
pair can be overridden.
|
||||
## Documentation
|
||||
|
||||
### With docker
|
||||
|
||||
Add a `CONFIG` environment variable in the `docker-compose.yml` file to the `opengist` service :
|
||||
|
||||
```diff
|
||||
environment:
|
||||
CONFIG: |
|
||||
log-level: info
|
||||
ssh.git-enabled: false
|
||||
disable-signup: true
|
||||
# ...
|
||||
```
|
||||
|
||||
### With binary
|
||||
|
||||
Create a `config.yml` file (you can reuse this [one](config.yml)) and run Opengist binary with the `--config` flag :
|
||||
|
||||
```shell
|
||||
./opengist --config /path/to/config.yml
|
||||
```
|
||||
The documentation is available in [/docs](/docs) directory.
|
||||
|
||||
|
||||
## Administration
|
||||
|
||||
### Use Nginx as a reverse proxy
|
||||
|
||||
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name opengist.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:6157;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run :
|
||||
```shell
|
||||
service nginx restart
|
||||
```
|
||||
|
||||
### Use Fail2ban
|
||||
|
||||
Fail2ban can be used to ban IPs that try to bruteforce the login page.
|
||||
Log level must be set at least to `warn`.
|
||||
|
||||
Add this filter in `etc/fail2ban/filter.d/opengist.conf` :
|
||||
```ini
|
||||
[Definition]
|
||||
failregex = Invalid .* authentication attempt from <HOST>
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
Add this jail in `etc/fail2ban/jail.d/opengist.conf` :
|
||||
```ini
|
||||
[opengist]
|
||||
enabled = true
|
||||
filter = opengist
|
||||
logpath = /home/*/.opengist/log/opengist.log
|
||||
maxretry = 10
|
||||
findtime = 3600
|
||||
bantime = 600
|
||||
banaction = iptables-allports
|
||||
port = anyport
|
||||
```
|
||||
|
||||
Then run
|
||||
```shell
|
||||
service fail2ban restart
|
||||
```
|
||||
## License
|
||||
|
||||
Opengist is licensed under the [AGPL-3.0 license](LICENSE).
|
||||
Opengist is licensed under the [AGPL-3.0 license](/LICENSE).
|
||||
|
||||
40
config.yml
40
config.yml
@@ -1,3 +1,7 @@
|
||||
# Learn more about Opengist configuration here:
|
||||
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/index.md
|
||||
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
|
||||
|
||||
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
|
||||
log-level: warn
|
||||
|
||||
@@ -5,15 +9,16 @@ log-level: warn
|
||||
# If not set, uses the URL from the request
|
||||
external-url:
|
||||
|
||||
# Prevents the creation of new accounts (either `true` or `false`). Default: false
|
||||
disable-signup: false
|
||||
|
||||
# Directory where Opengist will store its data. Default: ~/.opengist/
|
||||
opengist-home:
|
||||
|
||||
# Name of the SQLite database file. Default: opengist.db
|
||||
db-filename: opengist.db
|
||||
|
||||
# Set the journal mode for SQLite. Default: WAL
|
||||
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||
sqlite.journal-mode: WAL
|
||||
|
||||
|
||||
# HTTP server configuration
|
||||
# Host to bind to. Default: 0.0.0.0
|
||||
@@ -25,15 +30,6 @@ http.port: 6157
|
||||
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
|
||||
http.git-enabled: true
|
||||
|
||||
# Enable or disable TLS (either `true` or `false`). Default: false
|
||||
http.tls-enabled: false
|
||||
|
||||
# Path to the TLS certificate file if TLS is enabled
|
||||
http.cert-file:
|
||||
|
||||
# Path to the TLS key file if TLS is enabled
|
||||
http.key-file:
|
||||
|
||||
# SSH built-in server configuration
|
||||
# Note: it is not using the SSH daemon from your machine (yet)
|
||||
|
||||
@@ -56,3 +52,23 @@ ssh.external-domain:
|
||||
|
||||
# Path or alias to ssh-keygen executable. Default: ssh-keygen
|
||||
ssh.keygen-executable: ssh-keygen
|
||||
|
||||
|
||||
# OAuth2 configuration
|
||||
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea|openid-connect>/callback
|
||||
|
||||
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
|
||||
github.client-key:
|
||||
github.secret:
|
||||
|
||||
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
|
||||
gitea.client-key:
|
||||
gitea.secret:
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
gitea.url: https://gitea.com/
|
||||
|
||||
# To create a new OAuth2 application using OpenID Connect:
|
||||
oidc.client-key:
|
||||
oidc.secret:
|
||||
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
||||
oidc.discovery-url:
|
||||
|
||||
11
docker/entrypoint.sh
Executable file
11
docker/entrypoint.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
export USER=opengist
|
||||
UID=${UID:-1000}
|
||||
GID=${GID:-1000}
|
||||
groupmod -o -g "$GID" $USER
|
||||
usermod -o -u "$UID" $USER
|
||||
|
||||
chown -R "$USER:$USER" /opengist
|
||||
|
||||
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist"
|
||||
29
docs/administration/fail2ban-setup.md
Normal file
29
docs/administration/fail2ban-setup.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Fail2ban setup
|
||||
|
||||
Fail2ban can be used to ban IPs that try to bruteforce the login page.
|
||||
Log level must be set at least to `warn`.
|
||||
|
||||
Add this filter in `etc/fail2ban/filter.d/opengist.conf` :
|
||||
```ini
|
||||
[Definition]
|
||||
failregex = Invalid .* authentication attempt from <HOST>
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
Add this jail in `etc/fail2ban/jail.d/opengist.conf` :
|
||||
```ini
|
||||
[opengist]
|
||||
enabled = true
|
||||
filter = opengist
|
||||
logpath = /home/*/.opengist/log/opengist.log
|
||||
maxretry = 10
|
||||
findtime = 3600
|
||||
bantime = 600
|
||||
banaction = iptables-allports
|
||||
port = anyport
|
||||
```
|
||||
|
||||
Then run
|
||||
```shell
|
||||
service fail2ban restart
|
||||
```
|
||||
22
docs/administration/nginx-reverse-proxy.md
Normal file
22
docs/administration/nginx-reverse-proxy.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Use Nginx as a reverse proxy
|
||||
|
||||
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name opengist.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:6157;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run :
|
||||
```shell
|
||||
service nginx restart
|
||||
```
|
||||
39
docs/administration/oauth-providers.md
Normal file
39
docs/administration/oauth-providers.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Use OAuth providers
|
||||
|
||||
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
|
||||
|
||||
## Github
|
||||
|
||||
* Add a new OAuth app in your [Github account settings](https://github.com/settings/applications/new)
|
||||
* Set 'Authorization callback URL' to `http://opengist.domain/oauth/github/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
||||
```yaml
|
||||
github.client-key: <key>
|
||||
github.secret: <secret>
|
||||
```
|
||||
|
||||
|
||||
## Gitea
|
||||
|
||||
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
|
||||
* Set 'Redirect URI' to `http://opengist.domain/oauth/gitea/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
||||
```yaml
|
||||
gitea.client-key: <key>
|
||||
gitea.secret: <secret>
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
gitea.url: http://localhost:3000
|
||||
```
|
||||
|
||||
|
||||
## OpenID Connect
|
||||
|
||||
* Add a new OAuth app in Application settings of your OIDC provider
|
||||
* Set 'Redirect URI' to `http://opengist.domain/oauth/openid-connect/callback`
|
||||
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
||||
```yaml
|
||||
oidc.client-key: <key>
|
||||
oidc.secret: <secret>
|
||||
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
||||
oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration
|
||||
```
|
||||
47
docs/administration/run-with-systemd.md
Normal file
47
docs/administration/run-with-systemd.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Run with Systemd
|
||||
|
||||
For non-Docker users, you could run Opengist as a systemd service.
|
||||
|
||||
On Unix distributions with systemd, place the Opengist binary like:
|
||||
|
||||
```shell
|
||||
sudo cp opengist /usr/local/bin
|
||||
sudo mkdir -p /var/lib/opengist
|
||||
sudo cp config.yml /etc/opengist
|
||||
```
|
||||
|
||||
Edit the Opengist home directory configuration in `/etc/opengist/config.yml` like:
|
||||
```shell
|
||||
opengist-home: /var/lib/opengist
|
||||
```
|
||||
|
||||
Create a new user to run Opengist:
|
||||
```shell
|
||||
sudo useradd --system opengist
|
||||
sudo mkdir -p /var/lib/opengist
|
||||
sudo chown -R opengist:opengist /var/lib/opengist
|
||||
```
|
||||
|
||||
Then create a service file at `/etc/systemd/system/opengist.service`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=opengist Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=opengist
|
||||
Group=opengist
|
||||
ExecStart=opengist --config /etc/opengist/config.yml
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Finally, start the service:
|
||||
```shell
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now opengist
|
||||
systemctl status opengist
|
||||
```
|
||||
25
docs/configuration/cheat-sheet.md
Normal file
25
docs/configuration/cheat-sheet.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Configuration Cheat Sheet
|
||||
|
||||
| YAML Config Key | Environment Variable | Default value | Description |
|
||||
|-----------------------|--------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
||||
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
|
||||
| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. |
|
||||
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
|
||||
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
|
||||
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
|
||||
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
|
||||
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
|
||||
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
|
||||
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
|
||||
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
||||
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
||||
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
|
||||
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
||||
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
||||
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
||||
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
|
||||
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
|
||||
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
|
||||
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
|
||||
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
|
||||
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
|
||||
38
docs/configuration/index.md
Normal file
38
docs/configuration/index.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Configuration
|
||||
|
||||
Opengist provides flexible configuration options through either a YAML file and/or environment variables.
|
||||
You would only need to specify the configuration options you want to change — for any config option left untouched,
|
||||
Opengist will simply apply the default values.
|
||||
|
||||
The [configuration cheat sheet](cheat-sheet.md) lists all available configuration options.
|
||||
|
||||
|
||||
## Configuration via YAML file
|
||||
|
||||
The configuration file must be specified when launching the application, using the `--config` flag followed by the path
|
||||
to your YAML file.
|
||||
|
||||
```shell
|
||||
./opengist --config /path/to/config.yml
|
||||
```
|
||||
|
||||
You can start by copying and/or modifying the provided [config.yml](/config.yml) file.
|
||||
|
||||
|
||||
## Configuration via Environment Variables
|
||||
|
||||
Usage with Docker Compose :
|
||||
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
OG_LOG_LEVEL: "info"
|
||||
# etc.
|
||||
```
|
||||
Usage via command line :
|
||||
|
||||
```shell
|
||||
OG_LOG_LEVEL=info ./opengist
|
||||
```
|
||||
52
docs/index.md
Normal file
52
docs/index.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Opengist
|
||||
|
||||
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
||||
read and/or modified using standard Git commands, or with the web interface.
|
||||
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
|
||||
|
||||
Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Create public, unlisted or private snippets
|
||||
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Revisions history
|
||||
* Syntax highlighting ; markdown & CSV support
|
||||
* Like / Fork snippets
|
||||
* Search for snippets ; browse users snippets, likes and forks
|
||||
* Editor with indentation mode & size ; drag and drop files
|
||||
* Download raw files or as a ZIP archive
|
||||
* OAuth2 login with GitHub, Gitea, and OpenID Connect
|
||||
* Avatars via Gravatar or OAuth2 providers
|
||||
* Light/Dark mode
|
||||
* Responsive UI
|
||||
* Enable or disable signups
|
||||
* Restrict or unrestrict snippets visibility to anonymous users
|
||||
* Admin panel :
|
||||
* delete users/gists;
|
||||
* clean database/filesystem by syncing gists
|
||||
* run `git gc` for all repositories
|
||||
* SQLite database
|
||||
* Logging
|
||||
* Docker support
|
||||
|
||||
|
||||
## System requirements
|
||||
|
||||
[Git](https://git-scm.com/download) is obviously required to run Opengist, as it's the main feature of the app.
|
||||
Version **2.20** or later is recommended as the app has not been tested with older Git versions.
|
||||
|
||||
[OpenSSH](https://www.openssh.com/) suite if you wish to use Git over SSH.
|
||||
|
||||
|
||||
## Components
|
||||
|
||||
* Backend Web Framework: [Echo](https://echo.labstack.com/)
|
||||
* ORM: [GORM](https://gorm.io/)
|
||||
* Frontend libraries:
|
||||
* [Tailwind CSS](https://tailwindcss.com/)
|
||||
* [CodeMirror](https://codemirror.net/)
|
||||
* [Day.js](https://day.js.org/)
|
||||
* [highlight.js](https://highlightjs.org/)
|
||||
* and [others](/package.json)
|
||||
73
docs/installation.md
Normal file
73
docs/installation.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Installation
|
||||
|
||||
## With Docker
|
||||
|
||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1
|
||||
```
|
||||
|
||||
It can be used in a `docker-compose.yml` file :
|
||||
|
||||
1. Create a `docker-compose.yml` file with the following content
|
||||
2. Run `docker compose up -d`
|
||||
3. Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
```yml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
opengist:
|
||||
image: ghcr.io/thomiceli/opengist:1
|
||||
container_name: opengist
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6157:6157" # HTTP port
|
||||
- "2222:2222" # SSH port, can be removed if you don't use SSH
|
||||
volumes:
|
||||
- "$HOME/.opengist:/opengist"
|
||||
```
|
||||
|
||||
You can define which user/group should run the container and own the files by setting the `UID` and `GID` environment
|
||||
variables :
|
||||
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
UID: 1001
|
||||
GID: 1001
|
||||
```
|
||||
|
||||
### Via binary
|
||||
|
||||
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.5.0/opengist1.5.0-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.5.0-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
```
|
||||
|
||||
|
||||
## From source
|
||||
|
||||
Requirements :
|
||||
* [Git](https://git-scm.com/downloads) (2.20+)
|
||||
* [Go](https://go.dev/doc/install) (1.20+)
|
||||
* [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
|
||||
```shell
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
cd opengist
|
||||
make
|
||||
./opengist
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
42
docs/usage/init-via-git.md
Normal file
42
docs/usage/init-via-git.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Init Gists via Git
|
||||
|
||||
Opengist allows you to create new snippets via Git over HTTP.
|
||||
|
||||
Simply init a new Git repository where your file(s) is/are located:
|
||||
|
||||
```shell
|
||||
git init
|
||||
git add .
|
||||
git commit -m "My cool snippet"
|
||||
```
|
||||
|
||||
Then add this Opengist special remote URL and push your changes:
|
||||
|
||||
```shell
|
||||
git remote add origin http://localhost:6157/init
|
||||
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
Log in with your Opengist account credentials, and your snippet will be created at the specified URL:
|
||||
|
||||
```shell
|
||||
Username for 'http://localhost:6157': thomas
|
||||
Password for 'http://thomas@localhost:6157':
|
||||
Enumerating objects: 3, done.
|
||||
Counting objects: 100% (3/3), done.
|
||||
Delta compression using up to 8 threads
|
||||
Compressing objects: 100% (2/2), done.
|
||||
Writing objects: 100% (3/3), 416 bytes | 416.00 KiB/s, done.
|
||||
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
remote:
|
||||
remote: Your new repository has been created here: http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
|
||||
remote:
|
||||
remote: If you want to keep working with your gist, you could set the remote URL via:
|
||||
remote: git remote set-url origin http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
|
||||
remote:
|
||||
To http://localhost:6157/init
|
||||
* [new branch] master -> master
|
||||
```
|
||||
|
||||
https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066
|
||||
@@ -1,8 +0,0 @@
|
||||
//go:build fs_embed
|
||||
|
||||
package main
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates/*/*.html public/manifest.json public/assets/*.js public/assets/*.css public/assets/*.svg
|
||||
var dirFS embed.FS
|
||||
7
fs_os.go
7
fs_os.go
@@ -1,7 +0,0 @@
|
||||
//go:build !fs_embed
|
||||
|
||||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
var dirFS = os.DirFS(".")
|
||||
55
go.mod
55
go.mod
@@ -1,35 +1,54 @@
|
||||
module opengist
|
||||
module github.com/thomiceli/opengist
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/glebarez/go-sqlite v1.21.2
|
||||
github.com/glebarez/sqlite v1.9.0
|
||||
github.com/go-playground/validator/v10 v10.15.4
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/labstack/echo/v4 v4.10.0
|
||||
github.com/rs/zerolog v1.29.0
|
||||
golang.org/x/crypto v0.2.0
|
||||
github.com/hashicorp/go-memdb v1.3.4
|
||||
github.com/labstack/echo/v4 v4.11.1
|
||||
github.com/markbates/goth v1.78.0
|
||||
github.com/rs/zerolog v1.30.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/crypto v0.13.0
|
||||
golang.org/x/text v0.13.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/sqlite v1.3.2
|
||||
gorm.io/gorm v1.23.5
|
||||
gorm.io/gorm v1.25.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/net v0.4.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/time v0.2.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/oauth2 v0.12.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
modernc.org/libc v1.24.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.1 // indirect
|
||||
modernc.org/sqlite v1.25.0 // indirect
|
||||
)
|
||||
|
||||
533
go.sum
533
go.sum
@@ -1,110 +1,533 @@
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
|
||||
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
|
||||
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
|
||||
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
|
||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
|
||||
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
|
||||
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
|
||||
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
|
||||
github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
||||
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
|
||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
|
||||
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg=
|
||||
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
|
||||
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
|
||||
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
|
||||
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
|
||||
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.1 h1:9J+2/GKTlV503mk3yv8QJ6oEpRCUrRy0ad8TXEPoV8M=
|
||||
modernc.org/memory v1.7.1/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
|
||||
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -2,40 +2,53 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var OpengistVersion = "1.0.1"
|
||||
var OpengistVersion = "1.5.0"
|
||||
|
||||
var C *config
|
||||
|
||||
// Not using nested structs because the library
|
||||
// doesn't support dot notation in this case sadly
|
||||
type config struct {
|
||||
LogLevel string `yaml:"log-level"`
|
||||
ExternalUrl string `yaml:"external-url"`
|
||||
DisableSignup bool `yaml:"disable-signup"`
|
||||
OpengistHome string `yaml:"opengist-home"`
|
||||
DBFilename string `yaml:"db-filename"`
|
||||
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
|
||||
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
|
||||
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
|
||||
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
|
||||
|
||||
HttpHost string `yaml:"http.host"`
|
||||
HttpPort string `yaml:"http.port"`
|
||||
HttpGit bool `yaml:"http.git-enabled"`
|
||||
HttpTLSEnabled bool `yaml:"http.tls-enabled"`
|
||||
HttpCertFile string `yaml:"http.cert-file"`
|
||||
HttpKeyFile string `yaml:"http.key-file"`
|
||||
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
|
||||
|
||||
SshGit bool `yaml:"ssh.git-enabled"`
|
||||
SshHost string `yaml:"ssh.host"`
|
||||
SshPort string `yaml:"ssh.port"`
|
||||
SshExternalDomain string `yaml:"ssh.external-domain"`
|
||||
SshKeygen string `yaml:"ssh.keygen-executable"`
|
||||
HttpHost string `yaml:"http.host" env:"OG_HTTP_HOST"`
|
||||
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
|
||||
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
|
||||
|
||||
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
|
||||
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
|
||||
SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"`
|
||||
SshExternalDomain string `yaml:"ssh.external-domain" env:"OG_SSH_EXTERNAL_DOMAIN"`
|
||||
SshKeygen string `yaml:"ssh.keygen-executable" env:"OG_SSH_KEYGEN_EXECUTABLE"`
|
||||
|
||||
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
|
||||
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
|
||||
|
||||
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
|
||||
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
|
||||
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
|
||||
|
||||
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
|
||||
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
|
||||
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
|
||||
}
|
||||
|
||||
func configWithDefaults() (*config, error) {
|
||||
@@ -46,20 +59,22 @@ func configWithDefaults() (*config, error) {
|
||||
}
|
||||
|
||||
c.LogLevel = "warn"
|
||||
c.DisableSignup = false
|
||||
c.OpengistHome = filepath.Join(homeDir, ".opengist")
|
||||
c.DBFilename = "opengist.db"
|
||||
|
||||
c.SqliteJournalMode = "WAL"
|
||||
|
||||
c.HttpHost = "0.0.0.0"
|
||||
c.HttpPort = "6157"
|
||||
c.HttpGit = true
|
||||
c.HttpTLSEnabled = false
|
||||
|
||||
c.SshGit = true
|
||||
c.SshHost = "0.0.0.0"
|
||||
c.SshPort = "2222"
|
||||
c.SshKeygen = "ssh-keygen"
|
||||
|
||||
c.GiteaUrl = "http://gitea.com"
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -70,37 +85,16 @@ func InitConfig(configPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if configPath != "" {
|
||||
absolutePath, _ := filepath.Abs(configPath)
|
||||
absolutePath = filepath.Clean(absolutePath)
|
||||
file, err := os.Open(absolutePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
fmt.Println("No YML config file found at " + absolutePath)
|
||||
} else {
|
||||
fmt.Println("Using config file: " + absolutePath)
|
||||
|
||||
// Override default values with values from config.yml
|
||||
d := yaml.NewDecoder(file)
|
||||
if err = d.Decode(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No config file specified. Using default values.")
|
||||
if err = loadConfigFromYaml(c, configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Override default values with environment variables (as yaml)
|
||||
configEnv := os.Getenv("CONFIG")
|
||||
if configEnv != "" {
|
||||
fmt.Println("Using config from environment variable: CONFIG")
|
||||
d := yaml.NewDecoder(strings.NewReader(configEnv))
|
||||
if err = d.Decode(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = loadConfigFromEnv(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = checks(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
C = c
|
||||
@@ -123,11 +117,11 @@ func InitLog() {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
if os.Getenv("DEV") == "1" {
|
||||
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
|
||||
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
|
||||
} else {
|
||||
log.Logger = zerolog.New(file).Level(level).With().Timestamp().Logger()
|
||||
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
|
||||
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
|
||||
|
||||
if !utils.SliceContains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
|
||||
log.Warn().Msg("Invalid log level: " + C.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,3 +150,85 @@ func GetHomeDir() string {
|
||||
absolutePath, _ := filepath.Abs(C.OpengistHome)
|
||||
return filepath.Clean(absolutePath)
|
||||
}
|
||||
|
||||
func loadConfigFromYaml(c *config, configPath string) error {
|
||||
if configPath != "" {
|
||||
absolutePath, _ := filepath.Abs(configPath)
|
||||
absolutePath = filepath.Clean(absolutePath)
|
||||
file, err := os.Open(absolutePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
fmt.Println("No YAML config file found at " + absolutePath)
|
||||
} else {
|
||||
fmt.Println("Using YAML config file: " + absolutePath)
|
||||
|
||||
// Override default values with values from config.yml
|
||||
d := yaml.NewDecoder(file)
|
||||
if err = d.Decode(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No YAML config file specified.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadConfigFromEnv(c *config) error {
|
||||
v := reflect.ValueOf(c).Elem()
|
||||
var envVars []string
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
tag := v.Type().Field(i).Tag.Get("env")
|
||||
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
envValue := os.Getenv(strings.ToUpper(tag))
|
||||
if envValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v.Field(i).Kind() {
|
||||
case reflect.String:
|
||||
v.Field(i).SetString(envValue)
|
||||
case reflect.Bool:
|
||||
boolVal, err := strconv.ParseBool(envValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Field(i).SetBool(boolVal)
|
||||
}
|
||||
|
||||
envVars = append(envVars, tag)
|
||||
}
|
||||
|
||||
if len(envVars) > 0 {
|
||||
fmt.Println("Using environment variables config: " + strings.Join(envVars, ", "))
|
||||
} else {
|
||||
fmt.Println("No environment variables config specified.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checks(c *config) error {
|
||||
if _, err := url.Parse(c.ExternalUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := url.Parse(c.GiteaUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := url.Parse(c.OIDCDiscoveryUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
64
internal/db/admin_setting.go
Normal file
64
internal/db/admin_setting.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AdminSetting struct {
|
||||
Key string `gorm:"uniqueIndex"`
|
||||
Value string
|
||||
}
|
||||
|
||||
const (
|
||||
SettingDisableSignup = "disable-signup"
|
||||
SettingRequireLogin = "require-login"
|
||||
SettingDisableLoginForm = "disable-login-form"
|
||||
SettingDisableGravatar = "disable-gravatar"
|
||||
)
|
||||
|
||||
func GetSetting(key string) (string, error) {
|
||||
var setting AdminSetting
|
||||
err := db.Where("key = ?", key).First(&setting).Error
|
||||
return setting.Value, err
|
||||
}
|
||||
|
||||
func GetSettings() (map[string]string, error) {
|
||||
var settings []AdminSetting
|
||||
err := db.Find(&settings).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
for _, setting := range settings {
|
||||
result[setting.Key] = setting.Value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func UpdateSetting(key string, value string) error {
|
||||
return db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}}, // key column
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
||||
}).Create(&AdminSetting{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func setSetting(key string, value string) error {
|
||||
return db.Create(&AdminSetting{Key: key, Value: value}).Error
|
||||
}
|
||||
|
||||
func initAdminSettings(settings map[string]string) error {
|
||||
for key, value := range settings {
|
||||
if err := setSetting(key, value); err != nil {
|
||||
if !IsUniqueConstraintViolation(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
82
internal/db/db.go
Normal file
82
internal/db/db.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
msqlite "github.com/glebarez/go-sqlite"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func Setup(dbPath string, sharedCache bool) error {
|
||||
var err error
|
||||
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
|
||||
|
||||
if !utils.SliceContains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
|
||||
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
|
||||
}
|
||||
|
||||
sharedCacheStr := ""
|
||||
if sharedCache {
|
||||
sharedCacheStr = "&cache=shared"
|
||||
}
|
||||
|
||||
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.SetupJoinTable(&Gist{}, "Likes", &Like{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.SetupJoinTable(&User{}, "Liked", &Like{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ApplyMigrations(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Default admin setting values
|
||||
return initAdminSettings(map[string]string{
|
||||
SettingDisableSignup: "0",
|
||||
SettingRequireLogin: "0",
|
||||
SettingDisableLoginForm: "0",
|
||||
SettingDisableGravatar: "0",
|
||||
})
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
|
||||
func CountAll(table interface{}) (int64, error) {
|
||||
var count int64
|
||||
err := db.Model(table).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func IsUniqueConstraintViolation(err error) bool {
|
||||
var sqliteErr *msqlite.Error
|
||||
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 2067 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"gorm.io/gorm"
|
||||
"opengist/internal/git"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -15,7 +16,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
|
||||
@@ -29,6 +30,12 @@ type Gist struct {
|
||||
ForkedID uint
|
||||
}
|
||||
|
||||
type Like struct {
|
||||
UserID uint `gorm:"primaryKey"`
|
||||
GistID uint `gorm:"primaryKey"`
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
||||
// Decrement fork counter if the gist was forked
|
||||
err := tx.Model(&Gist{}).
|
||||
@@ -80,11 +87,11 @@ func GetAllGists(offset int) ([]*Gist, error) {
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func GetAllGistsFromUser(fromUser string, currentUserId uint, offset int, sort string, order string) ([]*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("users.username = ? and ((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", fromUser, currentUserId).
|
||||
Joins("join users on gists.user_id = users.id").
|
||||
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).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
@@ -93,6 +100,74 @@ func GetAllGistsFromUser(fromUser string, currentUserId uint, offset int, sort s
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
return db.Preload("User").Preload("Forked.User").
|
||||
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")
|
||||
}
|
||||
|
||||
func GetAllGistsFromUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := gistsFromUserStatement(fromUserId, currentUserId).Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
|
||||
var count int64
|
||||
err := gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
return db.Preload("User").Preload("Forked.User").
|
||||
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")
|
||||
}
|
||||
|
||||
func GetAllGistsLikedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := likedStatement(fromUserId, currentUserId).Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error) {
|
||||
var count int64
|
||||
err := likedStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
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 > 0 and gists.user_id = ?))", currentUserId).
|
||||
Where("gists.user_id = ?", fromUserId).
|
||||
Joins("join users on gists.user_id = users.id")
|
||||
}
|
||||
|
||||
func GetAllGistsForkedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := forkedStatement(fromUserId, currentUserId).Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func CountAllGistsForkedByUser(fromUserId uint, currentUserId uint) (int64, error) {
|
||||
var count int64
|
||||
err := forkedStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func GetAllGistsRows() ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Table("gists").
|
||||
@@ -116,6 +191,11 @@ func (gist *Gist) Update() error {
|
||||
}
|
||||
|
||||
func (gist *Gist) Delete() error {
|
||||
err := gist.DeleteRepository()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Delete(&gist).Error
|
||||
}
|
||||
|
||||
@@ -169,7 +249,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").
|
||||
@@ -186,6 +266,10 @@ func (gist *Gist) InitRepository() error {
|
||||
return git.InitRepository(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) InitRepositoryViaInit(ctx echo.Context) error {
|
||||
return git.InitRepositoryViaInit(gist.User.Username, gist.Uuid, ctx)
|
||||
}
|
||||
|
||||
func (gist *Gist) DeleteRepository() error {
|
||||
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
@@ -228,12 +312,12 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
|
||||
}, err
|
||||
}
|
||||
|
||||
func (gist *Gist) Log(skip string) ([]*git.Commit, error) {
|
||||
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
|
||||
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
||||
}
|
||||
|
||||
func (gist *Gist) NbCommits() (string, error) {
|
||||
return git.GetNumberOfCommitsOfRepository(gist.User.Username, gist.Uuid)
|
||||
return git.CountCommits(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
|
||||
@@ -293,7 +377,6 @@ func (gist *Gist) UpdatePreviewAndCount() error {
|
||||
gist.Preview = file.Content
|
||||
}
|
||||
|
||||
gist.Preview = file.Content
|
||||
gist.PreviewFilename = file.Filename
|
||||
}
|
||||
|
||||
@@ -305,8 +388,10 @@ 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"`
|
||||
Name []string `form:"name"`
|
||||
Content []string `form:"content"`
|
||||
}
|
||||
|
||||
type FileDTO struct {
|
||||
@@ -325,6 +410,5 @@ func (dto *GistDTO) ToGist() *Gist {
|
||||
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
|
||||
gist.Title = dto.Title
|
||||
gist.Description = dto.Description
|
||||
gist.Private = dto.Private
|
||||
return gist
|
||||
}
|
||||
103
internal/db/migration.go
Normal file
103
internal/db/migration.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MigrationVersion struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Version uint
|
||||
}
|
||||
|
||||
func ApplyMigrations(db *gorm.DB) error {
|
||||
// Create migration table if it doesn't exist
|
||||
if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error creating migration version table")
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the current migration version
|
||||
var currentVersion MigrationVersion
|
||||
db.First(¤tVersion)
|
||||
|
||||
// Define migrations
|
||||
migrations := []struct {
|
||||
Version uint
|
||||
Func func(*gorm.DB) error
|
||||
}{
|
||||
{1, v1_modifyConstraintToSSHKeys},
|
||||
{2, v2_lowercaseEmails},
|
||||
// Add more migrations here as needed
|
||||
}
|
||||
|
||||
// Apply migrations
|
||||
for _, m := range migrations {
|
||||
if m.Version > currentVersion.Version {
|
||||
tx := db.Begin()
|
||||
if err := tx.Error; err != nil {
|
||||
log.Fatal().Err(err).Msg("Error starting transaction")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.Func(db); err != nil {
|
||||
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
|
||||
tx.Rollback()
|
||||
return err
|
||||
} else {
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
log.Fatal().Err(err).Msg(fmt.Sprintf("Error committing migration %d:", m.Version))
|
||||
return err
|
||||
}
|
||||
currentVersion.Version = m.Version
|
||||
db.Save(¤tVersion)
|
||||
log.Info().Msg(fmt.Sprintf("Migration %d applied successfully", m.Version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Modify the constraint on the ssh_keys table to use ON DELETE CASCADE
|
||||
func v1_modifyConstraintToSSHKeys(db *gorm.DB) error {
|
||||
createSQL := `
|
||||
CREATE TABLE ssh_keys_temp (
|
||||
id integer primary key,
|
||||
title text,
|
||||
content text,
|
||||
sha text,
|
||||
created_at integer,
|
||||
last_used_at integer,
|
||||
user_id integer
|
||||
constraint fk_users_ssh_keys references users(id) on update cascade on delete cascade
|
||||
);
|
||||
`
|
||||
|
||||
if err := db.Exec(createSQL).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy data from the old table to the new table
|
||||
copySQL := `INSERT INTO ssh_keys_temp SELECT * FROM ssh_keys;`
|
||||
if err := db.Exec(copySQL).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drop the old table
|
||||
dropSQL := `DROP TABLE ssh_keys;`
|
||||
if err := db.Exec(dropSQL).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename the new table to the original table name
|
||||
renameSQL := `ALTER TABLE ssh_keys_temp RENAME TO ssh_keys;`
|
||||
return db.Exec(renameSQL).Error
|
||||
}
|
||||
|
||||
func v2_lowercaseEmails(db *gorm.DB) error {
|
||||
// Copy the lowercase emails into the new column
|
||||
copySQL := `UPDATE users SET email = lower(email);`
|
||||
return db.Exec(copySQL).Error
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SSHKey struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
@@ -13,6 +19,16 @@ type SSHKey struct {
|
||||
User User `validate:"-" `
|
||||
}
|
||||
|
||||
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error {
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sha := sha256.Sum256(pubKey.Marshal())
|
||||
sshKey.SHA = base64.StdEncoding.EncodeToString(sha[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSSHKeysByUserID(userId uint) ([]*SSHKey, error) {
|
||||
var sshKeys []*SSHKey
|
||||
err := db.
|
||||
@@ -32,7 +48,7 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
|
||||
return sshKey, err
|
||||
}
|
||||
|
||||
func GetSSHKeyByContent(sshKeyContent string) (*SSHKey, error) {
|
||||
func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) {
|
||||
sshKey := new(SSHKey)
|
||||
err := db.
|
||||
Where("content like ?", sshKeyContent+"%").
|
||||
@@ -49,9 +65,9 @@ func (sshKey *SSHKey) Delete() error {
|
||||
return db.Delete(&sshKey).Error
|
||||
}
|
||||
|
||||
func SSHKeyLastUsedNow(sshKeyID uint) error {
|
||||
func SSHKeyLastUsedNow(sshKeyContent string) error {
|
||||
return db.Model(&SSHKey{}).
|
||||
Where("id = ?", sshKeyID).
|
||||
Where("content = ?", sshKeyContent).
|
||||
Update("last_used_at", time.Now().Unix()).Error
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
@@ -12,9 +12,13 @@ type User struct {
|
||||
CreatedAt int64
|
||||
Email string
|
||||
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
||||
AvatarURL string
|
||||
GithubID string
|
||||
GiteaID string
|
||||
OIDCID string `gorm:"column:oidc_id"`
|
||||
|
||||
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
SSHKeys []SSHKey `gorm:"foreignKey:UserID"`
|
||||
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
@@ -35,7 +39,7 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
}
|
||||
|
||||
// Decrement forks counter for all gists forked by this user
|
||||
return tx.Model(&Gist{}).
|
||||
err = tx.Model(&Gist{}).
|
||||
Omit("updated_at").
|
||||
Where("id IN (?)", tx.
|
||||
Select("forked_id").
|
||||
@@ -44,6 +48,12 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
).
|
||||
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).
|
||||
Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete all gists created by this user
|
||||
return tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
|
||||
}
|
||||
|
||||
func UserExists(username string) (bool, error) {
|
||||
@@ -79,13 +89,51 @@ func GetUserById(userId uint) (*User, error) {
|
||||
return user, err
|
||||
}
|
||||
|
||||
func GetUserBySSHKeyID(sshKeyId uint) (*User, error) {
|
||||
user := new(User)
|
||||
func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error) {
|
||||
var users []*User
|
||||
|
||||
emails := make([]string, 0, len(emailsSet))
|
||||
for email := range emailsSet {
|
||||
emails = append(emails, email)
|
||||
}
|
||||
|
||||
err := db.
|
||||
Preload("SSHKeys").
|
||||
Joins("join ssh_keys on users.id = ssh_keys.user_id").
|
||||
Where("ssh_keys.id = ?", sshKeyId).
|
||||
First(&user).Error
|
||||
Where("email IN ?", emails).
|
||||
Find(&users).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userMap := make(map[string]*User)
|
||||
for _, user := range users {
|
||||
userMap[user.Email] = user
|
||||
}
|
||||
|
||||
return userMap, nil
|
||||
}
|
||||
|
||||
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
|
||||
key := new(SSHKey)
|
||||
err := db.
|
||||
Where("content = ?", sshKey).
|
||||
Where("user_id = ?", userId).
|
||||
First(&key).Error
|
||||
|
||||
return key, err
|
||||
}
|
||||
|
||||
func GetUserByProvider(id string, provider string) (*User, error) {
|
||||
user := new(User)
|
||||
var err error
|
||||
switch provider {
|
||||
case "github":
|
||||
err = db.Where("github_id = ?", id).First(&user).Error
|
||||
case "gitea":
|
||||
err = db.Where("gitea_id = ?", id).First(&user).Error
|
||||
case "openid-connect":
|
||||
err = db.Where("oidc_id = ?", id).First(&user).Error
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
@@ -118,6 +166,28 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (user *User) DeleteProviderID(provider string) error {
|
||||
switch provider {
|
||||
case "github":
|
||||
return db.Model(&user).
|
||||
Update("github_id", nil).
|
||||
Update("avatar_url", nil).
|
||||
Error
|
||||
case "gitea":
|
||||
return db.Model(&user).
|
||||
Update("gitea_id", nil).
|
||||
Update("avatar_url", nil).
|
||||
Error
|
||||
case "openid-connect":
|
||||
return db.Model(&user).
|
||||
Update("oidc_id", nil).
|
||||
Update("avatar_url", nil).
|
||||
Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// -- DTO -- //
|
||||
|
||||
type UserDTO struct {
|
||||
@@ -1,17 +1,44 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"opengist/internal/config"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ReposDirectory = "repos"
|
||||
)
|
||||
|
||||
const truncateLimit = 2 << 18
|
||||
|
||||
func RepositoryPath(user string, gist string) string {
|
||||
return filepath.Join(config.GetHomeDir(), "repos", strings.ToLower(user), gist)
|
||||
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
|
||||
}
|
||||
|
||||
func RepositoryUrl(ctx echo.Context, user string, gist string) string {
|
||||
httpProtocol := "http"
|
||||
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
|
||||
httpProtocol = "https"
|
||||
}
|
||||
|
||||
var baseHttpUrl string
|
||||
// if a custom external url is set, use it
|
||||
if config.C.ExternalUrl != "" {
|
||||
baseHttpUrl = config.C.ExternalUrl
|
||||
} else {
|
||||
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s/%s", baseHttpUrl, user, gist)
|
||||
}
|
||||
|
||||
func TmpRepositoryPath(gistId string) string {
|
||||
@@ -33,15 +60,24 @@ func InitRepository(user string, gist string) error {
|
||||
repositoryPath,
|
||||
)
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return copyFiles(repositoryPath)
|
||||
return createDotGitFiles(repositoryPath)
|
||||
}
|
||||
|
||||
func GetNumberOfCommitsOfRepository(user string, gist string) (string, error) {
|
||||
func InitRepositoryViaInit(user string, gist string, ctx echo.Context) error {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
if err := InitRepository(user, gist); err != nil {
|
||||
return err
|
||||
}
|
||||
repositoryUrl := RepositoryUrl(ctx, user, gist)
|
||||
return createDotGitHookFile(repositoryPath, "post-receive", fmt.Sprintf(postReceive, repositoryUrl, repositoryUrl))
|
||||
}
|
||||
|
||||
func CountCommits(user string, gist string) (string, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command(
|
||||
@@ -62,8 +98,9 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
|
||||
cmd := exec.Command(
|
||||
"git",
|
||||
"ls-tree",
|
||||
revision,
|
||||
"--name-only",
|
||||
"--",
|
||||
revision,
|
||||
)
|
||||
cmd.Dir = repositoryPath
|
||||
|
||||
@@ -81,7 +118,7 @@ func GetFileContent(user string, gist string, revision string, filename string,
|
||||
|
||||
var maxBytes int64 = -1
|
||||
if truncate {
|
||||
maxBytes = 2 << 18
|
||||
maxBytes = truncateLimit
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
@@ -98,10 +135,19 @@ func GetFileContent(user string, gist string, revision string, filename string,
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return truncateCommandOutput(stdout, maxBytes)
|
||||
output, truncated, err := truncateCommandOutput(stdout, maxBytes)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return output, truncated, nil
|
||||
}
|
||||
|
||||
func GetLog(user string, gist string, skip string) ([]*Commit, error) {
|
||||
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command(
|
||||
@@ -113,7 +159,7 @@ func GetLog(user string, gist string, skip string) ([]*Commit, error) {
|
||||
"--no-color",
|
||||
"-p",
|
||||
"--skip",
|
||||
skip,
|
||||
strconv.Itoa(skip),
|
||||
"--format=format:c %H%na %aN%nm %ae%nt %at",
|
||||
"--shortstat",
|
||||
"HEAD",
|
||||
@@ -124,8 +170,14 @@ func GetLog(user string, gist string, skip string) ([]*Commit, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(cmd *exec.Cmd) {
|
||||
waitErr := cmd.Wait()
|
||||
if waitErr != nil {
|
||||
err = waitErr
|
||||
}
|
||||
}(cmd)
|
||||
|
||||
return parseLog(stdout, 2<<18), nil
|
||||
return parseLog(stdout, truncateLimit), err
|
||||
}
|
||||
|
||||
func CloneTmp(user string, gist string, gistTmpId string, email string) error {
|
||||
@@ -147,9 +199,7 @@ func CloneTmp(user string, gist string, gistTmpId string, email string) error {
|
||||
}
|
||||
|
||||
// remove every file (and not the .git directory!)
|
||||
cmd = exec.Command("find", ".", "-maxdepth", "1", "-type", "f", "-delete")
|
||||
cmd.Dir = tmpRepositoryPath
|
||||
if err = cmd.Run(); err != nil {
|
||||
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -173,7 +223,7 @@ func ForkClone(userSrc string, gistSrc string, userDst string, gistDst string) e
|
||||
return err
|
||||
}
|
||||
|
||||
return copyFiles(repositoryPathDst)
|
||||
return createDotGitFiles(repositoryPathDst)
|
||||
}
|
||||
|
||||
func SetFileContent(gistTmpId string, filename string, content string) error {
|
||||
@@ -251,6 +301,67 @@ func RPC(user string, gist string, service string) ([]byte, error) {
|
||||
return stdout, err
|
||||
}
|
||||
|
||||
func GcRepos() error {
|
||||
subdirs, err := os.ReadDir(filepath.Join(config.GetHomeDir(), ReposDirectory))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, subdir := range subdirs {
|
||||
if !subdir.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
subRoot := filepath.Join(config.GetHomeDir(), ReposDirectory, subdir.Name())
|
||||
|
||||
gitRepos, err := os.ReadDir(subRoot)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Cannot read directory")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, repo := range gitRepos {
|
||||
if !repo.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
repoPath := filepath.Join(subRoot, repo.Name())
|
||||
|
||||
log.Info().Msg("Running git gc for repository " + repoPath)
|
||||
|
||||
cmd := exec.Command("git", "gc")
|
||||
cmd.Dir = repoPath
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Cannot run git gc for repository " + repoPath)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func HasNoCommits(user string, gist string) (bool, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command("git", "rev-parse", "--all")
|
||||
cmd.Dir = repositoryPath
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if out.String() == "" {
|
||||
return true, nil // No commits exist
|
||||
}
|
||||
|
||||
return false, nil // Commits exist
|
||||
}
|
||||
|
||||
func GetGitVersion() (string, error) {
|
||||
cmd := exec.Command("git", "--version")
|
||||
stdout, err := cmd.Output()
|
||||
@@ -266,16 +377,27 @@ func GetGitVersion() (string, error) {
|
||||
return versionFields[2], nil
|
||||
}
|
||||
|
||||
func copyFiles(repositoryPath string) error {
|
||||
func createDotGitFiles(repositoryPath string) error {
|
||||
f1, err := os.OpenFile(filepath.Join(repositoryPath, "git-daemon-export-ok"), os.O_RDONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f1.Close()
|
||||
|
||||
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", "pre-receive"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
|
||||
if err = createDotGitHookFile(repositoryPath, "pre-receive", preReceive); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDotGitHookFile(repositoryPath string, hook string, content string) error {
|
||||
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = preReceiveDst.WriteString(preReceive); err != nil {
|
||||
if _, err = preReceiveDst.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
defer preReceiveDst.Close()
|
||||
@@ -283,12 +405,35 @@ func copyFiles(repositoryPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeFilesExceptGit(dir string) error {
|
||||
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() && filepath.Base(path) == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if !d.IsDir() {
|
||||
return os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
const preReceive = `#!/bin/sh
|
||||
|
||||
disallowed_files=""
|
||||
|
||||
while read -r old_rev new_rev ref
|
||||
do
|
||||
if [ "$old_rev" = "0000000000000000000000000000000000000000" ]; then
|
||||
# This is the first commit, so we check all the files in that commit
|
||||
changed_files=$(git ls-tree -r --name-only "$new_rev")
|
||||
else
|
||||
# This is not the first commit, so we compare it with its predecessor
|
||||
changed_files=$(git diff --name-only "$old_rev" "$new_rev")
|
||||
fi
|
||||
|
||||
while IFS= read -r file
|
||||
do
|
||||
case $file in
|
||||
@@ -297,15 +442,29 @@ do
|
||||
;;
|
||||
esac
|
||||
done <<EOF
|
||||
$(git diff --name-only "$old_rev" "$new_rev")
|
||||
$changed_files
|
||||
EOF
|
||||
done
|
||||
|
||||
if [ -n "$disallowed_files" ]; then
|
||||
echo ""
|
||||
echo "Pushing files in folders is not allowed:"
|
||||
for file in $disallowed_files; do
|
||||
echo " $file"
|
||||
done
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
`
|
||||
|
||||
const postReceive = `#!/bin/sh
|
||||
|
||||
echo ""
|
||||
echo "Your new repository has been created here: %s"
|
||||
echo ""
|
||||
echo "If you want to keep working with your gist, you could set the remote URL via:"
|
||||
echo "git remote set-url origin %s"
|
||||
echo ""
|
||||
|
||||
rm -f $0
|
||||
`
|
||||
|
||||
298
internal/git/commands_test.go
Normal file
298
internal/git/commands_test.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setup(t *testing.T) {
|
||||
err := config.InitConfig("")
|
||||
require.NoError(t, err, "Could not init config")
|
||||
|
||||
err = os.MkdirAll(path.Join(config.GetHomeDir(), "tests"), 0755)
|
||||
ReposDirectory = path.Join("tests")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(config.GetHomeDir(), "tmp", "repos"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = InitRepository("thomas", "gist1")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func teardown(t *testing.T) {
|
||||
err := os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
|
||||
require.NoError(t, err, "Could not remove repos directory")
|
||||
}
|
||||
|
||||
func TestInitDeleteRepository(t *testing.T) {
|
||||
setup(t)
|
||||
defer teardown(t)
|
||||
|
||||
cmd := exec.Command("git", "rev-parse", "--is-bare-repository")
|
||||
cmd.Dir = RepositoryPath("thomas", "gist1")
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err, "Could not run git command")
|
||||
require.Equal(t, "true", strings.TrimSpace(string(out)), "Repository is not bare")
|
||||
|
||||
_, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "hooks", "pre-receive"))
|
||||
require.NoError(t, err, "pre-receive hook not found")
|
||||
|
||||
_, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "git-daemon-export-ok"))
|
||||
require.NoError(t, err, "git-daemon-export-ok file not found")
|
||||
|
||||
err = DeleteRepository("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not delete repository")
|
||||
require.NoDirExists(t, RepositoryPath("thomas", "gist1"), "Repository should not exist")
|
||||
}
|
||||
|
||||
func TestCommits(t *testing.T) {
|
||||
setup(t)
|
||||
defer teardown(t)
|
||||
|
||||
hasNoCommits, err := HasNoCommits("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not check if repository has no commits")
|
||||
require.True(t, hasNoCommits, "Repository should have no commits")
|
||||
|
||||
commitToBare(t, "thomas", "gist1", nil)
|
||||
|
||||
hasNoCommits, err = HasNoCommits("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not check if repository has no commits")
|
||||
require.False(t, hasNoCommits, "Repository should have commits")
|
||||
|
||||
nbCommits, err := CountCommits("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not count commits")
|
||||
require.Equal(t, "1", nbCommits, "Repository should have 1 commit")
|
||||
|
||||
commitToBare(t, "thomas", "gist1", nil)
|
||||
nbCommits, err = CountCommits("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not count commits")
|
||||
require.Equal(t, "2", nbCommits, "Repository should have 2 commits")
|
||||
}
|
||||
|
||||
func TestContent(t *testing.T) {
|
||||
setup(t)
|
||||
defer teardown(t)
|
||||
|
||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "I love Opengist\n",
|
||||
"my_other_file.txt": `I really
|
||||
hate Opengist`,
|
||||
"rip.txt": "byebye",
|
||||
})
|
||||
|
||||
files, err := GetFilesOfRepository("thomas", "gist1", "HEAD")
|
||||
require.NoError(t, err, "Could not get files of repository")
|
||||
require.Subset(t, []string{"my_file.txt", "my_other_file.txt", "rip.txt"}, files, "Files are not correct")
|
||||
|
||||
content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", false)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, "I love Opengist\n", content, "Content is not correct")
|
||||
|
||||
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, "I really\nhate Opengist", content, "Content is not correct")
|
||||
|
||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_renamed_file.txt": "I love Opengist\n",
|
||||
"my_other_file.txt": `I really
|
||||
like Opengist actually`,
|
||||
"new_file.txt": "Wait now there is a new file",
|
||||
})
|
||||
|
||||
files, err = GetFilesOfRepository("thomas", "gist1", "HEAD")
|
||||
require.NoError(t, err, "Could not get files of repository")
|
||||
require.Subset(t, []string{"my_renamed_file.txt", "my_other_file.txt", "new_file.txt"}, files, "Files are not correct")
|
||||
|
||||
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, "I really\nlike Opengist actually", content, "Content is not correct")
|
||||
|
||||
commits, err := GetLog("thomas", "gist1", 0)
|
||||
require.NoError(t, err, "Could not get log")
|
||||
require.Equal(t, 2, len(commits), "Commits count are not correct")
|
||||
require.Regexp(t, "[a-f0-9]{40}", commits[0].Hash, "Commit ID is not correct")
|
||||
require.Regexp(t, "[0-9]{10}", commits[0].Timestamp, "Commit timestamp is not correct")
|
||||
require.Equal(t, "thomas", commits[0].AuthorName, "Commit author name is not correct")
|
||||
require.Equal(t, "thomas@mail.com", commits[0].AuthorEmail, "Commit author email is not correct")
|
||||
require.Equal(t, "4 files changed, 2 insertions, 2 deletions", commits[0].Changed, "Commit author name is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "my_renamed_file.txt",
|
||||
OldFilename: "my_file.txt",
|
||||
Content: "",
|
||||
Truncated: false,
|
||||
IsCreated: false,
|
||||
IsDeleted: false,
|
||||
}, "File my_renamed_file.txt is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "rip.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -1 +0,0 @@
|
||||
-byebye
|
||||
\ No newline at end of file
|
||||
`,
|
||||
Truncated: false,
|
||||
IsCreated: false,
|
||||
IsDeleted: true,
|
||||
}, "File rip.txt is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "my_other_file.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -1,2 +1,2 @@
|
||||
I really
|
||||
-hate Opengist
|
||||
\ No newline at end of file
|
||||
+like Opengist actually
|
||||
\ No newline at end of file
|
||||
`,
|
||||
Truncated: false,
|
||||
IsCreated: false,
|
||||
IsDeleted: false,
|
||||
}, "File my_other_file.txt is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "new_file.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -0,0 +1 @@
|
||||
+Wait now there is a new file
|
||||
\ No newline at end of file
|
||||
`,
|
||||
Truncated: false,
|
||||
IsCreated: true,
|
||||
IsDeleted: false,
|
||||
}, "File new_file.txt is not correct")
|
||||
|
||||
commitsSkip1, err := GetLog("thomas", "gist1", 1)
|
||||
require.NoError(t, err, "Could not get log")
|
||||
require.Equal(t, commitsSkip1[0], commits[1], "Commits skips are not correct")
|
||||
}
|
||||
|
||||
func TestGitGc(t *testing.T) {
|
||||
setup(t)
|
||||
defer teardown(t)
|
||||
|
||||
err := GcRepos()
|
||||
require.NoError(t, err, "Could not run git gc")
|
||||
}
|
||||
|
||||
func TestFork(t *testing.T) {
|
||||
setup(t)
|
||||
defer teardown(t)
|
||||
|
||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "I love Opengist\n",
|
||||
})
|
||||
|
||||
err := ForkClone("thomas", "gist1", "thomas", "gist2")
|
||||
require.NoError(t, err, "Could not fork repository")
|
||||
|
||||
files1, err := GetFilesOfRepository("thomas", "gist1", "HEAD")
|
||||
require.NoError(t, err, "Could not get files of repository")
|
||||
files2, err := GetFilesOfRepository("thomas", "gist2", "HEAD")
|
||||
require.NoError(t, err, "Could not get files of repository")
|
||||
|
||||
require.Equal(t, files1, files2, "Files are not the same")
|
||||
|
||||
}
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
setup(t)
|
||||
defer teardown(t)
|
||||
|
||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "A",
|
||||
})
|
||||
|
||||
content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, 1, len(content), "Content size is not correct")
|
||||
|
||||
var builder strings.Builder
|
||||
for i := 0; i < truncateLimit+10; i++ {
|
||||
builder.WriteString("A")
|
||||
}
|
||||
str := builder.String()
|
||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": str,
|
||||
})
|
||||
|
||||
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.True(t, truncated, "Content should be truncated")
|
||||
require.Equal(t, truncateLimit, len(content), "Content size should be at truncate limit")
|
||||
|
||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "AA\n" + str,
|
||||
})
|
||||
|
||||
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.True(t, truncated, "Content should be truncated")
|
||||
require.Equal(t, 2, len(content), "Content size is not correct")
|
||||
}
|
||||
|
||||
func TestInitViaGitInit(t *testing.T) {
|
||||
setup(t)
|
||||
defer teardown(t)
|
||||
|
||||
e := echo.New()
|
||||
|
||||
// Create a mock HTTP request
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
|
||||
// Create a mock HTTP response recorder
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// Create a new Echo context
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
// Define your user and gist
|
||||
user := "testUser"
|
||||
gist := "testGist"
|
||||
|
||||
err := InitRepositoryViaInit(user, gist, c)
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func commitToBare(t *testing.T, user string, gist string, files map[string]string) {
|
||||
err := CloneTmp(user, gist, gist, "thomas@mail.com")
|
||||
require.NoError(t, err, "Could not commit to repository")
|
||||
|
||||
if len(files) > 0 {
|
||||
for filename, content := range files {
|
||||
if err := SetFileContent(gist, filename, content); err != nil {
|
||||
require.NoError(t, err, "Could not commit to repository")
|
||||
}
|
||||
|
||||
if err := AddAll(gist); err != nil {
|
||||
require.NoError(t, err, "Could not commit to repository")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err := CommitRepository(gist, user, "thomas@mail.com"); err != nil {
|
||||
require.NoError(t, err, "Could not commit to repository")
|
||||
}
|
||||
|
||||
if err := Push(gist); err != nil {
|
||||
require.NoError(t, err, "Could not commit to repository")
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
truncated := len(buf) >= int(maxBytes)
|
||||
truncated := maxBytes > 0 && len(buf) >= int(maxBytes)
|
||||
// Remove the last line if it's truncated
|
||||
if truncated {
|
||||
// Find the index of the last newline character
|
||||
|
||||
125
internal/i18n/locale.go
Normal file
125
internal/i18n/locale.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/i18n/locales"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/language/display"
|
||||
"gopkg.in/yaml.v3"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var title = cases.Title(language.English)
|
||||
var Locales = NewLocaleStore()
|
||||
|
||||
type LocaleStore struct {
|
||||
Locales map[string]*Locale
|
||||
}
|
||||
|
||||
type Locale struct {
|
||||
Code string
|
||||
Name string
|
||||
Messages map[string]string
|
||||
}
|
||||
|
||||
// NewLocaleStore creates a new LocaleStore
|
||||
func NewLocaleStore() *LocaleStore {
|
||||
return &LocaleStore{
|
||||
Locales: make(map[string]*Locale),
|
||||
}
|
||||
}
|
||||
|
||||
// loadLocaleFromYAML loads a single Locale from a given YAML file
|
||||
func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
|
||||
a, err := locales.Files.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := io.ReadAll(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag, err := language.Parse(localeCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := display.Self.Name(tag)
|
||||
if tag == language.AmericanEnglish {
|
||||
name = "English"
|
||||
}
|
||||
|
||||
locale := &Locale{
|
||||
Code: localeCode,
|
||||
Name: title.String(name),
|
||||
Messages: make(map[string]string),
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(data, &locale.Messages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store.Locales[localeCode] = locale
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *LocaleStore) LoadAll() error {
|
||||
return fs.WalkDir(locales.Files, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
localeKey := strings.TrimSuffix(path, filepath.Ext(path))
|
||||
err := store.loadLocaleFromYAML(localeKey, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (store *LocaleStore) GetLocale(lang string) (*Locale, error) {
|
||||
_, ok := store.Locales[lang]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("locale %s not found", lang)
|
||||
}
|
||||
|
||||
return store.Locales[lang], nil
|
||||
}
|
||||
|
||||
func (store *LocaleStore) HasLocale(lang string) bool {
|
||||
_, ok := store.Locales[lang]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (store *LocaleStore) MatchTag(langs []language.Tag) string {
|
||||
for _, lang := range langs {
|
||||
if store.HasLocale(lang.String()) {
|
||||
return lang.String()
|
||||
}
|
||||
}
|
||||
|
||||
return "en-US"
|
||||
}
|
||||
|
||||
func (l *Locale) Tr(key string, args ...any) template.HTML {
|
||||
message := l.Messages[key]
|
||||
|
||||
if message == "" {
|
||||
return Locales.Locales["en-US"].Tr(key, args...)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return template.HTML(message)
|
||||
}
|
||||
|
||||
return template.HTML(fmt.Sprintf(message, args...))
|
||||
}
|
||||
177
internal/i18n/locales/en-US.yml
Normal file
177
internal/i18n/locales/en-US.yml
Normal file
@@ -0,0 +1,177 @@
|
||||
gist.public: Public
|
||||
gist.unlisted: Unlisted
|
||||
gist.private: Private
|
||||
|
||||
gist.header.like: Like
|
||||
gist.header.unlike: Unlike
|
||||
gist.header.fork: Fork
|
||||
gist.header.edit: Edit
|
||||
gist.header.delete: Delete
|
||||
gist.header.forked-from: Forked from
|
||||
gist.header.last-active: Last active
|
||||
gist.header.select-tab: Select a tab
|
||||
gist.header.code: Code
|
||||
gist.header.revisions: Revisions
|
||||
gist.header.revision: Revision
|
||||
gist.header.clone-http: Clone via %s
|
||||
gist.header.clone-http-help: Clone with Git using HTTP basic authentication.
|
||||
gist.header.clone-ssh: Clone via SSH
|
||||
gist.header.clone-ssh-help: Clone with Git using an SSH key.
|
||||
gist.header.share: Share
|
||||
gist.header.share-help: Copy shareable link for this gist.
|
||||
gist.header.download-zip: Download ZIP
|
||||
|
||||
gist.raw: Raw
|
||||
gist.file-truncated: This file has been truncated.
|
||||
gist.watch-full-file: View the full file.
|
||||
gist.file-not-valid: This file is not a valid CSV file.
|
||||
gist.no-content: No content
|
||||
|
||||
gist.new.new_gist: New gist
|
||||
gist.new.title: Title
|
||||
gist.new.description: Description
|
||||
gist.new.filename-with-extension: Filename with extension
|
||||
gist.new.indent-mode: Indent mode
|
||||
gist.new.indent-mode-space: Space
|
||||
gist.new.indent-mode-tab: Tab
|
||||
gist.new.indent-size: Indent size
|
||||
gist.new.wrap-mode: Wrap mode
|
||||
gist.new.wrap-mode-no: No wrap
|
||||
gist.new.wrap-mode-soft: Soft wrap
|
||||
gist.new.add-file: Add file
|
||||
gist.new.create-public-button: Create public gist
|
||||
gist.new.create-unlisted-button: Create unlisted gist
|
||||
gist.new.create-private-button: Create private gist
|
||||
|
||||
gist.edit.editing: Editing
|
||||
gist.edit.change-visibility: Make
|
||||
gist.edit.delete: Delete
|
||||
gist.edit.cancel: Cancel
|
||||
gist.edit.save: Save
|
||||
|
||||
gist.list.joined: Joined
|
||||
gist.list.all: All gists
|
||||
gist.list.search-results: Search results
|
||||
gist.list.sort: Sort
|
||||
gist.list.sort-by-created: created
|
||||
gist.list.sort-by-updated: updated
|
||||
gist.list.order-by-asc: Least recently
|
||||
gist.list.order-by-desc: Recently
|
||||
gist.list.select-tab: Select a tab
|
||||
gist.list.liked: Liked
|
||||
gist.list.likes: likes
|
||||
gist.list.forked: Forked
|
||||
gist.list.forked-from: Forked from
|
||||
gist.list.forks: forks
|
||||
gist.list.files: files
|
||||
gist.list.last-active: Last active
|
||||
gist.list.no-gists: No gists
|
||||
|
||||
gist.forks: Forks
|
||||
gist.forks.view: View fork
|
||||
gist.forks.no: No public forks
|
||||
|
||||
gist.likes: Likes
|
||||
gist.likes.no: No likes yet
|
||||
|
||||
gist.revisions: Revisions
|
||||
gist.revision.revised: revised this gist
|
||||
gist.revision.go-to-revision: Go to revision
|
||||
gist.revision.file-created: file created
|
||||
gist.revision.file-deleted: file deleted
|
||||
gist.revision.file-renamed: renamed to
|
||||
gist.revision.diff-truncated: Diff truncated because it's too large to be shown
|
||||
gist.revision.file-renamed-no-changes: File renamed without changes
|
||||
gist.revision.empty-file: Empty file
|
||||
gist.revision.no-changes: No changes
|
||||
gist.revision.no-revisions: No revisions to show
|
||||
|
||||
settings: Settings
|
||||
settings.email: Email
|
||||
settings.email-help: Used for commits and Gravatar
|
||||
settings.email-set: Set email
|
||||
settings.link-accounts: Link accounts
|
||||
settings.link-github-account: Link GitHub account
|
||||
settings.link-gitea-account: Link Gitea account
|
||||
settings.unlink-github-account: Unlink GitHub account
|
||||
settings.unlink-gitea-account: Unlink Gitea account
|
||||
settings.delete-account: Delete account
|
||||
settings.delete-account-confirm: Are you sure you want to delete your account ?
|
||||
settings.add-ssh-key: Add SSH key
|
||||
settings.add-ssh-key-help: Used only to pull/push gists using Git via SSH
|
||||
settings.add-ssh-key-title: Title
|
||||
settings.add-ssh-key-content: Key
|
||||
settings.delete-ssh-key: Delete
|
||||
settings.delete-ssh-key-confirm: Confirm deletion of SSH key
|
||||
settings.ssh-key-added-at: Added
|
||||
settings.ssh-key-never-used: Never used
|
||||
settings.ssh-key-last-used: Last used
|
||||
|
||||
auth.signup-disabled: Administrator has disabled signing up
|
||||
auth.login: Login
|
||||
auth.signup: Register
|
||||
auth.new-account: New account
|
||||
auth.username: Username
|
||||
auth.password: Password
|
||||
auth.register-instead: Register instead
|
||||
auth.login-instead: Login instead
|
||||
auth.github-oauth: Continue with GitHub account
|
||||
auth.gitea-oauth: Continue with Gitea account
|
||||
|
||||
error: Error
|
||||
|
||||
header.menu.all: All
|
||||
header.menu.new: New
|
||||
header.menu.search: Search
|
||||
header.menu.my-gists: My gists
|
||||
header.menu.liked: Liked
|
||||
header.menu.admin: Admin
|
||||
header.menu.settings: Settings
|
||||
header.menu.logout: Logout
|
||||
header.menu.register: Register
|
||||
header.menu.login: Login
|
||||
header.menu.light: Light
|
||||
header.menu.dark: Dark
|
||||
header.menu.system: System
|
||||
footer.powered-by: Powered by %s
|
||||
|
||||
pagination.older: Older
|
||||
pagination.newer: Newer
|
||||
pagination.previous: Previous
|
||||
pagination.next: Next
|
||||
|
||||
admin.admin_panel: Admin panel
|
||||
admin.general: General
|
||||
admin.users: Users
|
||||
admin.gists: Gists
|
||||
admin.configuration: Configuration
|
||||
admin.versions: Versions
|
||||
admin.ssh_keys: SSH keys
|
||||
admin.stats: Stats
|
||||
admin.actions: Actions
|
||||
admin.actions.sync-fs: Synchronize gists from filesystem
|
||||
admin.actions.sync-db: Synchronize gists from database
|
||||
admin.actions.git-gc: Garbage collect git repositories
|
||||
admin.id: ID
|
||||
admin.user: User
|
||||
admin.delete: Delete
|
||||
admin.created_at: Created
|
||||
|
||||
admin.config-link: This configuration can be %s by a YAML config file and/or environment variables.
|
||||
admin.config-link-overriden: overridden
|
||||
admin.disable-signup: Disable signup
|
||||
admin.disable-signup_help: Forbid the creation of new accounts.
|
||||
admin.require-login: Require login
|
||||
admin.require-login_help: Enforce users to be logged in to see gists.
|
||||
admin.disable-login: Disable login form
|
||||
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
|
||||
admin.disable-gravatar: Disable Gravatar
|
||||
admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider.
|
||||
|
||||
admin.users.delete_confirm: Do you want to delete this user ?
|
||||
|
||||
admin.gists.title: Title
|
||||
admin.gists.private: Private ?
|
||||
admin.gists.nb-files: Nb. files
|
||||
admin.gists.nb-likes: Nb. likes
|
||||
admin.gists.delete_confirm: Do you want to delete this gist ?
|
||||
177
internal/i18n/locales/fr-FR.yml
Normal file
177
internal/i18n/locales/fr-FR.yml
Normal file
@@ -0,0 +1,177 @@
|
||||
gist.public: Public
|
||||
gist.unlisted: Non répertorié
|
||||
gist.private: Privé
|
||||
|
||||
gist.header.like: J'aime
|
||||
gist.header.unlike: Je n'aime plus
|
||||
gist.header.fork: Fork
|
||||
gist.header.edit: Éditer
|
||||
gist.header.delete: Supprimer
|
||||
gist.header.forked-from: Forké de
|
||||
gist.header.last-active: Dernière activité
|
||||
gist.header.select-tab: Sélectionner un onglet
|
||||
gist.header.code: Code
|
||||
gist.header.revisions: Révisions
|
||||
gist.header.revision: Révision
|
||||
gist.header.clone-http: Cloner via %s
|
||||
gist.header.clone-http-help: Cloner avec Git en utilisant l'authentification HTTP basic.
|
||||
gist.header.clone-ssh: Cloner via SSH
|
||||
gist.header.clone-ssh-help: Cloner avec Git en utilisant une clé SSH.
|
||||
gist.header.share: Partager
|
||||
gist.header.share-help: Copier le lien partageable de ce gist.
|
||||
gist.header.download-zip: Télécharger en ZIP
|
||||
|
||||
gist.raw: Brut
|
||||
gist.file-truncated: Ce fichier a été tronqué.
|
||||
gist.watch-full-file: Voir le fichier complet.
|
||||
gist.file-not-valid: Ce fichier n'est pas un fichier CSV valide.
|
||||
gist.no-content: Pas de contenu
|
||||
|
||||
gist.new.new_gist: Nouveau gist
|
||||
gist.new.title: Titre
|
||||
gist.new.description: Description
|
||||
gist.new.filename-with-extension: Nom de fichier avec extension
|
||||
gist.new.indent-mode: Mode d'indentation
|
||||
gist.new.indent-mode-space: Espace
|
||||
gist.new.indent-mode-tab: Tabulation
|
||||
gist.new.indent-size: Taille d'indentation
|
||||
gist.new.wrap-mode: Mode d'enroulement
|
||||
gist.new.wrap-mode-no: Sans enroulement
|
||||
gist.new.wrap-mode-soft: Enroulement doux
|
||||
gist.new.add-file: Ajouter un fichier
|
||||
gist.new.create-public-button: Créer un gist public
|
||||
gist.new.create-unlisted-button: Créer un gist non repertorié
|
||||
gist.new.create-private-button: Créer un gist privé
|
||||
|
||||
gist.edit.editing: Édition de
|
||||
gist.edit.change-visibility: Rendre
|
||||
gist.edit.delete: Supprimer
|
||||
gist.edit.cancel: Annuler
|
||||
gist.edit.save: Sauvegarder
|
||||
|
||||
gist.list.joined: Inscrit
|
||||
gist.list.all: Tous les gists
|
||||
gist.list.search-results: Résultats de recherche
|
||||
gist.list.sort: Trier
|
||||
gist.list.sort-by-created: créé
|
||||
gist.list.sort-by-updated: mis à jour
|
||||
gist.list.order-by-asc: Le moins récemment
|
||||
gist.list.order-by-desc: Récemment
|
||||
gist.list.select-tab: Sélectionner un onglet
|
||||
gist.list.liked: Aimé
|
||||
gist.list.likes: j'aimes
|
||||
gist.list.forked: Forké
|
||||
gist.list.forked-from: Forké de
|
||||
gist.list.forks: forks
|
||||
gist.list.files: fichiers
|
||||
gist.list.last-active: Dernière activité
|
||||
gist.list.no-gists: Aucun gist
|
||||
|
||||
gist.forks: Forks
|
||||
gist.forks.view: Voir le fork
|
||||
gist.forks.no: Pas de forks publics
|
||||
|
||||
gist.likes: J'aime
|
||||
gist.likes.no: Aucun j'aime pour le moment
|
||||
|
||||
gist.revisions: Révisions
|
||||
gist.revision.revised: a révisé ce gist
|
||||
gist.revision.go-to-revision: Aller à la révision
|
||||
gist.revision.file-created: fichier créé
|
||||
gist.revision.file-deleted: fichier supprimé
|
||||
gist.revision.file-renamed: renommé en
|
||||
gist.revision.diff-truncated: Révision tronquée car trop volumineuse pour être affichée
|
||||
gist.revision.file-renamed-no-changes: Fichier renommé sans modifications
|
||||
gist.revision.empty-file: Fichier vide
|
||||
gist.revision.no-changes: Aucun changement
|
||||
gist.revision.no-revisions: Aucune révision à afficher
|
||||
|
||||
settings: Paramètres
|
||||
settings.email: Email
|
||||
settings.email-help: Utilisé pour les commits et Gravatar
|
||||
settings.email-set: Définir l'email
|
||||
settings.link-accounts: Lier les comptes
|
||||
settings.link-github-account: Lier le compte GitHub
|
||||
settings.link-gitea-account: Lier le compte Gitea
|
||||
settings.unlink-github-account: Détacher le compte GitHub
|
||||
settings.unlink-gitea-account: Détacher le compte Gitea
|
||||
settings.delete-account: Supprimer le compte
|
||||
settings.delete-account-confirm: Êtes-vous sûr de vouloir supprimer votre compte ?
|
||||
settings.add-ssh-key: Ajouter une clé SSH
|
||||
settings.add-ssh-key-help: Utilisé uniquement pour pull/push des gists avec Git via SSH
|
||||
settings.add-ssh-key-title: Titre
|
||||
settings.add-ssh-key-content: Clé
|
||||
settings.delete-ssh-key: Supprimer
|
||||
settings.delete-ssh-key-confirm: Confirmer la suppression de la clé SSH
|
||||
settings.ssh-key-added-at: Ajouté
|
||||
settings.ssh-key-never-used: Jamais utilisé
|
||||
settings.ssh-key-last-used: Dernière utilisation
|
||||
|
||||
auth.signup-disabled: L'administrateur a désactivé l'inscription
|
||||
auth.login: Connexion
|
||||
auth.signup: Inscription
|
||||
auth.new-account: Nouveau compte
|
||||
auth.username: Nom d'utilisateur
|
||||
auth.password: Mot de passe
|
||||
auth.register-instead: Je préfère m'inscrire
|
||||
auth.login-instead: Je préfère me connecter
|
||||
auth.github-oauth: Continuer avec un compte GitHub
|
||||
auth.gitea-oauth: Continuer avec un compte Gitea
|
||||
|
||||
error: Erreur
|
||||
|
||||
header.menu.all: Tous
|
||||
header.menu.new: Nouveau
|
||||
header.menu.search: Recherche
|
||||
header.menu.my-gists: Mes gists
|
||||
header.menu.liked: Aimés
|
||||
header.menu.admin: Admin
|
||||
header.menu.settings: Paramètres
|
||||
header.menu.logout: Déconnexion
|
||||
header.menu.register: Inscription
|
||||
header.menu.login: Connexion
|
||||
header.menu.light: Clair
|
||||
header.menu.dark: Sombre
|
||||
header.menu.system: Système
|
||||
footer.powered-by: Propulsé par %s
|
||||
|
||||
pagination.older: Plus ancien
|
||||
pagination.newer: Plus récent
|
||||
pagination.previous: Précédent
|
||||
pagination.next: Suivant
|
||||
|
||||
admin.admin_panel: Panneau d'administration
|
||||
admin.general: Général
|
||||
admin.users: Utilisateurs
|
||||
admin.gists: Gists
|
||||
admin.configuration: Configuration
|
||||
admin.versions: Versions
|
||||
admin.ssh_keys: Clés SSH
|
||||
admin.stats: Statistiques
|
||||
admin.actions: Actions
|
||||
admin.actions.sync-fs: Synchroniser les gists depuis le système de fichiers
|
||||
admin.actions.sync-db: Synchroniser les gists depuis la base de données
|
||||
admin.actions.git-gc: Nettoyage des dépôts git
|
||||
admin.id: ID
|
||||
admin.user: Utilisateur
|
||||
admin.delete: Supprimer
|
||||
admin.created_at: Créé
|
||||
|
||||
admin.config-link: Cette configuration peut être %s par un fichier de configuration YAML et/ou des variables d'environnement.
|
||||
admin.config-link-overriden: remplacée
|
||||
admin.disable-signup: Désactiver l'inscription
|
||||
admin.disable-signup_help: Interdire la création de nouveaux comptes.
|
||||
admin.require-login: Exiger la connexion
|
||||
admin.require-login_help: Obliger les utilisateurs à être connectés pour voir les gists.
|
||||
admin.disable-login: Désactiver le formulaire de connexion
|
||||
admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place.
|
||||
admin.disable-gravatar: Désactiver Gravatar
|
||||
admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar.
|
||||
|
||||
admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ?
|
||||
|
||||
admin.gists.title: Titre
|
||||
admin.gists.private: Privé ?
|
||||
admin.gists.nb-files: Nb. de fichiers
|
||||
admin.gists.nb-likes: Nb. de j'aime
|
||||
admin.gists.delete_confirm: Voulez-vous supprimer ce gist ?
|
||||
6
internal/i18n/locales/fs_embed.go
Normal file
6
internal/i18n/locales/fs_embed.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package locales
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.yml
|
||||
var Files embed.FS
|
||||
72
internal/memdb/memdb.go
Normal file
72
internal/memdb/memdb.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package memdb
|
||||
|
||||
import "github.com/hashicorp/go-memdb"
|
||||
import ogdb "github.com/thomiceli/opengist/internal/db"
|
||||
|
||||
var db *memdb.MemDB
|
||||
|
||||
type GistInit struct {
|
||||
UserID uint
|
||||
Gist *ogdb.Gist
|
||||
}
|
||||
|
||||
func Setup() error {
|
||||
var err error
|
||||
schema := &memdb.DBSchema{
|
||||
Tables: map[string]*memdb.TableSchema{
|
||||
"gist_init": {
|
||||
Name: "gist_init",
|
||||
Indexes: map[string]*memdb.IndexSchema{
|
||||
"id": {
|
||||
Name: "id",
|
||||
Unique: true,
|
||||
Indexer: &memdb.UintFieldIndex{Field: "UserID"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
db, err = memdb.NewMemDB(schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertGistInit(userId uint, gist *ogdb.Gist) error {
|
||||
txn := db.Txn(true)
|
||||
if err := txn.Insert("gist_init", &GistInit{
|
||||
UserID: userId,
|
||||
Gist: gist,
|
||||
}); err != nil {
|
||||
txn.Abort()
|
||||
return err
|
||||
}
|
||||
|
||||
txn.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetGistInitAndDelete(userId uint) (*GistInit, error) {
|
||||
txn := db.Txn(true)
|
||||
defer txn.Abort()
|
||||
|
||||
raw, err := txn.First("gist_init", "id", userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gistInit := raw.(*GistInit)
|
||||
if err := txn.Delete("gist_init", gistInit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txn.Commit()
|
||||
return gistInit, nil
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func Setup(dbpath string) error {
|
||||
var err error
|
||||
|
||||
if db, err = gorm.Open(sqlite.Open(dbpath+"?_fk=true"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.AutoMigrate(&User{}, &SSHKey{}, &Gist{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CountAll(table interface{}) (int64, error) {
|
||||
var count int64
|
||||
err := db.Model(table).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -3,16 +3,16 @@ package ssh
|
||||
import (
|
||||
"errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"opengist/internal/git"
|
||||
"opengist/internal/models"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func runGitCommand(ch ssh.Channel, gitCmd string, keyID uint, ip string) error {
|
||||
func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
||||
verb, args := parseCommand(gitCmd)
|
||||
if !strings.HasPrefix(verb, "git-") {
|
||||
verb = ""
|
||||
@@ -32,30 +32,38 @@ func runGitCommand(ch ssh.Channel, gitCmd string, keyID uint, ip string) error {
|
||||
userName := strings.ToLower(repoFields[0])
|
||||
gistName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git")
|
||||
|
||||
gist, err := models.GetGist(userName, gistName)
|
||||
gist, err := db.GetGist(userName, gistName)
|
||||
if err != nil {
|
||||
return errors.New("gist not found")
|
||||
}
|
||||
|
||||
if verb == "receive-pack" {
|
||||
user, err := models.GetUserBySSHKeyID(keyID)
|
||||
requireLogin, err := db.GetSetting(db.SettingRequireLogin)
|
||||
if err != nil {
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
|
||||
// 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 := db.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")
|
||||
}
|
||||
|
||||
if user.ID != gist.UserID {
|
||||
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
_ = db.SSHKeyLastUsedNow(pubKey.Content)
|
||||
}
|
||||
|
||||
_ = models.SSHKeyLastUsedNow(keyID)
|
||||
|
||||
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
||||
|
||||
cmd := exec.Command("git", verb, repositoryPath)
|
||||
|
||||
@@ -3,16 +3,15 @@ package ssh
|
||||
import (
|
||||
"errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net"
|
||||
"opengist/internal/config"
|
||||
"opengist/internal/models"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
@@ -24,7 +23,8 @@ func Start() {
|
||||
|
||||
sshConfig := &ssh.ServerConfig{
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
pkey, err := models.GetSSHKeyByContent(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))))
|
||||
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
||||
_, err := db.SSHKeyDoesExists(strKey)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
@@ -33,7 +33,7 @@ func Start() {
|
||||
log.Warn().Msg("Invalid SSH authentication attempt from " + conn.RemoteAddr().String())
|
||||
return nil, errors.New("unknown public key")
|
||||
}
|
||||
return &ssh.Permissions{Extensions: map[string]string{"key-id": strconv.Itoa(int(pkey.ID))}}, nil
|
||||
return &ssh.Permissions{Extensions: map[string]string{"key": strKey}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -71,13 +71,12 @@ func listen(serverConfig *ssh.ServerConfig) {
|
||||
}
|
||||
|
||||
go ssh.DiscardRequests(reqs)
|
||||
keyID, _ := strconv.Atoi(sConn.Permissions.Extensions["key-id"])
|
||||
go handleConnexion(channels, uint(keyID), sConn.RemoteAddr().String())
|
||||
go handleConnexion(channels, sConn.Permissions.Extensions["key"], sConn.RemoteAddr().String())
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnexion(channels <-chan ssh.NewChannel, keyID uint, ip string) {
|
||||
func handleConnexion(channels <-chan ssh.NewChannel, key string, ip string) {
|
||||
for channel := range channels {
|
||||
if channel.ChannelType() != "session" {
|
||||
_ = channel.Reject(ssh.UnknownChannelType, "Unknown channel type")
|
||||
@@ -109,7 +108,7 @@ func handleConnexion(channels <-chan ssh.NewChannel, keyID uint, ip string) {
|
||||
payloadCmd = payloadCmd[i:]
|
||||
}
|
||||
|
||||
if err = runGitCommand(ch, payloadCmd, keyID, ip); err != nil {
|
||||
if err = runGitCommand(ch, payloadCmd, key, ip); err != nil {
|
||||
_, _ = ch.Stderr().Write([]byte("Opengist: " + err.Error() + "\r\n"))
|
||||
}
|
||||
_, _ = ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0})
|
||||
|
||||
10
internal/utils/slice.go
Normal file
10
internal/utils/slice.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package utils
|
||||
|
||||
func SliceContains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -3,9 +3,9 @@ package web
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"opengist/internal/config"
|
||||
"opengist/internal/git"
|
||||
"opengist/internal/models"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
var (
|
||||
syncReposFromFS = false
|
||||
syncReposFromDB = false
|
||||
gitGcRepos = false
|
||||
)
|
||||
|
||||
func adminIndex(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Admin panel")
|
||||
setData(ctx, "htmlTitle", "Admin panel")
|
||||
setData(ctx, "adminHeaderPage", "index")
|
||||
|
||||
setData(ctx, "opengistVersion", config.OpengistVersion)
|
||||
@@ -30,19 +32,19 @@ func adminIndex(ctx echo.Context) error {
|
||||
}
|
||||
setData(ctx, "gitVersion", gitVersion)
|
||||
|
||||
countUsers, err := models.CountAll(&models.User{})
|
||||
countUsers, err := db.CountAll(&db.User{})
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot count users", err)
|
||||
}
|
||||
setData(ctx, "countUsers", countUsers)
|
||||
|
||||
countGists, err := models.CountAll(&models.Gist{})
|
||||
countGists, err := db.CountAll(&db.Gist{})
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot count gists", err)
|
||||
}
|
||||
setData(ctx, "countGists", countGists)
|
||||
|
||||
countKeys, err := models.CountAll(&models.SSHKey{})
|
||||
countKeys, err := db.CountAll(&db.SSHKey{})
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot count SSH keys", err)
|
||||
}
|
||||
@@ -50,21 +52,23 @@ func adminIndex(ctx echo.Context) error {
|
||||
|
||||
setData(ctx, "syncReposFromFS", syncReposFromFS)
|
||||
setData(ctx, "syncReposFromDB", syncReposFromDB)
|
||||
setData(ctx, "gitGcRepos", gitGcRepos)
|
||||
return html(ctx, "admin_index.html")
|
||||
}
|
||||
|
||||
func adminUsers(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Users")
|
||||
setData(ctx, "htmlTitle", "Users - Admin panel")
|
||||
setData(ctx, "adminHeaderPage", "users")
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
var data []*models.User
|
||||
var data []*db.User
|
||||
var err error
|
||||
if data, err = models.GetAllUsers(pageInt - 1); err != nil {
|
||||
if data, err = db.GetAllUsers(pageInt - 1); err != nil {
|
||||
return errorRes(500, "Cannot get users", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, data, pageInt, 10, "data", "admin/users", 1); err != nil {
|
||||
if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
}
|
||||
|
||||
@@ -72,17 +76,18 @@ func adminUsers(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func adminGists(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Users")
|
||||
setData(ctx, "title", "Gists")
|
||||
setData(ctx, "htmlTitle", "Gists - Admin panel")
|
||||
setData(ctx, "adminHeaderPage", "gists")
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
var data []*models.Gist
|
||||
var data []*db.Gist
|
||||
var err error
|
||||
if data, err = models.GetAllGists(pageInt - 1); err != nil {
|
||||
if data, err = db.GetAllGists(pageInt - 1); err != nil {
|
||||
return errorRes(500, "Cannot get gists", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, data, pageInt, 10, "data", "admin/gists", 1); err != nil {
|
||||
if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
}
|
||||
|
||||
@@ -91,7 +96,7 @@ func adminGists(ctx echo.Context) error {
|
||||
|
||||
func adminUserDelete(ctx echo.Context) error {
|
||||
userId, _ := strconv.ParseUint(ctx.Param("user"), 10, 64)
|
||||
user, err := models.GetUserById(uint(userId))
|
||||
user, err := db.GetUserById(uint(userId))
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot retrieve user", err)
|
||||
}
|
||||
@@ -101,11 +106,11 @@ func adminUserDelete(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
addFlash(ctx, "User has been deleted", "success")
|
||||
return redirect(ctx, "/admin/users")
|
||||
return redirect(ctx, "/admin-panel/users")
|
||||
}
|
||||
|
||||
func adminGistDelete(ctx echo.Context) error {
|
||||
gist, err := models.GetGistByID(ctx.Param("gist"))
|
||||
gist, err := db.GetGistByID(ctx.Param("gist"))
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot retrieve gist", err)
|
||||
}
|
||||
@@ -119,7 +124,7 @@ func adminGistDelete(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
addFlash(ctx, "Gist has been deleted", "success")
|
||||
return redirect(ctx, "/admin/gists")
|
||||
return redirect(ctx, "/admin-panel/gists")
|
||||
}
|
||||
|
||||
func adminSyncReposFromFS(ctx echo.Context) error {
|
||||
@@ -130,7 +135,7 @@ func adminSyncReposFromFS(ctx echo.Context) error {
|
||||
}
|
||||
syncReposFromFS = true
|
||||
|
||||
gists, err := models.GetAllGistsRows()
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
syncReposFromFS = false
|
||||
@@ -148,7 +153,7 @@ func adminSyncReposFromFS(ctx echo.Context) error {
|
||||
}
|
||||
syncReposFromFS = false
|
||||
}()
|
||||
return redirect(ctx, "/admin")
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
func adminSyncReposFromDB(ctx echo.Context) error {
|
||||
@@ -167,7 +172,7 @@ func adminSyncReposFromDB(ctx echo.Context) error {
|
||||
|
||||
for _, e := range entries {
|
||||
path := strings.Split(e, string(os.PathSeparator))
|
||||
gist, _ := models.GetGist(path[len(path)-2], path[len(path)-1])
|
||||
gist, _ := db.GetGist(path[len(path)-2], path[len(path)-1])
|
||||
|
||||
if gist.ID == 0 {
|
||||
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
|
||||
@@ -178,7 +183,44 @@ func adminSyncReposFromDB(ctx echo.Context) error {
|
||||
}
|
||||
}
|
||||
syncReposFromDB = false
|
||||
return
|
||||
}()
|
||||
return redirect(ctx, "/admin")
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
func adminGcRepos(ctx echo.Context) error {
|
||||
addFlash(ctx, "Garbage collecting repositories...", "success")
|
||||
go func() {
|
||||
if gitGcRepos {
|
||||
return
|
||||
}
|
||||
gitGcRepos = true
|
||||
if err := git.GcRepos(); err != nil {
|
||||
log.Error().Err(err).Msg("Error garbage collecting repositories")
|
||||
gitGcRepos = false
|
||||
return
|
||||
}
|
||||
gitGcRepos = false
|
||||
}()
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
func adminConfig(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Configuration")
|
||||
setData(ctx, "htmlTitle", "Configuration - Admin panel")
|
||||
setData(ctx, "adminHeaderPage", "config")
|
||||
|
||||
return html(ctx, "admin_config.html")
|
||||
}
|
||||
|
||||
func adminSetConfig(ctx echo.Context) error {
|
||||
key := ctx.FormValue("key")
|
||||
value := ctx.FormValue("value")
|
||||
|
||||
if err := db.UpdateSetting(key, value); err != nil {
|
||||
return errorRes(500, "Cannot set setting", err)
|
||||
}
|
||||
|
||||
return ctx.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,31 +1,54 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/gitea"
|
||||
"github.com/markbates/goth/providers/github"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"gorm.io/gorm"
|
||||
"opengist/internal/config"
|
||||
"opengist/internal/models"
|
||||
)
|
||||
|
||||
var title = cases.Title(language.English)
|
||||
|
||||
func register(ctx echo.Context) error {
|
||||
setData(ctx, "title", "New account")
|
||||
setData(ctx, "title", tr(ctx, "auth.new-account"))
|
||||
setData(ctx, "htmlTitle", "New account")
|
||||
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
|
||||
func processRegister(ctx echo.Context) error {
|
||||
if config.C.DisableSignup {
|
||||
if getData(ctx, "DisableSignup") == true {
|
||||
return errorRes(403, "Signing up is disabled", nil)
|
||||
}
|
||||
|
||||
if getData(ctx, "DisableLoginForm") == true {
|
||||
return errorRes(403, "Signing up via registration form is disabled", nil)
|
||||
}
|
||||
|
||||
setData(ctx, "title", "New account")
|
||||
setData(ctx, "htmlTitle", "New account")
|
||||
|
||||
sess := getSession(ctx)
|
||||
|
||||
var dto = new(models.UserDTO)
|
||||
dto := new(db.UserDTO)
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, "Cannot bind data", err)
|
||||
}
|
||||
@@ -35,7 +58,7 @@ func processRegister(ctx echo.Context) error {
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
|
||||
if exists, err := models.UserExists(dto.Username); err != nil || exists {
|
||||
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
||||
addFlash(ctx, "Username already exists", "error")
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
@@ -65,24 +88,29 @@ func processRegister(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func login(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Login")
|
||||
setData(ctx, "title", tr(ctx, "auth.login"))
|
||||
setData(ctx, "htmlTitle", "Login")
|
||||
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
|
||||
func processLogin(ctx echo.Context) error {
|
||||
if getData(ctx, "DisableLoginForm") == true {
|
||||
return errorRes(403, "Logging in via login form is disabled", nil)
|
||||
}
|
||||
|
||||
var err error
|
||||
sess := getSession(ctx)
|
||||
|
||||
dto := &models.UserDTO{}
|
||||
dto := &db.UserDTO{}
|
||||
if err = ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, "Cannot bind data", err)
|
||||
}
|
||||
password := dto.Password
|
||||
|
||||
var user *models.User
|
||||
var user *db.User
|
||||
|
||||
if user, err = models.GetUserByUsername(dto.Username); err != nil {
|
||||
if user, err = db.GetUserByUsername(dto.Username); err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot get user", err)
|
||||
}
|
||||
@@ -107,8 +135,268 @@ func processLogin(ctx echo.Context) error {
|
||||
return redirect(ctx, "/")
|
||||
}
|
||||
|
||||
func oauthCallback(ctx echo.Context) error {
|
||||
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
||||
if err != nil {
|
||||
return errorRes(400, "Cannot complete user auth", err)
|
||||
}
|
||||
|
||||
currUser := getUserLogged(ctx)
|
||||
if currUser != nil {
|
||||
// if user is logged in, link account to user and update its avatar URL
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
currUser.GithubID = user.UserID
|
||||
currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
|
||||
case "gitea":
|
||||
currUser.GiteaID = user.UserID
|
||||
currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
|
||||
case "openid-connect":
|
||||
currUser.OIDCID = user.UserID
|
||||
currUser.AvatarURL = user.AvatarURL
|
||||
}
|
||||
|
||||
if err = currUser.Update(); err != nil {
|
||||
return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Account linked to "+title.String(user.Provider), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
// if user is not in database, create it
|
||||
userDB, err := db.GetUserByProvider(user.UserID, user.Provider)
|
||||
if err != nil {
|
||||
if getData(ctx, "DisableSignup") == true {
|
||||
return errorRes(403, "Signing up is disabled", nil)
|
||||
}
|
||||
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot get user", err)
|
||||
}
|
||||
|
||||
userDB = &db.User{
|
||||
Username: user.NickName,
|
||||
Email: user.Email,
|
||||
MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))),
|
||||
}
|
||||
|
||||
// set provider id and avatar URL
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
userDB.GithubID = user.UserID
|
||||
userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
|
||||
case "gitea":
|
||||
userDB.GiteaID = user.UserID
|
||||
userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
|
||||
case "openid-connect":
|
||||
userDB.OIDCID = user.UserID
|
||||
userDB.AvatarURL = user.AvatarURL
|
||||
}
|
||||
|
||||
if err = userDB.Create(); err != nil {
|
||||
if db.IsUniqueConstraintViolation(err) {
|
||||
addFlash(ctx, "Username "+user.NickName+" already exists in Opengist", "error")
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
|
||||
return errorRes(500, "Cannot create user", err)
|
||||
}
|
||||
|
||||
if userDB.ID == 1 {
|
||||
if err = userDB.SetAdmin(); err != nil {
|
||||
return errorRes(500, "Cannot set user admin", err)
|
||||
}
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
|
||||
case "gitea":
|
||||
resp, err = http.Get(urlJoin(config.C.GiteaUrl, user.NickName+".keys"))
|
||||
case "openid-connect":
|
||||
err = errors.New("cannot get keys from OIDC provider")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
addFlash(ctx, "Could not get user keys", "error")
|
||||
log.Error().Err(err).Msg("Could not get user keys")
|
||||
}
|
||||
|
||||
keys := strings.Split(string(body), "\n")
|
||||
if len(keys[len(keys)-1]) == 0 {
|
||||
keys = keys[:len(keys)-1]
|
||||
}
|
||||
for _, key := range keys {
|
||||
sshKey := db.SSHKey{
|
||||
Title: "Added from " + user.Provider,
|
||||
Content: key,
|
||||
User: *userDB,
|
||||
}
|
||||
|
||||
if err = sshKey.Create(); err != nil {
|
||||
addFlash(ctx, "Could not create ssh key", "error")
|
||||
log.Error().Err(err).Msg("Could not create ssh key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sess := getSession(ctx)
|
||||
sess.Values["user"] = userDB.ID
|
||||
saveSession(sess, ctx)
|
||||
deleteCsrfCookie(ctx)
|
||||
|
||||
return redirect(ctx, "/")
|
||||
}
|
||||
|
||||
func oauth(ctx echo.Context) error {
|
||||
provider := ctx.Param("provider")
|
||||
|
||||
httpProtocol := "http"
|
||||
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
|
||||
httpProtocol = "https"
|
||||
}
|
||||
|
||||
var opengistUrl string
|
||||
if config.C.ExternalUrl != "" {
|
||||
opengistUrl = config.C.ExternalUrl
|
||||
} else {
|
||||
opengistUrl = httpProtocol + "://" + ctx.Request().Host
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case "github":
|
||||
goth.UseProviders(
|
||||
github.New(
|
||||
config.C.GithubClientKey,
|
||||
config.C.GithubSecret,
|
||||
urlJoin(opengistUrl, "/oauth/github/callback"),
|
||||
),
|
||||
)
|
||||
|
||||
case "gitea":
|
||||
goth.UseProviders(
|
||||
gitea.NewCustomisedURL(
|
||||
config.C.GiteaClientKey,
|
||||
config.C.GiteaSecret,
|
||||
urlJoin(opengistUrl, "/oauth/gitea/callback"),
|
||||
urlJoin(config.C.GiteaUrl, "/login/oauth/authorize"),
|
||||
urlJoin(config.C.GiteaUrl, "/login/oauth/access_token"),
|
||||
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
|
||||
),
|
||||
)
|
||||
case "openid-connect":
|
||||
oidcProvider, err := openidConnect.New(
|
||||
config.C.OIDCClientKey,
|
||||
config.C.OIDCSecret,
|
||||
urlJoin(opengistUrl, "/oauth/openid-connect/callback"),
|
||||
config.C.OIDCDiscoveryUrl,
|
||||
"openid",
|
||||
"email",
|
||||
"profile",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot create OIDC provider", err)
|
||||
}
|
||||
|
||||
goth.UseProviders(oidcProvider)
|
||||
}
|
||||
|
||||
currUser := getUserLogged(ctx)
|
||||
if currUser != nil {
|
||||
isDelete := false
|
||||
var err error
|
||||
switch provider {
|
||||
case "github":
|
||||
if currUser.GithubID != "" {
|
||||
isDelete = true
|
||||
err = currUser.DeleteProviderID(provider)
|
||||
}
|
||||
case "gitea":
|
||||
if currUser.GiteaID != "" {
|
||||
isDelete = true
|
||||
err = currUser.DeleteProviderID(provider)
|
||||
}
|
||||
case "openid-connect":
|
||||
if currUser.OIDCID != "" {
|
||||
isDelete = true
|
||||
err = currUser.DeleteProviderID(provider)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
|
||||
}
|
||||
|
||||
if isDelete {
|
||||
addFlash(ctx, "Account unlinked from "+title.String(provider), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
}
|
||||
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
|
||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||
if provider != "github" && provider != "gitea" && provider != "openid-connect" {
|
||||
return errorRes(400, "Unsupported provider", nil)
|
||||
}
|
||||
|
||||
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
|
||||
return nil
|
||||
}
|
||||
|
||||
func logout(ctx echo.Context) error {
|
||||
deleteSession(ctx)
|
||||
deleteCsrfCookie(ctx)
|
||||
return redirect(ctx, "/all")
|
||||
}
|
||||
|
||||
func urlJoin(base string, elem ...string) string {
|
||||
joined, err := url.JoinPath(base, elem...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot join url")
|
||||
}
|
||||
|
||||
return joined
|
||||
}
|
||||
|
||||
func getAvatarUrlFromProvider(provider string, identifier string) string {
|
||||
switch provider {
|
||||
case "github":
|
||||
return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4"
|
||||
case "gitea":
|
||||
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", identifier))
|
||||
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 = json.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 ""
|
||||
}
|
||||
return field.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ import (
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"gorm.io/gorm"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"opengist/internal/config"
|
||||
"opengist/internal/models"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -20,11 +21,9 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
userName := ctx.Param("user")
|
||||
gistName := ctx.Param("gistname")
|
||||
|
||||
if strings.HasSuffix(gistName, ".git") {
|
||||
gistName = strings.TrimSuffix(gistName, ".git")
|
||||
}
|
||||
gistName = strings.TrimSuffix(gistName, ".git")
|
||||
|
||||
gist, err := models.GetGist(userName, gistName)
|
||||
gist, err := db.GetGist(userName, gistName)
|
||||
if err != nil {
|
||||
return notFound("Gist not found")
|
||||
}
|
||||
@@ -81,49 +80,104 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
setData(ctx, "hasLiked", hasLiked)
|
||||
}
|
||||
|
||||
if gist.Private > 0 {
|
||||
setData(ctx, "NoIndex", true)
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// 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, _ := db.GetGist(userName, gistName)
|
||||
setData(ctx, "gist", gist)
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// gistNewPushInit has the same behavior as gistSoftInit but create a new gist empty instead
|
||||
func gistNewPushSoftInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
setData(c, "gist", new(db.Gist))
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func allGists(ctx echo.Context) error {
|
||||
var err error
|
||||
var urlPage string
|
||||
|
||||
fromUserStr := ctx.Param("user")
|
||||
|
||||
userLogged := getUserLogged(ctx)
|
||||
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
sort := "created"
|
||||
sortText := tr(ctx, "gist.list.sort-by-created")
|
||||
order := "desc"
|
||||
orderText := "Recently"
|
||||
orderText := tr(ctx, "gist.list.order-by-desc")
|
||||
|
||||
if ctx.QueryParam("sort") == "updated" {
|
||||
sort = "updated"
|
||||
sortText = tr(ctx, "gist.list.sort-by-updated")
|
||||
}
|
||||
|
||||
if ctx.QueryParam("order") == "asc" {
|
||||
order = "asc"
|
||||
orderText = "Least recently"
|
||||
orderText = tr(ctx, "gist.list.order-by-asc")
|
||||
}
|
||||
|
||||
setData(ctx, "sort", sort)
|
||||
setData(ctx, "sort", sortText)
|
||||
setData(ctx, "order", orderText)
|
||||
|
||||
var gists []*models.Gist
|
||||
var gists []*db.Gist
|
||||
var currentUserId uint
|
||||
if userLogged != nil {
|
||||
currentUserId = userLogged.ID
|
||||
} else {
|
||||
currentUserId = 0
|
||||
}
|
||||
if fromUserStr == "" {
|
||||
setData(ctx, "htmlTitle", "All gists")
|
||||
fromUserStr = "all"
|
||||
gists, err = models.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
|
||||
} else {
|
||||
setData(ctx, "htmlTitle", "All gists from "+fromUserStr)
|
||||
|
||||
fromUser, err := models.GetUserByUsername(fromUserStr)
|
||||
if fromUserStr == "" {
|
||||
urlctx := ctx.Request().URL.Path
|
||||
if strings.HasSuffix(urlctx, "search") {
|
||||
setData(ctx, "htmlTitle", "Search results")
|
||||
setData(ctx, "mode", "search")
|
||||
setData(ctx, "searchQuery", ctx.QueryParam("q"))
|
||||
setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
|
||||
urlPage = "search"
|
||||
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
|
||||
} else if strings.HasSuffix(urlctx, "all") {
|
||||
setData(ctx, "htmlTitle", "All gists")
|
||||
setData(ctx, "mode", "all")
|
||||
urlPage = "all"
|
||||
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
|
||||
}
|
||||
} else {
|
||||
liked := false
|
||||
forked := false
|
||||
|
||||
liked, err = regexp.MatchString(`/[^/]*/liked`, ctx.Request().URL.Path)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error matching regexp", err)
|
||||
}
|
||||
|
||||
forked, err = regexp.MatchString(`/[^/]*/forked`, ctx.Request().URL.Path)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error matching regexp", err)
|
||||
}
|
||||
|
||||
var fromUser *db.User
|
||||
|
||||
fromUser, err = db.GetUserByUsername(fromUserStr)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return notFound("User not found")
|
||||
@@ -132,7 +186,40 @@ func allGists(ctx echo.Context) error {
|
||||
}
|
||||
setData(ctx, "fromUser", fromUser)
|
||||
|
||||
gists, err = models.GetAllGistsFromUser(fromUserStr, currentUserId, pageInt-1, sort, order)
|
||||
if countFromUser, err := db.CountAllGistsFromUser(fromUser.ID, currentUserId); err != nil {
|
||||
return errorRes(500, "Error counting gists", err)
|
||||
} else {
|
||||
setData(ctx, "countFromUser", countFromUser)
|
||||
}
|
||||
|
||||
if countLiked, err := db.CountAllGistsLikedByUser(fromUser.ID, currentUserId); err != nil {
|
||||
return errorRes(500, "Error counting liked gists", err)
|
||||
} else {
|
||||
setData(ctx, "countLiked", countLiked)
|
||||
}
|
||||
|
||||
if countForked, err := db.CountAllGistsForkedByUser(fromUser.ID, currentUserId); err != nil {
|
||||
return errorRes(500, "Error counting forked gists", err)
|
||||
} else {
|
||||
setData(ctx, "countForked", countForked)
|
||||
}
|
||||
|
||||
if liked {
|
||||
urlPage = fromUserStr + "/liked"
|
||||
setData(ctx, "htmlTitle", "All gists liked by "+fromUserStr)
|
||||
setData(ctx, "mode", "liked")
|
||||
gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
} else if forked {
|
||||
urlPage = fromUserStr + "/forked"
|
||||
setData(ctx, "htmlTitle", "All gists forked by "+fromUserStr)
|
||||
setData(ctx, "mode", "forked")
|
||||
gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
} else {
|
||||
urlPage = fromUserStr
|
||||
setData(ctx, "htmlTitle", "All gists from "+fromUserStr)
|
||||
setData(ctx, "mode", "fromUser")
|
||||
gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -143,11 +230,12 @@ func allGists(ctx echo.Context) error {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
}
|
||||
|
||||
setData(ctx, "urlPage", urlPage)
|
||||
return html(ctx, "all.html")
|
||||
}
|
||||
|
||||
func gistIndex(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
revision := ctx.Param("revision")
|
||||
|
||||
if revision == "" {
|
||||
@@ -172,13 +260,13 @@ func gistIndex(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func revisions(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
userName := gist.User.Username
|
||||
gistName := gist.Uuid
|
||||
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
commits, err := gist.Log(strconv.Itoa((pageInt - 1) * 10))
|
||||
commits, err := gist.Log((pageInt - 1) * 10)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching commits log", err)
|
||||
}
|
||||
@@ -187,8 +275,22 @@ func revisions(ctx echo.Context) error {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
}
|
||||
|
||||
emailsSet := map[string]struct{}{}
|
||||
for _, commit := range commits {
|
||||
if commit.AuthorEmail == "" {
|
||||
continue
|
||||
}
|
||||
emailsSet[strings.ToLower(commit.AuthorEmail)] = struct{}{}
|
||||
}
|
||||
|
||||
emailsUsers, err := db.GetUsersFromEmails(emailsSet)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching users emails", err)
|
||||
}
|
||||
|
||||
setData(ctx, "page", "revisions")
|
||||
setData(ctx, "revision", "HEAD")
|
||||
setData(ctx, "emails", emailsUsers)
|
||||
setData(ctx, "htmlTitle", "Revision of "+gist.Title)
|
||||
|
||||
return html(ctx, "revisions.html")
|
||||
@@ -210,13 +312,13 @@ func processCreate(ctx echo.Context) error {
|
||||
return errorRes(400, "Bad request", err)
|
||||
}
|
||||
|
||||
dto := new(models.GistDTO)
|
||||
var gist *models.Gist
|
||||
dto := new(db.GistDTO)
|
||||
var gist *db.Gist
|
||||
|
||||
if isCreate {
|
||||
setData(ctx, "htmlTitle", "Create a new gist")
|
||||
} else {
|
||||
gist = getData(ctx, "gist").(*models.Gist)
|
||||
gist = getData(ctx, "gist").(*db.Gist)
|
||||
setData(ctx, "htmlTitle", "Edit "+gist.Title)
|
||||
}
|
||||
|
||||
@@ -224,7 +326,7 @@ func processCreate(ctx echo.Context) error {
|
||||
return errorRes(400, "Cannot bind data", err)
|
||||
}
|
||||
|
||||
dto.Files = make([]models.FileDTO, 0)
|
||||
dto.Files = make([]db.FileDTO, 0)
|
||||
fileCounter := 0
|
||||
for i := 0; i < len(ctx.Request().PostForm["content"]); i++ {
|
||||
name := ctx.Request().PostForm["name"][i]
|
||||
@@ -240,8 +342,8 @@ func processCreate(ctx echo.Context) error {
|
||||
return errorRes(400, "Invalid character unescaped", err)
|
||||
}
|
||||
|
||||
dto.Files = append(dto.Files, models.FileDTO{
|
||||
Filename: name,
|
||||
dto.Files = append(dto.Files, db.FileDTO{
|
||||
Filename: strings.Trim(name, " "),
|
||||
Content: escapedValue,
|
||||
})
|
||||
}
|
||||
@@ -305,7 +407,7 @@ func processCreate(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
if err = gist.AddAndCommitFiles(&dto.Files); err != nil {
|
||||
return errorRes(500, "Error adding and commiting files", err)
|
||||
return errorRes(500, "Error adding and committing files", err)
|
||||
}
|
||||
|
||||
if isCreate {
|
||||
@@ -322,9 +424,9 @@ func processCreate(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func toggleVisibility(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
var gist = getData(ctx, "gist").(*db.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)
|
||||
}
|
||||
@@ -334,12 +436,7 @@ func toggleVisibility(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func deleteGist(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
|
||||
err := gist.DeleteRepository()
|
||||
if err != nil {
|
||||
return errorRes(500, "Error deleting the repository", err)
|
||||
}
|
||||
var gist = getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
if err := gist.Delete(); err != nil {
|
||||
return errorRes(500, "Error deleting this gist", err)
|
||||
@@ -350,7 +447,7 @@ func deleteGist(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func like(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
var gist = getData(ctx, "gist").(*db.Gist)
|
||||
currentUser := getUserLogged(ctx)
|
||||
|
||||
hasLiked, err := currentUser.HasLiked(gist)
|
||||
@@ -376,7 +473,7 @@ func like(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func fork(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
var gist = getData(ctx, "gist").(*db.Gist)
|
||||
currentUser := getUserLogged(ctx)
|
||||
|
||||
alreadyForked, err := gist.GetForkParent(currentUser)
|
||||
@@ -398,7 +495,7 @@ func fork(ctx echo.Context) error {
|
||||
return errorRes(500, "Error creating an UUID", err)
|
||||
}
|
||||
|
||||
newGist := &models.Gist{
|
||||
newGist := &db.Gist{
|
||||
Uuid: strings.Replace(uuidGist.String(), "-", "", -1),
|
||||
Title: gist.Title,
|
||||
Preview: gist.Preview,
|
||||
@@ -427,7 +524,7 @@ func fork(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func rawFile(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
||||
|
||||
if err != nil {
|
||||
@@ -441,8 +538,32 @@ func rawFile(ctx echo.Context) error {
|
||||
return plainText(ctx, 200, file.Content)
|
||||
}
|
||||
|
||||
func downloadFile(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Error getting file content", err)
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
return notFound("File not found")
|
||||
}
|
||||
|
||||
ctx.Response().Header().Set("Content-Type", "text/plain")
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
|
||||
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
|
||||
_, err = ctx.Response().Write([]byte(file.Content))
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Error downloading the file", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func edit(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
var gist = getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
files, err := gist.Files("HEAD")
|
||||
if err != nil {
|
||||
@@ -456,7 +577,7 @@ func edit(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func downloadZip(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
var gist = getData(ctx, "gist").(*db.Gist)
|
||||
var revision = ctx.Param("revision")
|
||||
|
||||
files, err := gist.Files(revision)
|
||||
@@ -501,7 +622,7 @@ func downloadZip(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func likes(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
var gist = getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
@@ -514,13 +635,13 @@ func likes(ctx echo.Context) error {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
}
|
||||
|
||||
setData(ctx, "htmlTitle", "Likes for "+gist.Title)
|
||||
setData(ctx, "htmlTitle", "Like for "+gist.Title)
|
||||
setData(ctx, "revision", "HEAD")
|
||||
return html(ctx, "likes.html")
|
||||
}
|
||||
|
||||
func forks(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
var gist = getData(ctx, "gist").(*db.Gist)
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
currentUser := getUserLogged(ctx)
|
||||
|
||||
@@ -4,12 +4,16 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/memdb"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"opengist/internal/git"
|
||||
"opengist/internal/models"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -45,24 +49,31 @@ func gitHttp(ctx echo.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
noAuth := ctx.QueryParam("service") == "git-upload-pack" ||
|
||||
isInit := strings.HasPrefix(ctx.Request().URL.Path, "/init/info/refs")
|
||||
isInitReceive := strings.HasPrefix(ctx.Request().URL.Path, "/init/git-receive-pack")
|
||||
isInfoRefs := strings.HasSuffix(route.gitUrl, "/info/refs$")
|
||||
isPull := ctx.QueryParam("service") == "git-upload-pack" ||
|
||||
strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") ||
|
||||
ctx.Request().Method == "GET"
|
||||
ctx.Request().Method == "GET" && !isInfoRefs
|
||||
|
||||
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)
|
||||
log.Info().Err(err).Msg("Repository directory does not exist")
|
||||
return errorRes(404, "Repository directory does not exist", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Set("repositoryPath", repositoryPath)
|
||||
setData(ctx, "repositoryPath", repositoryPath)
|
||||
|
||||
// Requires Basic Auth if we push the repository
|
||||
if noAuth {
|
||||
// Shows basic auth if :
|
||||
// - user wants to push the gist
|
||||
// - user wants to clone/pull a private gist
|
||||
// - gist is not found (obfuscation)
|
||||
// - admin setting to require login is set to true
|
||||
if isPull && gist.Private != 2 && gist.ID != 0 && !getData(ctx, "RequireLogin").(bool) {
|
||||
return route.handler(ctx)
|
||||
}
|
||||
|
||||
@@ -81,12 +92,70 @@ func gitHttp(ctx echo.Context) error {
|
||||
return basicAuth(ctx)
|
||||
}
|
||||
|
||||
if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot verify password", err)
|
||||
if !isInit && !isInitReceive {
|
||||
if gist.ID == 0 {
|
||||
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
|
||||
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 plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
} else {
|
||||
var user *db.User
|
||||
if user, err = db.GetUserByUsername(authUsername); err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot get user", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return errorRes(401, "Invalid credentials", nil)
|
||||
}
|
||||
|
||||
if ok, err := argon2id.verify(authPassword, user.Password); !ok {
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot check for password", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return errorRes(401, "Invalid credentials", nil)
|
||||
}
|
||||
|
||||
if isInit {
|
||||
gist = new(db.Gist)
|
||||
gist.UserID = user.ID
|
||||
gist.User = *user
|
||||
uuidGist, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return errorRes(500, "Error creating an UUID", err)
|
||||
}
|
||||
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
|
||||
gist.Title = "gist:" + gist.Uuid
|
||||
|
||||
if err = gist.InitRepositoryViaInit(ctx); err != nil {
|
||||
return errorRes(500, "Cannot init repository in the file system", err)
|
||||
}
|
||||
|
||||
if err = gist.Create(); err != nil {
|
||||
return errorRes(500, "Cannot init repository in database", err)
|
||||
}
|
||||
|
||||
if err := memdb.InsertGistInit(user.ID, gist); err != nil {
|
||||
return errorRes(500, "Cannot save the URL for the new Gist", err)
|
||||
}
|
||||
|
||||
setData(ctx, "gist", gist)
|
||||
} else {
|
||||
gistFromMemdb, err := memdb.GetGistInitAndDelete(user.ID)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot get the gist link from the in memory database", err)
|
||||
}
|
||||
|
||||
gist := gistFromMemdb.Gist
|
||||
setData(ctx, "gist", gist)
|
||||
setData(ctx, "repositoryPath", git.RepositoryPath(gist.User.Username, gist.Uuid))
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return errorRes(403, "Unauthorized", nil)
|
||||
}
|
||||
|
||||
return route.handler(ctx)
|
||||
@@ -122,7 +191,7 @@ func pack(ctx echo.Context, serviceType string) error {
|
||||
}
|
||||
}
|
||||
|
||||
repositoryPath := ctx.Get("repositoryPath").(string)
|
||||
repositoryPath := getData(ctx, "repositoryPath").(string)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
|
||||
@@ -136,7 +205,16 @@ func pack(ctx echo.Context, serviceType string) error {
|
||||
|
||||
// updatedAt is updated only if serviceType is receive-pack
|
||||
if serviceType == "receive-pack" {
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
|
||||
return err
|
||||
} else if hasNoCommits {
|
||||
if err = gist.Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = gist.SetLastActiveNow()
|
||||
_ = gist.UpdatePreviewAndCount()
|
||||
}
|
||||
@@ -147,13 +225,12 @@ func infoRefs(ctx echo.Context) error {
|
||||
noCacheHeaders(ctx)
|
||||
var service string
|
||||
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
serviceType := ctx.QueryParam("service")
|
||||
if !strings.HasPrefix(serviceType, "git-") {
|
||||
service = ""
|
||||
if strings.HasPrefix(serviceType, "git-") {
|
||||
service = strings.TrimPrefix(serviceType, "git-")
|
||||
}
|
||||
service = strings.TrimPrefix(serviceType, "git-")
|
||||
|
||||
if service != "upload-pack" && service != "receive-pack" {
|
||||
if err := gist.UpdateServerInfo(); err != nil {
|
||||
@@ -232,7 +309,7 @@ func basicAuthDecode(encoded string) (string, string, error) {
|
||||
|
||||
func sendFile(ctx echo.Context, contentType string) error {
|
||||
gitFile := "/" + strings.Join(strings.Split(ctx.Request().URL.Path, "/")[3:], "/")
|
||||
gitFile = path.Join(ctx.Get("repositoryPath").(string), gitFile)
|
||||
gitFile = path.Join(getData(ctx, "repositoryPath").(string), gitFile)
|
||||
fi, err := os.Stat(gitFile)
|
||||
if os.IsNotExist(err) {
|
||||
return errorRes(404, "File not found", nil)
|
||||
|
||||
@@ -2,21 +2,24 @@ package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"github.com/thomiceli/opengist/public"
|
||||
"github.com/thomiceli/opengist/templates"
|
||||
"golang.org/x/text/language"
|
||||
htmlpkg "html"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"opengist/internal/config"
|
||||
"opengist/internal/git"
|
||||
"opengist/internal/models"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -24,17 +27,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var dev = os.Getenv("DEV") == "1"
|
||||
var dev bool
|
||||
var store *sessions.CookieStore
|
||||
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 {
|
||||
@@ -46,13 +49,13 @@ var fm = template.FuncMap{
|
||||
return strings.Split(i, "\n")
|
||||
},
|
||||
"isMarkdown": func(i string) bool {
|
||||
return ".md" == strings.ToLower(filepath.Ext(i))
|
||||
return strings.ToLower(filepath.Ext(i)) == ".md"
|
||||
},
|
||||
"isCsv": func(i string) bool {
|
||||
return ".csv" == strings.ToLower(filepath.Ext(i))
|
||||
return strings.ToLower(filepath.Ext(i)) == ".csv"
|
||||
},
|
||||
"csvFile": func(file *git.File) *git.CsvFile {
|
||||
if ".csv" != strings.ToLower(filepath.Ext(file.Filename)) {
|
||||
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -70,22 +73,50 @@ var fm = template.FuncMap{
|
||||
"slug": func(s string) string {
|
||||
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
|
||||
},
|
||||
"avatarUrl": func(userHash string) string {
|
||||
return "https://www.gravatar.com/avatar/" + userHash + "?d=identicon&s=200"
|
||||
},
|
||||
"emailToMD5": func(email string) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))))
|
||||
},
|
||||
"asset": func(jsfile string) string {
|
||||
if dev {
|
||||
return "http://localhost:16157/" + jsfile
|
||||
"avatarUrl": func(user *db.User, noGravatar bool) string {
|
||||
if user.AvatarURL != "" {
|
||||
return user.AvatarURL
|
||||
}
|
||||
return "/" + manifestEntries[jsfile].File
|
||||
|
||||
if user.MD5Hash != "" && !noGravatar {
|
||||
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
|
||||
}
|
||||
|
||||
return defaultAvatar()
|
||||
},
|
||||
"asset": func(file string) string {
|
||||
if dev {
|
||||
return "http://localhost:16157/" + file
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + manifestEntries[file].File
|
||||
},
|
||||
"dev": func() bool {
|
||||
return dev
|
||||
},
|
||||
"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
|
||||
},
|
||||
"unescape": htmlpkg.UnescapeString,
|
||||
"join": func(s ...string) string {
|
||||
return strings.Join(s, "")
|
||||
},
|
||||
"toStr": func(i interface{}) string {
|
||||
return fmt.Sprint(i)
|
||||
},
|
||||
}
|
||||
|
||||
var EmbedFS fs.FS
|
||||
|
||||
type Template struct {
|
||||
templates *template.Template
|
||||
}
|
||||
@@ -94,15 +125,26 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Con
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func Start() {
|
||||
type Server struct {
|
||||
echo *echo.Echo
|
||||
dev bool
|
||||
}
|
||||
|
||||
func NewServer(isDev bool) *Server {
|
||||
dev = isDev
|
||||
store = sessions.NewCookieStore([]byte("opengist"))
|
||||
assetsFS := echo.MustSubFS(EmbedFS, "public/assets")
|
||||
gothic.Store = store
|
||||
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
|
||||
if err := i18n.Locales.LoadAll(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to load locales")
|
||||
}
|
||||
|
||||
e.Use(dataInit)
|
||||
e.Use(locale)
|
||||
e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
|
||||
Getter: middleware.MethodFromForm("_method"),
|
||||
}))
|
||||
@@ -117,11 +159,11 @@ func Start() {
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
e.Use(middleware.Recover())
|
||||
//e.Use(middleware.Recover())
|
||||
e.Use(middleware.Secure())
|
||||
|
||||
e.Renderer = &Template{
|
||||
templates: template.Must(template.New("t").Funcs(fm).ParseFS(EmbedFS, "templates/*/*.html")),
|
||||
templates: template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html")),
|
||||
}
|
||||
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
|
||||
if err, ok := er.(*echo.HTTPError); ok {
|
||||
@@ -144,19 +186,21 @@ func Start() {
|
||||
|
||||
if !dev {
|
||||
parseManifestEntries()
|
||||
e.GET("/assets/*", cacheControl(echo.WrapHandler(http.StripPrefix("/assets", http.FileServer(http.FS(assetsFS))))))
|
||||
e.GET("/assets/*", cacheControl(echo.WrapHandler(http.FileServer(http.FS(public.Files)))))
|
||||
}
|
||||
|
||||
// Web based routes
|
||||
g1 := e.Group("")
|
||||
{
|
||||
g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf",
|
||||
CookiePath: "/",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
}))
|
||||
g1.Use(csrfInit)
|
||||
if !dev {
|
||||
g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf",
|
||||
CookiePath: "/",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
}))
|
||||
g1.Use(csrfInit)
|
||||
}
|
||||
|
||||
g1.GET("/", create, logged)
|
||||
g1.POST("/", processCreate, logged)
|
||||
@@ -166,6 +210,8 @@ func Start() {
|
||||
g1.GET("/login", login)
|
||||
g1.POST("/login", processLogin)
|
||||
g1.GET("/logout", logout)
|
||||
g1.GET("/oauth/:provider", oauth)
|
||||
g1.GET("/oauth/:provider/callback", oauthCallback)
|
||||
|
||||
g1.GET("/settings", userSettings, logged)
|
||||
g1.POST("/settings/email", emailProcess, logged)
|
||||
@@ -173,7 +219,7 @@ func Start() {
|
||||
g1.POST("/settings/ssh-keys", sshKeysProcess, logged)
|
||||
g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged)
|
||||
|
||||
g2 := g1.Group("/admin")
|
||||
g2 := g1.Group("/admin-panel")
|
||||
{
|
||||
g2.Use(adminPermission)
|
||||
g2.GET("", adminIndex)
|
||||
@@ -183,14 +229,24 @@ func Start() {
|
||||
g2.POST("/gists/:gist/delete", adminGistDelete)
|
||||
g2.POST("/sync-fs", adminSyncReposFromFS)
|
||||
g2.POST("/sync-db", adminSyncReposFromDB)
|
||||
g2.POST("/gc-repos", adminGcRepos)
|
||||
g2.GET("/configuration", adminConfig)
|
||||
g2.PUT("/set-config", adminSetConfig)
|
||||
}
|
||||
|
||||
g1.GET("/all", allGists)
|
||||
g1.GET("/:user", allGists)
|
||||
if config.C.HttpGit {
|
||||
e.Any("/init/*", gitHttp, gistNewPushSoftInit)
|
||||
}
|
||||
|
||||
g1.GET("/all", allGists, checkRequireLogin)
|
||||
g1.GET("/search", allGists, checkRequireLogin)
|
||||
g1.GET("/:user", allGists, checkRequireLogin)
|
||||
g1.GET("/:user/liked", allGists, checkRequireLogin)
|
||||
g1.GET("/:user/forked", allGists, checkRequireLogin)
|
||||
|
||||
g3 := g1.Group("/:user/:gistname")
|
||||
{
|
||||
g3.Use(gistInit)
|
||||
g3.Use(checkRequireLogin, gistInit)
|
||||
g3.GET("", gistIndex)
|
||||
g3.GET("/rev/:revision", gistIndex)
|
||||
g3.GET("/revisions", revisions)
|
||||
@@ -198,6 +254,7 @@ func Start() {
|
||||
g3.POST("/visibility", toggleVisibility, logged, writePermission)
|
||||
g3.POST("/delete", deleteGist, logged, writePermission)
|
||||
g3.GET("/raw/:revision/:file", rawFile)
|
||||
g3.GET("/download/:revision/:file", downloadFile)
|
||||
g3.GET("/edit", edit, logged, writePermission)
|
||||
g3.POST("/edit", processCreate, logged, writePermission)
|
||||
g3.POST("/like", like, logged)
|
||||
@@ -207,36 +264,94 @@ func Start() {
|
||||
}
|
||||
}
|
||||
|
||||
debugStr := ""
|
||||
// Git HTTP routes
|
||||
if config.C.HttpGit {
|
||||
e.Any("/:user/:gistname/*", gitHttp, gistInit)
|
||||
debugStr = " (with Git over HTTP)"
|
||||
e.Any("/:user/:gistname/*", gitHttp, gistSoftInit)
|
||||
}
|
||||
|
||||
e.Any("/*", noRouteFound)
|
||||
|
||||
return &Server{echo: e, dev: dev}
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
addr := config.C.HttpHost + ":" + config.C.HttpPort
|
||||
|
||||
if config.C.HttpTLSEnabled {
|
||||
log.Info().Msg("Starting HTTPS server on https://" + addr + debugStr)
|
||||
if err := e.StartTLS(addr, config.C.HttpCertFile, config.C.HttpKeyFile); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start HTTPS server")
|
||||
}
|
||||
} else {
|
||||
log.Info().Msg("Starting HTTP server on http://" + addr + debugStr)
|
||||
if err := e.Start(addr); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start HTTP server")
|
||||
}
|
||||
log.Info().Msg("Starting HTTP server on http://" + addr)
|
||||
if err := s.echo.Start(addr); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal().Err(err).Msg("Failed to start HTTP server")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
if err := s.echo.Close(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to stop HTTP server")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.echo.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), "data", echo.Map{})
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), dataKey, echo.Map{})
|
||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||
setData(ctx, "loadStartTime", time.Now())
|
||||
setData(ctx, "signupDisabled", config.C.DisableSignup)
|
||||
|
||||
if err := loadSettings(ctx); err != nil {
|
||||
return errorRes(500, "Cannot read settings from database", err)
|
||||
}
|
||||
|
||||
setData(ctx, "c", config.C)
|
||||
|
||||
setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
|
||||
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
|
||||
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func locale(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
|
||||
// Check URL arguments
|
||||
lang := ctx.Request().URL.Query().Get("lang")
|
||||
changeLang := lang != ""
|
||||
|
||||
// Then check cookies
|
||||
if len(lang) == 0 {
|
||||
cookie, _ := ctx.Request().Cookie("lang")
|
||||
if cookie != nil {
|
||||
lang = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Check again in case someone changes the supported language list.
|
||||
if lang != "" && !i18n.Locales.HasLocale(lang) {
|
||||
lang = ""
|
||||
changeLang = false
|
||||
}
|
||||
|
||||
//3.Then check from 'Accept-Language' header.
|
||||
if len(lang) == 0 {
|
||||
tags, _, _ := language.ParseAcceptLanguage(ctx.Request().Header.Get("Accept-Language"))
|
||||
lang = i18n.Locales.MatchTag(tags)
|
||||
}
|
||||
|
||||
if changeLang {
|
||||
ctx.SetCookie(&http.Cookie{Name: "lang", Value: lang, Path: "/", MaxAge: 1<<31 - 1})
|
||||
}
|
||||
|
||||
localeUsed, err := i18n.Locales.GetLocale(lang)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot get locale", err)
|
||||
}
|
||||
|
||||
setData(ctx, "localeName", localeUsed.Name)
|
||||
setData(ctx, "locale", localeUsed)
|
||||
setData(ctx, "allLocales", i18n.Locales.Locales)
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
@@ -247,9 +362,9 @@ func sessionInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
sess := getSession(ctx)
|
||||
if sess.Values["user"] != nil {
|
||||
var err error
|
||||
var user *models.User
|
||||
var user *db.User
|
||||
|
||||
if user, err = models.GetUserById(sess.Values["user"].(uint)); err != nil {
|
||||
if user, err = db.GetUserById(sess.Values["user"].(uint)); err != nil {
|
||||
sess.Values["user"] = nil
|
||||
saveSession(sess, ctx)
|
||||
setData(ctx, "userLogged", nil)
|
||||
@@ -277,8 +392,8 @@ func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist")
|
||||
user := getUserLogged(ctx)
|
||||
if !gist.(*models.Gist).CanWrite(user) {
|
||||
return redirect(ctx, "/"+gist.(*models.Gist).User.Username+"/"+gist.(*models.Gist).Uuid)
|
||||
if !gist.(*db.Gist).CanWrite(user) {
|
||||
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Uuid)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
@@ -300,7 +415,22 @@ func logged(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
if user != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
return redirect(ctx, "/login")
|
||||
return redirect(ctx, "/all")
|
||||
}
|
||||
}
|
||||
|
||||
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
if user := getUserLogged(ctx); user != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
require := getData(ctx, "RequireLogin")
|
||||
if require == true {
|
||||
addFlash(ctx, "You must be logged in to access gists", "error")
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +454,7 @@ type Asset struct {
|
||||
var manifestEntries map[string]Asset
|
||||
|
||||
func parseManifestEntries() {
|
||||
file, err := EmbedFS.Open("public/manifest.json")
|
||||
file, err := public.Files.Open("manifest.json")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open manifest.json")
|
||||
}
|
||||
@@ -336,3 +466,10 @@ func parseManifestEntries() {
|
||||
log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json")
|
||||
}
|
||||
}
|
||||
|
||||
func defaultAvatar() string {
|
||||
if dev {
|
||||
return "http://localhost:16157/default.png"
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + manifestEntries["default.png"].File
|
||||
}
|
||||
@@ -2,12 +2,10 @@ package web
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"opengist/internal/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -16,7 +14,7 @@ import (
|
||||
func userSettings(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
|
||||
keys, err := models.GetSSHKeysByUserID(user.ID)
|
||||
keys, err := db.GetSSHKeysByUserID(user.ID)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot get SSH keys", err)
|
||||
}
|
||||
@@ -39,7 +37,7 @@ func emailProcess(ctx echo.Context) error {
|
||||
hash = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))))
|
||||
}
|
||||
|
||||
user.Email = email
|
||||
user.Email = strings.ToLower(email)
|
||||
user.MD5Hash = hash
|
||||
|
||||
if err := user.Update(); err != nil {
|
||||
@@ -63,7 +61,7 @@ func accountDeleteProcess(ctx echo.Context) error {
|
||||
func sshKeysProcess(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
|
||||
var dto = new(models.SSHKeyDTO)
|
||||
var dto = new(db.SSHKeyDTO)
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, "Cannot bind data", err)
|
||||
}
|
||||
@@ -81,9 +79,7 @@ func sshKeysProcess(ctx echo.Context) error {
|
||||
addFlash(ctx, "Invalid SSH key", "error")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
sha := sha256.Sum256(pubKey.Marshal())
|
||||
key.SHA = base64.StdEncoding.EncodeToString(sha[:])
|
||||
key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||
|
||||
if err := key.Create(); err != nil {
|
||||
return errorRes(500, "Cannot add SSH key", err)
|
||||
@@ -101,7 +97,7 @@ func sshKeysDelete(ctx echo.Context) error {
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
key, err := models.GetSSHKeyByID(uint(keyId))
|
||||
key, err := db.GetSSHKeyByID(uint(keyId))
|
||||
|
||||
if err != nil || key.UserID != user.ID {
|
||||
return redirect(ctx, "/settings")
|
||||
91
internal/web/test/auth_test.go
Normal file
91
internal/web/test/auth_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
err = s.request("GET", "/", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.request("GET", "/register", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
user1db, err := db.GetUserById(1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, user1.Username, user1db.Username)
|
||||
require.True(t, user1db.IsAdmin)
|
||||
|
||||
err = s.request("GET", "/", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "thomas", Password: "azeaze"}
|
||||
err = s.request("POST", "/register", user2, 200)
|
||||
require.Error(t, err)
|
||||
|
||||
user3 := db.UserDTO{Username: "kaguya", Password: "kaguya"}
|
||||
register(t, s, user3)
|
||||
|
||||
user3db, err := db.GetUserById(2)
|
||||
require.NoError(t, err)
|
||||
require.False(t, user3db.IsAdmin)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
count, err := db.CountAll(db.User{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), count)
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
err = s.request("GET", "/login", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
login(t, s, user1)
|
||||
require.NotEmpty(t, s.sessionCookie)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "thomas", Password: "azeaze"}
|
||||
user3 := db.UserDTO{Username: "azeaze", Password: ""}
|
||||
|
||||
err = s.request("POST", "/login", user2, 302)
|
||||
require.Empty(t, s.sessionCookie)
|
||||
require.Error(t, err)
|
||||
|
||||
err = s.request("POST", "/login", user3, 302)
|
||||
require.Empty(t, s.sessionCookie)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func register(t *testing.T, s *testServer, user db.UserDTO) {
|
||||
err := s.request("POST", "/register", user, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func login(t *testing.T, s *testServer, user db.UserDTO) {
|
||||
err := s.request("POST", "/login", user, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
200
internal/web/test/gist_test.go
Normal file
200
internal/web/test/gist_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGists(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
err = s.request("GET", "/", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
err = s.request("GET", "/all", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.request("POST", "/", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
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, user1.Username, gist1db.User.Username)
|
||||
|
||||
err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1files, err := git.GetFilesOfRepository(gist1db.User.Username, gist1db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(gist1files))
|
||||
|
||||
gist1fileContent, _, err := git.GetFileContent(gist1db.User.Username, gist1db.Uuid, "HEAD", gist1.Name[0], false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1.Content[0], gist1fileContent)
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "gist2",
|
||||
Description: "my second gist",
|
||||
Private: 0,
|
||||
Name: []string{"", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
}
|
||||
err = s.request("POST", "/", gist2, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3 := db.GistDTO{
|
||||
Title: "gist3",
|
||||
Description: "my third gist",
|
||||
Private: 0,
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
}
|
||||
err = s.request("POST", "/", gist3, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3files, err := git.GetFilesOfRepository(gist3db.User.Username, gist3db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "gistfile1.txt", gist3files[0])
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1.Name = []string{"gist1.txt"}
|
||||
gist1.Content = []string{"only want one gist"}
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1files, err = git.GetFilesOfRepository(gist1db.User.Username, gist1db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(gist1files))
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/delete", nil, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestVisibility(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",
|
||||
Description: "my first gist",
|
||||
Private: 1,
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, gist1db.Private)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, gist1db.Private)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist1db.Private)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, gist1db.Private)
|
||||
}
|
||||
|
||||
func TestLikeFork(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",
|
||||
Description: "my first gist",
|
||||
Private: 1,
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "kaguya", Password: "kaguya"}
|
||||
register(t, s, user2)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist1db.NbLikes)
|
||||
likeCount, err := db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), likeCount)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/like", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, gist1db.NbLikes)
|
||||
likeCount, err = db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), likeCount)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/like", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist1db.NbLikes)
|
||||
likeCount, err = db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), likeCount)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/fork", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db.Title, gist2db.Title)
|
||||
require.Equal(t, gist1db.Description, gist2db.Description)
|
||||
require.Equal(t, gist1db.Private, gist2db.Private)
|
||||
require.Equal(t, user2.Username, gist2db.User.Username)
|
||||
}
|
||||
162
internal/web/test/server.go
Normal file
162
internal/web/test/server.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/memdb"
|
||||
"github.com/thomiceli/opengist/internal/web"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testServer struct {
|
||||
server *web.Server
|
||||
sessionCookie string
|
||||
}
|
||||
|
||||
func newTestServer() (*testServer, error) {
|
||||
s := &testServer{
|
||||
server: web.NewServer(true),
|
||||
}
|
||||
|
||||
go s.start()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *testServer) start() {
|
||||
s.server.Start()
|
||||
}
|
||||
|
||||
func (s *testServer) stop() {
|
||||
s.server.Stop()
|
||||
}
|
||||
|
||||
func (s *testServer) request(method, uri string, data interface{}, expectedCode int) error {
|
||||
var bodyReader io.Reader
|
||||
if method == http.MethodPost || method == http.MethodPut {
|
||||
values := structToURLValues(data)
|
||||
bodyReader = strings.NewReader(values.Encode())
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(method, "http://localhost:6157"+uri, bodyReader)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if method == http.MethodPost || method == http.MethodPut {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
if s.sessionCookie != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: s.sessionCookie})
|
||||
}
|
||||
|
||||
s.server.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != expectedCode {
|
||||
return fmt.Errorf("unexpected status code %d, expected %d", w.Code, expectedCode)
|
||||
}
|
||||
|
||||
if method == http.MethodPost {
|
||||
if strings.Contains(uri, "/login") || strings.Contains(uri, "/register") {
|
||||
cookie := ""
|
||||
h := w.Header().Get("Set-Cookie")
|
||||
parts := strings.Split(h, "; ")
|
||||
for _, p := range parts {
|
||||
if strings.HasPrefix(p, "session=") {
|
||||
cookie = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if cookie == "" {
|
||||
return errors.New("unable to find access session token in response headers")
|
||||
}
|
||||
s.sessionCookie = strings.TrimPrefix(cookie, "session=")
|
||||
} else if strings.Contains(uri, "/logout") {
|
||||
s.sessionCookie = ""
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func structToURLValues(s interface{}) url.Values {
|
||||
v := url.Values{}
|
||||
if s == nil {
|
||||
return v
|
||||
}
|
||||
|
||||
rValue := reflect.ValueOf(s)
|
||||
if rValue.Kind() != reflect.Struct {
|
||||
return v
|
||||
}
|
||||
|
||||
for i := 0; i < rValue.NumField(); i++ {
|
||||
field := rValue.Type().Field(i)
|
||||
tag := field.Tag.Get("form")
|
||||
if tag != "" {
|
||||
if field.Type.Kind() == reflect.Int {
|
||||
fieldValue := rValue.Field(i).Int()
|
||||
v.Add(tag, strconv.FormatInt(fieldValue, 10))
|
||||
} else if field.Type.Kind() == reflect.Slice {
|
||||
fieldValue := rValue.Field(i).Interface().([]string)
|
||||
for _, va := range fieldValue {
|
||||
v.Add(tag, va)
|
||||
}
|
||||
} else {
|
||||
fieldValue := rValue.Field(i).String()
|
||||
v.Add(tag, fieldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func setup(t *testing.T) {
|
||||
err := config.InitConfig("")
|
||||
require.NoError(t, err, "Could not init config")
|
||||
|
||||
err = os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755)
|
||||
require.NoError(t, err, "Could not create Opengist home directory")
|
||||
|
||||
git.ReposDirectory = path.Join("tests")
|
||||
|
||||
config.InitLog()
|
||||
|
||||
homePath := config.GetHomeDir()
|
||||
log.Info().Msg("Data directory: " + homePath)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(homePath, "repos"), 0755)
|
||||
require.NoError(t, err, "Could not create repos directory")
|
||||
|
||||
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
|
||||
require.NoError(t, err, "Could not create tmp repos directory")
|
||||
|
||||
err = db.Setup("file::memory:", true)
|
||||
require.NoError(t, err, "Could not initialize database")
|
||||
|
||||
err = memdb.Setup()
|
||||
require.NoError(t, err, "Could not initialize in memory database")
|
||||
}
|
||||
|
||||
func teardown(t *testing.T, s *testServer) {
|
||||
s.stop()
|
||||
|
||||
err := db.Close()
|
||||
require.NoError(t, err, "Could not close database")
|
||||
|
||||
err = os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
|
||||
require.NoError(t, err, "Could not remove repos directory")
|
||||
}
|
||||
@@ -10,23 +10,29 @@ import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"opengist/internal/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type dataTypeKey string
|
||||
|
||||
const dataKey dataTypeKey = "data"
|
||||
|
||||
func setData(ctx echo.Context, key string, value any) {
|
||||
data := ctx.Request().Context().Value("data").(echo.Map)
|
||||
data := ctx.Request().Context().Value(dataKey).(echo.Map)
|
||||
data[key] = value
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), "data", data)
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), dataKey, data)
|
||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||
}
|
||||
|
||||
func getData(ctx echo.Context, key string) any {
|
||||
data := ctx.Request().Context().Value("data").(echo.Map)
|
||||
data := ctx.Request().Context().Value(dataKey).(echo.Map)
|
||||
return data[key]
|
||||
}
|
||||
|
||||
@@ -36,11 +42,11 @@ func html(ctx echo.Context, template string) error {
|
||||
|
||||
func htmlWithCode(ctx echo.Context, code int, template string) error {
|
||||
setErrorFlashes(ctx)
|
||||
return ctx.Render(code, template, ctx.Request().Context().Value("data"))
|
||||
return ctx.Render(code, template, ctx.Request().Context().Value(dataKey))
|
||||
}
|
||||
|
||||
func redirect(ctx echo.Context, location string) error {
|
||||
return ctx.Redirect(302, location)
|
||||
return ctx.Redirect(302, config.C.ExternalUrl+location)
|
||||
}
|
||||
|
||||
func plainText(ctx echo.Context, code int, message string) error {
|
||||
@@ -55,10 +61,10 @@ func errorRes(code int, message string, err error) error {
|
||||
return &echo.HTTPError{Code: code, Message: message, Internal: err}
|
||||
}
|
||||
|
||||
func getUserLogged(ctx echo.Context) *models.User {
|
||||
func getUserLogged(ctx echo.Context) *db.User {
|
||||
user := getData(ctx, "userLogged")
|
||||
if user != nil {
|
||||
return user.(*models.User)
|
||||
return user.(*db.User)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -104,6 +110,20 @@ func deleteCsrfCookie(ctx echo.Context) {
|
||||
ctx.SetCookie(&http.Cookie{Name: "_csrf", Path: "/", MaxAge: -1})
|
||||
}
|
||||
|
||||
func loadSettings(ctx echo.Context) error {
|
||||
settings, err := db.GetSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, value := range settings {
|
||||
s := strings.ReplaceAll(key, "-", " ")
|
||||
s = title.String(s)
|
||||
setData(ctx, strings.ReplaceAll(s, " ", ""), value == "1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type OpengistValidator struct {
|
||||
v *validator.Validate
|
||||
}
|
||||
@@ -148,7 +168,7 @@ func validateReservedKeywords(fl validator.FieldLevel) bool {
|
||||
name := fl.Field().String()
|
||||
|
||||
restrictedNames := map[string]struct{}{}
|
||||
for _, restrictedName := range []string{"assets", "register", "login", "logout", "config", "admin", "all"} {
|
||||
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init"} {
|
||||
restrictedNames[restrictedName] = struct{}{}
|
||||
}
|
||||
|
||||
@@ -193,11 +213,11 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
|
||||
|
||||
switch labels {
|
||||
case 1:
|
||||
setData(ctx, "prevLabel", "Previous")
|
||||
setData(ctx, "nextLabel", "Next")
|
||||
setData(ctx, "prevLabel", tr(ctx, "pagination.previous"))
|
||||
setData(ctx, "nextLabel", tr(ctx, "pagination.next"))
|
||||
case 2:
|
||||
setData(ctx, "prevLabel", "Newer")
|
||||
setData(ctx, "nextLabel", "Older")
|
||||
setData(ctx, "prevLabel", tr(ctx, "pagination.newer"))
|
||||
setData(ctx, "nextLabel", tr(ctx, "pagination.older"))
|
||||
}
|
||||
|
||||
setData(ctx, "urlPage", urlPage)
|
||||
@@ -205,6 +225,11 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
|
||||
return nil
|
||||
}
|
||||
|
||||
func tr(ctx echo.Context, key string) template.HTML {
|
||||
l := getData(ctx, "locale").(*i18n.Locale)
|
||||
return l.Tr(key)
|
||||
}
|
||||
|
||||
type Argon2ID struct {
|
||||
format string
|
||||
version int
|
||||
|
||||
19
opengist.go
19
opengist.go
@@ -4,11 +4,12 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"opengist/internal/config"
|
||||
"opengist/internal/git"
|
||||
"opengist/internal/models"
|
||||
"opengist/internal/ssh"
|
||||
"opengist/internal/web"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/memdb"
|
||||
"github.com/thomiceli/opengist/internal/ssh"
|
||||
"github.com/thomiceli/opengist/internal/web"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
@@ -51,17 +52,19 @@ func initialize() {
|
||||
}
|
||||
|
||||
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
|
||||
if err := models.Setup(filepath.Join(homePath, config.C.DBFilename)); err != nil {
|
||||
if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize database")
|
||||
}
|
||||
|
||||
web.EmbedFS = dirFS
|
||||
if err := memdb.Setup(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize in memory database")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
initialize()
|
||||
|
||||
go web.Start()
|
||||
go web.NewServer(os.Getenv("OG_DEV") == "1").Start()
|
||||
go ssh.Start()
|
||||
|
||||
select {}
|
||||
|
||||
214
package-lock.json
generated
214
package-lock.json
generated
@@ -7,9 +7,6 @@
|
||||
"": {
|
||||
"name": "opengist",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"nodemon": "^2.0.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.2.2",
|
||||
"@codemirror/lang-javascript": "^6.1.4",
|
||||
@@ -22,16 +19,19 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"codemirror": "^6.0.1",
|
||||
"cssnano": "^5.1.15",
|
||||
"dayjs": "^1.11.9",
|
||||
"github-markdown-css": "^5.2.0",
|
||||
"highlight.js": "^11.7.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"moment": "^2.29.3",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss-cssnext": "^3.1.1",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"sass": "^1.62.1",
|
||||
"sugarss": "^4.0.1",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"vite": "^4.2.1"
|
||||
"vite": "^4.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -916,7 +916,8 @@
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
@@ -993,6 +994,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
@@ -1066,6 +1068,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1080,6 +1083,7 @@
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -1088,12 +1092,14 @@
|
||||
"node_modules/brace-expansion/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
@@ -1200,6 +1206,7 @@
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -1226,6 +1233,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
@@ -1326,7 +1334,8 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "2.6.12",
|
||||
@@ -1537,10 +1546,17 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.9",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
|
||||
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
@@ -1859,6 +1875,7 @@
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -1890,6 +1907,7 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1905,6 +1923,15 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/github-markdown-css": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.2.0.tgz",
|
||||
"integrity": "sha512-hq5RaCInSUZ48bImOZpkppW2/MT44StRgsbsZ8YA4vJFwLKB/Vo3k7R2t+pUGqO+ThG0QDMi96TewV/B3vyItg==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -1947,6 +1974,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -1963,7 +1991,14 @@
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
|
||||
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
@@ -1997,6 +2032,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
@@ -2020,6 +2056,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2028,6 +2065,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
@@ -2039,6 +2077,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@@ -2333,6 +2372,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -2346,19 +2386,11 @@
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.4",
|
||||
@@ -2389,6 +2421,7 @@
|
||||
"version": "2.0.22",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
|
||||
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^3.2.7",
|
||||
@@ -2416,6 +2449,7 @@
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@@ -2424,6 +2458,7 @@
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
||||
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
@@ -2438,6 +2473,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2554,6 +2590,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -4085,7 +4122,8 @@
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.0",
|
||||
@@ -4152,6 +4190,7 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
@@ -4305,6 +4344,23 @@
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
|
||||
"integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
@@ -4368,6 +4424,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"semver": "~7.0.0"
|
||||
},
|
||||
@@ -4379,6 +4436,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
|
||||
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -4461,6 +4519,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
@@ -4660,6 +4719,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
@@ -4671,6 +4731,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
|
||||
"integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"nopt": "~1.0.10"
|
||||
},
|
||||
@@ -4687,7 +4748,8 @@
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uniq": {
|
||||
"version": "1.0.1",
|
||||
@@ -4754,9 +4816,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz",
|
||||
"integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.2.3.tgz",
|
||||
"integrity": "sha512-kLU+m2q0Y434Y1kCy3TchefAdtFso0ILi0dLyFV8Us3InXTU11H/B5ZTqCKIQHzSKNxVG/yEx813EA9f1imQ9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.17.5",
|
||||
@@ -5570,7 +5632,8 @@
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
"version": "7.4.1",
|
||||
@@ -5629,6 +5692,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
@@ -5679,7 +5743,8 @@
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"boolbase": {
|
||||
"version": "1.0.0",
|
||||
@@ -5691,6 +5756,7 @@
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -5699,7 +5765,8 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5707,6 +5774,7 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
@@ -5775,6 +5843,7 @@
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
@@ -5790,6 +5859,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
@@ -5882,7 +5952,8 @@
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.6.12",
|
||||
@@ -6040,10 +6111,17 @@
|
||||
"css-tree": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.9",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
|
||||
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
@@ -6298,6 +6376,7 @@
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
@@ -6318,6 +6397,7 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
@@ -6326,6 +6406,12 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"github-markdown-css": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.2.0.tgz",
|
||||
"integrity": "sha512-hq5RaCInSUZ48bImOZpkppW2/MT44StRgsbsZ8YA4vJFwLKB/Vo3k7R2t+pUGqO+ThG0QDMi96TewV/B3vyItg==",
|
||||
"dev": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -6361,7 +6447,8 @@
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "11.7.0",
|
||||
@@ -6372,7 +6459,14 @@
|
||||
"ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
|
||||
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
|
||||
"dev": true
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.3.0",
|
||||
@@ -6400,6 +6494,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
@@ -6416,12 +6511,14 @@
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
@@ -6429,7 +6526,8 @@
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"isnumeric": {
|
||||
"version": "0.2.0",
|
||||
@@ -6675,6 +6773,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -6685,16 +6784,11 @@
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.4",
|
||||
@@ -6719,6 +6813,7 @@
|
||||
"version": "2.0.22",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
|
||||
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^3.2.7",
|
||||
@@ -6735,7 +6830,8 @@
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6743,6 +6839,7 @@
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
||||
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"abbrev": "1"
|
||||
}
|
||||
@@ -6750,7 +6847,8 @@
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-range": {
|
||||
"version": "0.1.2",
|
||||
@@ -6833,7 +6931,8 @@
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "2.3.0",
|
||||
@@ -8064,7 +8163,8 @@
|
||||
"pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.3.0",
|
||||
@@ -8108,6 +8208,7 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
@@ -8206,6 +8307,17 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
|
||||
"integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
@@ -8258,6 +8370,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "~7.0.0"
|
||||
},
|
||||
@@ -8265,7 +8378,8 @@
|
||||
"semver": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
|
||||
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A=="
|
||||
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8325,6 +8439,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
@@ -8458,6 +8573,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
@@ -8466,6 +8582,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
|
||||
"integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nopt": "~1.0.10"
|
||||
}
|
||||
@@ -8479,7 +8596,8 @@
|
||||
"undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true
|
||||
},
|
||||
"uniq": {
|
||||
"version": "1.0.1",
|
||||
@@ -8530,9 +8648,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"vite": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz",
|
||||
"integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.2.3.tgz",
|
||||
"integrity": "sha512-kLU+m2q0Y434Y1kCy3TchefAdtFso0ILi0dLyFV8Us3InXTU11H/B5ZTqCKIQHzSKNxVG/yEx813EA9f1imQ9A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.17.5",
|
||||
|
||||
@@ -19,16 +19,18 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"codemirror": "^6.0.1",
|
||||
"cssnano": "^5.1.15",
|
||||
"dayjs": "^1.11.9",
|
||||
"github-markdown-css": "^5.2.0",
|
||||
"highlight.js": "^11.7.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"moment": "^2.29.3",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss-cssnext": "^3.1.1",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"sass": "^1.62.1",
|
||||
"sugarss": "^4.0.1",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"vite": "^4.2.1"
|
||||
"vite": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
35
public/admin.ts
Normal file
35
public/admin.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let elems = Array.from(document.getElementsByClassName("toggle-button"));
|
||||
for (let elem of elems) {
|
||||
elem.addEventListener('click', () => {
|
||||
registerDomSetting(elem as HTMLElement)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const setSetting = (key: string, value: string) => {
|
||||
const data = new URLSearchParams();
|
||||
data.append('key', key);
|
||||
data.append('value', value);
|
||||
if (document.getElementsByName('_csrf').length !== 0) {
|
||||
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
|
||||
}
|
||||
return fetch('/admin-panel/set-config', {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
body: data,
|
||||
});
|
||||
};
|
||||
|
||||
const registerDomSetting = (el: HTMLElement) => {
|
||||
// @ts-ignore
|
||||
el.dataset["bool"] = !(el.dataset["bool"] === 'true');
|
||||
setSetting(el.id, el.dataset["bool"] === 'true' ? '1' : '0')
|
||||
.then(() => {
|
||||
el.classList.toggle("bg-primary-600");
|
||||
el.classList.toggle("dark:bg-gray-400");
|
||||
el.classList.toggle("bg-gray-300");
|
||||
(el.childNodes.item(1) as HTMLElement).classList.toggle("translate-x-5");
|
||||
});
|
||||
};
|
||||
|
||||
BIN
public/default.png
Normal file
BIN
public/default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon-32.png
Normal file
BIN
public/favicon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 294 B |
8
public/fs_embed.go
Normal file
8
public/fs_embed.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build fs_embed
|
||||
|
||||
package public
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed manifest.json assets/*.js assets/*.css assets/*.svg assets/*.png
|
||||
var Files embed.FS
|
||||
7
public/fs_os.go
Normal file
7
public/fs_os.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !fs_embed
|
||||
|
||||
package public
|
||||
|
||||
import "os"
|
||||
|
||||
var Files = os.DirFS(".")
|
||||
50
public/hljs.ts
Normal file
50
public/hljs.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import hljs from 'highlight.js';
|
||||
import md from 'markdown-it';
|
||||
|
||||
document.querySelectorAll('.markdown').forEach((e: HTMLElement) => {
|
||||
e.innerHTML = md({
|
||||
html: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return '<pre class="hljs"><code>' +
|
||||
hljs.highlight(str, {language: lang, ignoreIllegals: true}).value +
|
||||
'</code></pre>';
|
||||
} catch (__) {
|
||||
}
|
||||
}
|
||||
|
||||
return '<pre class="hljs"><code>' + md().utils.escapeHtml(str) + '</code></pre>';
|
||||
}
|
||||
}).render(e.textContent);
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
|
||||
const ext = el.dataset.filename?.split('.').pop() || '';
|
||||
|
||||
if (hljs.autoDetection(ext) && ext !== 'txt') {
|
||||
el.querySelectorAll<HTMLElement>('td.line-code').forEach((ell) => {
|
||||
ell.classList.add('language-' + ext);
|
||||
hljs.highlightElement(ell);
|
||||
});
|
||||
}
|
||||
|
||||
el.addEventListener('click', event => {
|
||||
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
|
||||
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
||||
|
||||
const nextSibling = (event.target as HTMLElement).nextSibling;
|
||||
if (nextSibling instanceof HTMLElement) {
|
||||
nextSibling.classList.add('selected');
|
||||
}
|
||||
|
||||
|
||||
const filename = el.dataset.filenameSlug;
|
||||
const line = (event.target as HTMLElement).textContent;
|
||||
const url = location.protocol + '//' + location.host + location.pathname;
|
||||
const hash = '#file-' + filename + '-' + line;
|
||||
window.history.pushState(null, null, url + hash);
|
||||
location.hash = hash;
|
||||
}
|
||||
});
|
||||
});
|
||||
111
public/main.ts
111
public/main.ts
@@ -1,19 +1,57 @@
|
||||
import './style.css';
|
||||
import './markdown.css';
|
||||
import './favicon.svg';
|
||||
import 'highlight.js/styles/tokyo-night-dark.css';
|
||||
import moment from 'moment';
|
||||
import md from 'markdown-it';
|
||||
import hljs from 'highlight.js';
|
||||
import './style.scss';
|
||||
import './favicon-32.png';
|
||||
import './opengist.svg';
|
||||
import './default.png';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeMenu = document.getElementById('theme-menu')!;
|
||||
|
||||
document.getElementById('light-mode')!.onclick = (e) => {
|
||||
e.stopPropagation()
|
||||
localStorage.theme = 'light';
|
||||
themeMenu.classList.toggle('hidden');
|
||||
// @ts-ignore
|
||||
checkTheme()
|
||||
}
|
||||
|
||||
document.getElementById('dark-mode')!.onclick = (e) => {
|
||||
e.stopPropagation()
|
||||
localStorage.theme = 'dark';
|
||||
themeMenu.classList.toggle('hidden');
|
||||
// @ts-ignore
|
||||
checkTheme()
|
||||
}
|
||||
|
||||
document.getElementById('system-mode')!.onclick = (e) => {
|
||||
e.stopPropagation()
|
||||
localStorage.removeItem('theme');
|
||||
themeMenu.classList.toggle('hidden');
|
||||
// @ts-ignore
|
||||
checkTheme();
|
||||
}
|
||||
|
||||
document.getElementById('theme-btn')!.onclick = () => {
|
||||
themeMenu.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('user-btn')?.addEventListener("click" , () => {
|
||||
document.getElementById('user-menu').classList.toggle('hidden');
|
||||
})
|
||||
|
||||
document.querySelectorAll('.moment-timestamp').forEach((e: HTMLElement) => {
|
||||
e.title = moment.unix(parseInt(e.innerHTML)).format('LLLL');
|
||||
e.innerHTML = moment.unix(parseInt(e.innerHTML)).fromNow();
|
||||
e.title = dayjs.unix(parseInt(e.innerHTML)).format('LLLL');
|
||||
e.innerHTML = dayjs.unix(parseInt(e.innerHTML)).fromNow();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.moment-timestamp-date').forEach((e: HTMLElement) => {
|
||||
e.innerHTML = moment.unix(parseInt(e.innerHTML)).format('DD/MM/YYYY HH:mm');
|
||||
e.innerHTML = dayjs.unix(parseInt(e.innerHTML)).format('DD/MM/YYYY HH:mm');
|
||||
});
|
||||
|
||||
const rev = document.querySelector<HTMLElement>('.revision-text');
|
||||
@@ -30,39 +68,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
}
|
||||
|
||||
document.querySelectorAll('.markdown').forEach((e: HTMLElement) => {
|
||||
e.innerHTML = md().render(e.innerHTML);
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
|
||||
const ext = el.dataset.filename?.split('.').pop() || '';
|
||||
|
||||
if (hljs.autoDetection(ext) && ext !== 'txt') {
|
||||
el.querySelectorAll<HTMLElement>('td.line-code').forEach((ell) => {
|
||||
ell.classList.add('language-' + ext);
|
||||
hljs.highlightElement(ell);
|
||||
});
|
||||
}
|
||||
|
||||
el.addEventListener('click', event => {
|
||||
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
|
||||
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
||||
|
||||
const nextSibling = (event.target as HTMLElement).nextSibling;
|
||||
if (nextSibling instanceof HTMLElement) {
|
||||
nextSibling.classList.add('selected');
|
||||
}
|
||||
|
||||
|
||||
const filename = el.dataset.filenameSlug;
|
||||
const line = (event.target as HTMLElement).textContent;
|
||||
const url = location.protocol + '//' + location.host + location.pathname;
|
||||
const hash = '#file-' + filename + '-' + line;
|
||||
window.history.pushState(null, null, url + hash);
|
||||
location.hash = hash;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const colorhash = () => {
|
||||
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
||||
@@ -130,11 +135,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('language-btn')!.onclick = () => {
|
||||
document.getElementById('language-list')!.classList.toggle('hidden');
|
||||
};
|
||||
|
||||
|
||||
document.querySelectorAll('.copy-gist-btn').forEach((e: HTMLElement) => {
|
||||
e.onclick = () => {
|
||||
navigator.clipboard.writeText(e.parentNode!.querySelector<HTMLElement>('.gist-content')!.textContent || '').catch((err) => {
|
||||
navigator.clipboard.writeText(e.parentNode!.parentNode!.querySelector<HTMLElement>('.gist-content')!.textContent || '').catch((err) => {
|
||||
console.error('Could not copy text: ', err);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const gistmenuvisibility = document.getElementById('gist-menu-visibility');
|
||||
if (gistmenuvisibility) {
|
||||
let submitgistbutton = (document.getElementById('submit-gist') as HTMLInputElement);
|
||||
document.getElementById('gist-visibility-menu-button')!.onclick = () => {
|
||||
gistmenuvisibility!.classList.toggle('hidden');
|
||||
}
|
||||
Array.from(document.querySelectorAll('.gist-visibility-option')).forEach((el) => {
|
||||
(el as HTMLElement).onclick = () => {
|
||||
submitgistbutton.textContent = (el as HTMLElement).dataset.btntext;
|
||||
submitgistbutton!.value = (el as HTMLElement).dataset.visibility || '0';
|
||||
gistmenuvisibility!.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
942
public/markdown.css
vendored
942
public/markdown.css
vendored
@@ -1,942 +0,0 @@
|
||||
/* https://github.com/sindresorhus/github-markdown-css/blob/main/github-markdown-dark.css */
|
||||
|
||||
.markdown-body {
|
||||
color-scheme: dark;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
margin: 0;
|
||||
color: #c9d1d9;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.markdown-body .octicon {
|
||||
display: inline-block;
|
||||
fill: currentColor;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.markdown-body h1:hover .anchor .octicon-link:before,
|
||||
.markdown-body h2:hover .anchor .octicon-link:before,
|
||||
.markdown-body h3:hover .anchor .octicon-link:before,
|
||||
.markdown-body h4:hover .anchor .octicon-link:before,
|
||||
.markdown-body h5:hover .anchor .octicon-link:before,
|
||||
.markdown-body h6:hover .anchor .octicon-link:before {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
|
||||
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
|
||||
}
|
||||
|
||||
.markdown-body details,
|
||||
.markdown-body figcaption,
|
||||
.markdown-body figure {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.markdown-body summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
.markdown-body [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
background-color: transparent;
|
||||
color: #58a6ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body a:active,
|
||||
.markdown-body a:hover {
|
||||
outline-width: 0;
|
||||
}
|
||||
|
||||
.markdown-body abbr[title] {
|
||||
border-bottom: none;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
.markdown-body b,
|
||||
.markdown-body strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
margin: .67em 0;
|
||||
font-weight: 600;
|
||||
padding-bottom: .3em;
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #21262d;
|
||||
}
|
||||
|
||||
.markdown-body mark {
|
||||
background-color: rgba(187,128,9,0.15);
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.markdown-body small {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.markdown-body sub,
|
||||
.markdown-body sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.markdown-body sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
.markdown-body sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
.markdown-body img {
|
||||
border-style: none;
|
||||
max-width: 100%;
|
||||
box-sizing: content-box;
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
.markdown-body code,
|
||||
.markdown-body kbd,
|
||||
.markdown-body pre,
|
||||
.markdown-body samp {
|
||||
font-family: monospace,monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.markdown-body figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
box-sizing: content-box;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid #21262d;
|
||||
height: .25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: #30363d;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body input {
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.markdown-body [type=button],
|
||||
.markdown-body [type=reset],
|
||||
.markdown-body [type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
.markdown-body [type=button]::-moz-focus-inner,
|
||||
.markdown-body [type=reset]::-moz-focus-inner,
|
||||
.markdown-body [type=submit]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body [type=button]:-moz-focusring,
|
||||
.markdown-body [type=reset]:-moz-focusring,
|
||||
.markdown-body [type=submit]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
.markdown-body [type=checkbox],
|
||||
.markdown-body [type=radio] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body [type=number]::-webkit-inner-spin-button,
|
||||
.markdown-body [type=number]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.markdown-body [type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.markdown-body [type=search]::-webkit-search-cancel-button,
|
||||
.markdown-body [type=search]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.markdown-body ::-webkit-input-placeholder {
|
||||
color: inherit;
|
||||
opacity: .54;
|
||||
}
|
||||
|
||||
.markdown-body ::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.markdown-body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body hr::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body hr::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-body td,
|
||||
.markdown-body th {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body details summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown-body details:not([open])>*:not(summary) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.markdown-body kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
||||
line-height: 10px;
|
||||
color: #c9d1d9;
|
||||
vertical-align: middle;
|
||||
background-color: #161b22;
|
||||
border: solid 1px rgba(110,118,129,0.4);
|
||||
border-bottom-color: rgba(110,118,129,0.4);
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 -1px 0 rgba(110,118,129,0.4);
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-weight: 600;
|
||||
padding-bottom: .3em;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #21262d;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-weight: 600;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.markdown-body h4 {
|
||||
font-weight: 600;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.markdown-body h5 {
|
||||
font-weight: 600;
|
||||
font-size: .875em;
|
||||
}
|
||||
|
||||
.markdown-body h6 {
|
||||
font-weight: 600;
|
||||
font-size: .85em;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
color: #8b949e;
|
||||
border-left: .25em solid #30363d;
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.markdown-body ol ol,
|
||||
.markdown-body ul ol {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
.markdown-body ul ul ol,
|
||||
.markdown-body ul ol ol,
|
||||
.markdown-body ol ul ol,
|
||||
.markdown-body ol ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
.markdown-body dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.markdown-body tt,
|
||||
.markdown-body code {
|
||||
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
||||
font-size: 12px;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
.markdown-body .octicon {
|
||||
display: inline-block;
|
||||
overflow: visible !important;
|
||||
vertical-align: text-bottom;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.markdown-body ::placeholder {
|
||||
color: #484f58;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.markdown-body input::-webkit-outer-spin-button,
|
||||
.markdown-body input::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.markdown-body .pl-c {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.markdown-body .pl-c1,
|
||||
.markdown-body .pl-s .pl-v {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.markdown-body .pl-e,
|
||||
.markdown-body .pl-en {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
|
||||
.markdown-body .pl-smi,
|
||||
.markdown-body .pl-s .pl-s1 {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.markdown-body .pl-ent {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.markdown-body .pl-k {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.markdown-body .pl-s,
|
||||
.markdown-body .pl-pds,
|
||||
.markdown-body .pl-s .pl-pse .pl-s1,
|
||||
.markdown-body .pl-sr,
|
||||
.markdown-body .pl-sr .pl-cce,
|
||||
.markdown-body .pl-sr .pl-sre,
|
||||
.markdown-body .pl-sr .pl-sra {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.markdown-body .pl-v,
|
||||
.markdown-body .pl-smw {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.markdown-body .pl-bu {
|
||||
color: #f85149;
|
||||
}
|
||||
|
||||
.markdown-body .pl-ii {
|
||||
color: #f0f6fc;
|
||||
background-color: #8e1519;
|
||||
}
|
||||
|
||||
.markdown-body .pl-c2 {
|
||||
color: #f0f6fc;
|
||||
background-color: #b62324;
|
||||
}
|
||||
|
||||
.markdown-body .pl-sr .pl-cce {
|
||||
font-weight: bold;
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.markdown-body .pl-ml {
|
||||
color: #f2cc60;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mh,
|
||||
.markdown-body .pl-mh .pl-en,
|
||||
.markdown-body .pl-ms {
|
||||
font-weight: bold;
|
||||
color: #1f6feb;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mi {
|
||||
font-style: italic;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mb {
|
||||
font-weight: bold;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.markdown-body .pl-md {
|
||||
color: #ffdcd7;
|
||||
background-color: #67060c;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mi1 {
|
||||
color: #aff5b4;
|
||||
background-color: #033a16;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mc {
|
||||
color: #ffdfb6;
|
||||
background-color: #5a1e02;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mi2 {
|
||||
color: #c9d1d9;
|
||||
background-color: #1158c7;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mdr {
|
||||
font-weight: bold;
|
||||
color: #d2a8ff;
|
||||
}
|
||||
|
||||
.markdown-body .pl-ba {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.markdown-body .pl-sg {
|
||||
color: #484f58;
|
||||
}
|
||||
|
||||
.markdown-body .pl-corl {
|
||||
text-decoration: underline;
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.markdown-body [data-catalyst] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.markdown-body g-emoji {
|
||||
font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
||||
font-size: 1em;
|
||||
font-style: normal !important;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
vertical-align: -0.075em;
|
||||
}
|
||||
|
||||
.markdown-body g-emoji img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.markdown-body::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body>*:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-body>*:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-body a:not([href]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body .absent {
|
||||
color: #f85149;
|
||||
}
|
||||
|
||||
.markdown-body .anchor {
|
||||
float: left;
|
||||
padding-right: 4px;
|
||||
margin-left: -20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.markdown-body .anchor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.markdown-body p,
|
||||
.markdown-body blockquote,
|
||||
.markdown-body ul,
|
||||
.markdown-body ol,
|
||||
.markdown-body dl,
|
||||
.markdown-body table,
|
||||
.markdown-body pre,
|
||||
.markdown-body details {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body blockquote>:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-body blockquote>:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body sup>a::before {
|
||||
content: "[";
|
||||
}
|
||||
|
||||
.markdown-body sup>a::after {
|
||||
content: "]";
|
||||
}
|
||||
|
||||
.markdown-body h1 .octicon-link,
|
||||
.markdown-body h2 .octicon-link,
|
||||
.markdown-body h3 .octicon-link,
|
||||
.markdown-body h4 .octicon-link,
|
||||
.markdown-body h5 .octicon-link,
|
||||
.markdown-body h6 .octicon-link {
|
||||
color: #c9d1d9;
|
||||
vertical-align: middle;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.markdown-body h1:hover .anchor,
|
||||
.markdown-body h2:hover .anchor,
|
||||
.markdown-body h3:hover .anchor,
|
||||
.markdown-body h4:hover .anchor,
|
||||
.markdown-body h5:hover .anchor,
|
||||
.markdown-body h6:hover .anchor {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body h1:hover .anchor .octicon-link,
|
||||
.markdown-body h2:hover .anchor .octicon-link,
|
||||
.markdown-body h3:hover .anchor .octicon-link,
|
||||
.markdown-body h4:hover .anchor .octicon-link,
|
||||
.markdown-body h5:hover .anchor .octicon-link,
|
||||
.markdown-body h6:hover .anchor .octicon-link {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.markdown-body h1 tt,
|
||||
.markdown-body h1 code,
|
||||
.markdown-body h2 tt,
|
||||
.markdown-body h2 code,
|
||||
.markdown-body h3 tt,
|
||||
.markdown-body h3 code,
|
||||
.markdown-body h4 tt,
|
||||
.markdown-body h4 code,
|
||||
.markdown-body h5 tt,
|
||||
.markdown-body h5 code,
|
||||
.markdown-body h6 tt,
|
||||
.markdown-body h6 code {
|
||||
padding: 0 .2em;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.markdown-body ul.no-list,
|
||||
.markdown-body ol.no-list {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.markdown-body ol[type="1"] {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.markdown-body ol[type=a] {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
.markdown-body ol[type=i] {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
.markdown-body div>ol:not([type]) {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.markdown-body ul ul,
|
||||
.markdown-body ul ol,
|
||||
.markdown-body ol ol,
|
||||
.markdown-body ol ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body li>p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.markdown-body li+li {
|
||||
margin-top: .25em;
|
||||
}
|
||||
|
||||
.markdown-body dl {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body dl dt {
|
||||
padding: 0;
|
||||
margin-top: 16px;
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body dl dd {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body table th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body table th,
|
||||
.markdown-body table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #30363d;
|
||||
}
|
||||
|
||||
.markdown-body table tr {
|
||||
background-color: #0d1117;
|
||||
border-top: 1px solid #21262d;
|
||||
}
|
||||
|
||||
.markdown-body table tr:nth-child(2n) {
|
||||
background-color: #161b22;
|
||||
}
|
||||
|
||||
.markdown-body table img {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.markdown-body img[align=right] {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.markdown-body img[align=left] {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.markdown-body .emoji {
|
||||
max-width: none;
|
||||
vertical-align: text-top;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.markdown-body span.frame {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-body span.frame>span {
|
||||
display: block;
|
||||
float: left;
|
||||
width: auto;
|
||||
padding: 7px;
|
||||
margin: 13px 0 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid #30363d;
|
||||
}
|
||||
|
||||
.markdown-body span.frame span img {
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.markdown-body span.frame span span {
|
||||
display: block;
|
||||
padding: 5px 0 0;
|
||||
clear: both;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.markdown-body span.align-center {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown-body span.align-center>span {
|
||||
display: block;
|
||||
margin: 13px auto 0;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown-body span.align-center span img {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown-body span.align-right {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown-body span.align-right>span {
|
||||
display: block;
|
||||
margin: 13px 0 0;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.markdown-body span.align-right span img {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.markdown-body span.float-left {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-body span.float-left span {
|
||||
margin: 13px 0 0;
|
||||
}
|
||||
|
||||
.markdown-body span.float-right {
|
||||
display: block;
|
||||
float: right;
|
||||
margin-left: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-body span.float-right>span {
|
||||
display: block;
|
||||
margin: 13px auto 0;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.markdown-body code,
|
||||
.markdown-body tt {
|
||||
padding: .2em .4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(110,118,129,0.4);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.markdown-body code br,
|
||||
.markdown-body tt br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown-body del code {
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.markdown-body pre>code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body .highlight {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body .highlight pre {
|
||||
margin-bottom: 0;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.markdown-body .highlight pre,
|
||||
.markdown-body pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: #161b22;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.markdown-body pre code,
|
||||
.markdown-body pre tt {
|
||||
display: inline;
|
||||
max-width: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body .csv-data td,
|
||||
.markdown-body .csv-data th {
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.markdown-body .csv-data .blob-num {
|
||||
padding: 10px 8px 9px;
|
||||
text-align: right;
|
||||
background: #0d1117;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body .csv-data tr {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.markdown-body .csv-data th {
|
||||
font-weight: 600;
|
||||
background: #161b22;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.markdown-body .footnotes {
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
border-top: 1px solid #30363d;
|
||||
}
|
||||
|
||||
.markdown-body .footnotes ol {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.markdown-body .footnotes li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-body .footnotes li:target::before {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
bottom: -8px;
|
||||
left: -24px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
border: 2px solid #1f6feb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.markdown-body .footnotes li:target {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.markdown-body .footnotes .data-footnote-backref g-emoji {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item label {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item.enabled label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item+.task-list-item {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item .handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item-checkbox {
|
||||
margin: 0 .2em .25em -1.6em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
|
||||
margin: 0 -1.6em .25em .2em;
|
||||
}
|
||||
|
||||
.markdown-body ::-webkit-calendar-picker-indicator {
|
||||
filter: invert(50%);
|
||||
}
|
||||
29
public/opengist.svg
Normal file
29
public/opengist.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="document" transform="scale(1.6666666666666667 1.6666666666666667) translate(150.0 150.0)">
|
||||
<path class="st0" d="M131.3,24.3c13.7-71-33.9-139.5-106.4-152.9C-47.7-142-117.6-95.3-131.3-24.3s33.9,139.5,106.4,152.9
|
||||
C47.7,142,117.6,95.3,131.3,24.3z"/>
|
||||
<path class="st0" d="M128.9,0c0,55.7-36.8,103-88,119.8c0.2-1.2,0.3-2.5,0.3-4c0.1-22.3,0.2-36.2,0.2-52.8
|
||||
c0-11.7-0.2-18.1-0.2-18.1c1.8,0,21.1-6,29.9-12.1S89.2,15.1,90.5-1.4c1.3-16.6-6-36.2-12.4-47.8C65.3-72.4,54.7-86.6,45.4-94.5
|
||||
c-9.3-7.8-16.1-6.1-22.1-1.4S8.5-76.9,2.2-71.2c-3,2.8-10.6,12-20.4,3.3C-21-70.3-38-93.6-48.5-90.6c-13.1,3.7-28.1,27.3-35.1,43.8
|
||||
c-9,21-10.8,33.6-6.1,63.5c4.7,29.9,7.5,60,11.8,76.4c1,4,2.3,7.4,4,10.4c-33.2-22.8-55-60.7-55-103.5
|
||||
c0-69.7,57.7-126.3,128.9-126.3S128.9-69.7,128.9,0z"/>
|
||||
<path d="M0-145c-81.8,0-148.1,64.9-148.1,145S-81.8,145,0,145S148.1,80.1,148.1,0S81.8-145,0-145z M40.9,119.8
|
||||
c0.2-1.2,0.3-2.5,0.3-4c0.1-22.3,0.2-36.2,0.2-52.8c0-11.7-0.2-18.1-0.2-18.1c1.8,0,21.1-6,29.9-12.1S89.2,15.1,90.5-1.4
|
||||
c1.3-16.6-6-36.2-12.4-47.8C65.3-72.4,54.7-86.6,45.4-94.5c-9.3-7.8-16.1-6.1-22.1-1.4S8.5-76.9,2.2-71.2c-3,2.8-10.6,12-20.4,3.3
|
||||
C-21-70.3-38-93.6-48.5-90.6c-13.1,3.7-28.1,27.3-35.1,43.8c-9,21-10.8,33.6-6.1,63.5c4.7,29.9,7.5,60,11.8,76.4
|
||||
c1,4,2.3,7.4,4,10.4c-33.2-22.8-55-60.7-55-103.5c0-69.7,57.7-126.3,128.9-126.3S128.9-69.7,128.9,0
|
||||
C128.9,55.7,92.1,103,40.9,119.8z"/>
|
||||
<path class="st0" d="M-102.8-7.2l91.2-9.4l-0.3-7l-91.2,9.4L-102.8-7.2z"/>
|
||||
<path class="st0" d="M12-17.3c0.8-9.6-6.5-18-16.3-18.8s-18.4,6.4-19.2,16S-17-2.1-7.2-1.3S11.2-7.7,12-17.3z"/>
|
||||
<path class="st0" d="M62.9-24.6c0.8-9.6-6.5-18-16.3-18.8c-9.8-0.8-18.4,6.4-19.2,16c-0.8,9.6,6.5,18,16.3,18.8S62.1-15,62.9-24.6z
|
||||
"/>
|
||||
<path class="st0" d="M-11.8-16.8l67.6-7.3l-0.5-6.3l-67.5,7.3L-11.8-16.8z"/>
|
||||
<path class="st0" d="M53.1-23.6l49.5-12.2l-0.6-6.3L52.5-29.9L53.1-23.6z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
51
public/style.css
vendored
51
public/style.css
vendored
@@ -9,23 +9,23 @@
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-gray-800;
|
||||
@apply bg-gray-50 dark:bg-gray-800;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-primary-500;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
@apply text-primary-600;
|
||||
p a:hover, h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover, h6 a:hover {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply placeholder-gray-400;
|
||||
@apply placeholder-gray-300 dark:placeholder-gray-400;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"], pre[class*="language-"] {
|
||||
@apply bg-gray-900 mt-1 pt-1 !important;
|
||||
@apply bg-white dark:bg-gray-900 mt-1 pt-1 !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
@@ -63,13 +63,12 @@ pre {
|
||||
}
|
||||
|
||||
.cm-line, .cm-gutter {
|
||||
@apply bg-gray-900 !important;
|
||||
caret-color: white !important;
|
||||
@apply bg-white dark:bg-gray-900 dark:caret-white caret-slate-700 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.cm-activeLine, .cm-activeLineGutter {
|
||||
@apply bg-gray-800 !important;
|
||||
@apply bg-gray-50 dark:bg-gray-800 !important;
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
@@ -77,7 +76,7 @@ pre {
|
||||
}
|
||||
|
||||
.cm-gutterElement {
|
||||
@apply text-gray-300 px-4 !important
|
||||
@apply text-gray-700 dark:text-gray-300 px-4 !important
|
||||
}
|
||||
|
||||
.code td {
|
||||
@@ -100,20 +99,26 @@ pre {
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background: none !important;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.line-code.selected {
|
||||
background-color: rgba(65, 25, 63, 0.46) !important;
|
||||
box-shadow: inset 4px 0 0 rgb(107, 38, 102) !important;
|
||||
background-color: rgb(255, 247, 190) !important;
|
||||
box-shadow: inset 4px 0 0 rgb(255, 213, 65) !important;
|
||||
}
|
||||
|
||||
.dark .line-code.selected {
|
||||
background-color: rgb(54, 49, 32) !important;
|
||||
box-shadow: inset 4px 0 0 rgb(161, 128, 21) !important;
|
||||
}
|
||||
|
||||
.line-code {
|
||||
@apply pl-2;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.line-num {
|
||||
@apply cursor-pointer text-slate-400 hover:text-white;
|
||||
@apply cursor-pointer text-slate-600 dark:text-slate-400 hover:text-black dark:hover:text-white;
|
||||
}
|
||||
|
||||
table.csv-table {
|
||||
@@ -125,13 +130,25 @@ table.csv-table thead {
|
||||
}
|
||||
|
||||
table.csv-table thead tr {
|
||||
@apply bg-slate-800;
|
||||
@apply bg-slate-100 dark:bg-slate-800;
|
||||
}
|
||||
|
||||
table.csv-table thead tr th {
|
||||
@apply border py-2 px-1 border-slate-700;
|
||||
@apply border py-2 px-1 border-slate-300 dark:border-slate-700;
|
||||
}
|
||||
|
||||
table.csv-table tbody td {
|
||||
@apply border py-1.5 px-1 border-slate-800;
|
||||
}
|
||||
@apply border py-1.5 px-1 border-slate-200 dark:border-slate-800;
|
||||
}
|
||||
|
||||
dl.dl-config {
|
||||
@apply grid grid-cols-3 text-sm;
|
||||
}
|
||||
|
||||
dl.dl-config dt {
|
||||
@apply col-span-1 text-gray-700 dark:text-slate-300 font-bold;
|
||||
}
|
||||
|
||||
dl.dl-config dd {
|
||||
@apply ml-1 col-span-2 break-words;
|
||||
}
|
||||
|
||||
9
public/style.scss
vendored
Normal file
9
public/style.scss
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
:root {
|
||||
@import "github-markdown-css/github-markdown-light";
|
||||
@import 'highlight.js/scss/base16/one-light.scss';
|
||||
}
|
||||
|
||||
.dark {
|
||||
@import "github-markdown-css/github-markdown-dark";
|
||||
@import 'highlight.js/scss/base16/onedark.scss';
|
||||
}
|
||||
64
scripts/build-all.sh
Executable file
64
scripts/build-all.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/bin/sh
|
||||
|
||||
CHECKSUMS_FILE="build/checksums.txt"
|
||||
BINARY_NAME="opengist"
|
||||
TARGETS="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 linux/armv6 linux/armv7 linux/386 windows/amd64"
|
||||
VERSION=$(git describe --tags | sed 's/^v//')
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not retrieve version from git tags. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for TARGET in $TARGETS; do
|
||||
GOOS=${TARGET%/*}
|
||||
GOARCH=${TARGET#*/}
|
||||
|
||||
case $GOOS-$GOARCH in
|
||||
linux-armv6)
|
||||
GOARCH="arm"
|
||||
GOARM=6
|
||||
;;
|
||||
linux-armv7)
|
||||
GOARCH="arm"
|
||||
GOARM=7
|
||||
;;
|
||||
*)
|
||||
unset GOARM
|
||||
;;
|
||||
esac
|
||||
|
||||
OUTPUT_PARENT_DIR="build/$GOOS-$GOARCH${GOARM:+v$GOARM}-$VERSION"
|
||||
OUTPUT_DIR="$OUTPUT_PARENT_DIR/$BINARY_NAME"
|
||||
OUTPUT_FILE="$OUTPUT_DIR/$BINARY_NAME"
|
||||
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
OUTPUT_FILE="$OUTPUT_FILE.exe"
|
||||
fi
|
||||
|
||||
echo "Building version $VERSION for $GOOS/$GOARCH${GOARM:+v$GOARM}..."
|
||||
mkdir -p $OUTPUT_DIR
|
||||
env GOOS=$GOOS GOARCH=$GOARCH GOARM=$GOARM CGO_ENABLED=0 go build -tags fs_embed -o $OUTPUT_FILE
|
||||
cp README.md $OUTPUT_DIR
|
||||
cp LICENSE $OUTPUT_DIR
|
||||
cp config.yml $OUTPUT_DIR
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building for $GOOS/$GOARCH${GOARM:+v$GOARM}. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Archive the binary with README and LICENSE
|
||||
echo "Archiving for $GOOS/$GOARCH${GOARM:+v$GOARM}..."
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
# ZIP for Windows
|
||||
cd $OUTPUT_PARENT_DIR && zip -r "../$BINARY_NAME$VERSION-$GOOS-$GOARCH${GOARM:+v$GOARM}.zip" "$BINARY_NAME/" && cd - > /dev/null
|
||||
sha256sum "build/$BINARY_NAME$VERSION-$GOOS-$GOARCH${GOARM:+v$GOARM}.zip" | awk '{print $1 " " substr($2,7)}' >> $CHECKSUMS_FILE
|
||||
else
|
||||
# tar.gz for other platforms
|
||||
tar -czf "build/$BINARY_NAME$VERSION-$GOOS-$GOARCH${GOARM:+v$GOARM}.tar.gz" -C $OUTPUT_PARENT_DIR "$BINARY_NAME"
|
||||
sha256sum "build/$BINARY_NAME$VERSION-$GOOS-$GOARCH${GOARM:+v$GOARM}.tar.gz" | awk '{print $1 " " substr($2,7)}' >> $CHECKSUMS_FILE
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Build and archiving complete."
|
||||
0
watch.sh → scripts/watch.sh
Normal file → Executable file
0
watch.sh → scripts/watch.sh
Normal file → Executable file
17
tailwind.config.js
vendored
17
tailwind.config.js
vendored
@@ -22,9 +22,21 @@ module.exports = {
|
||||
800: "#232429",
|
||||
900: "#131316"
|
||||
},
|
||||
emerald: colors.emerald,
|
||||
rose: colors.rose,
|
||||
primary: colors.sky,
|
||||
primary: {
|
||||
50: '#d6e1ff',
|
||||
100: '#d1dfff',
|
||||
200: '#b9d2fe',
|
||||
300: '#84b1fb',
|
||||
400: '#74a4f6',
|
||||
500: '#588fee',
|
||||
600: '#3c79e2',
|
||||
700: '#356fc0',
|
||||
800: '#2d6195',
|
||||
900: '#2a5574',
|
||||
950: '#173040',
|
||||
},
|
||||
|
||||
slate: colors.slate
|
||||
},
|
||||
extend: {
|
||||
@@ -34,4 +46,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography"),require('@tailwindcss/forms')],
|
||||
darkMode: 'class',
|
||||
}
|
||||
|
||||
18
templates/base/admin_footer.html
vendored
18
templates/base/admin_footer.html
vendored
@@ -1,9 +1,13 @@
|
||||
{{ if false }}{{/* prevent IDE errors */}}
|
||||
<div><main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "admin_footer" }}
|
||||
{{ if .urlPage }}
|
||||
<div class="flex mt-4 justify-center space-x-2">
|
||||
{{ template "pagination" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
</div>
|
||||
{{ if .urlPage }}
|
||||
<div class="flex mt-4 justify-center space-x-2">
|
||||
{{ template "pagination" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
22
templates/base/admin_header.html
vendored
22
templates/base/admin_header.html
vendored
@@ -2,21 +2,27 @@
|
||||
<div class="py-10">
|
||||
<header class="pb-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold leading-tight">Admin panel</h1>
|
||||
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "admin.admin_panel" }}</h1>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="mb-4">
|
||||
<div class="">
|
||||
<nav class="flex space-x-4" aria-label="Tabs">
|
||||
<a href="/admin" class="{{ if eq .adminHeaderPage "index" }}bg-gray-700 text-slate-300 hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-400 hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}">General</a>
|
||||
<a href="/admin/users" class="{{ if eq .adminHeaderPage "users" }}bg-gray-700 text-slate-300 hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-400 hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">Users</a>
|
||||
<a href="/admin/gists" class="{{ if eq .adminHeaderPage "gists" }}bg-gray-700 text-slate-300 hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-400 hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">Gists</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel" class="{{ if eq .adminHeaderPage "index" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}">{{ .locale.Tr "admin.general" }} </a>
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel/users" class="{{ if eq .adminHeaderPage "users" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.users" }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel/gists" class="{{ if eq .adminHeaderPage "gists" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.gists" }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel/configuration" class="{{ if eq .adminHeaderPage "config" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.configuration" }}</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ if false }}
|
||||
{{/* prevent IDE errors */}}
|
||||
</main></div>
|
||||
{{ end }}
|
||||
|
||||
40
templates/base/base_footer.html
vendored
40
templates/base/base_footer.html
vendored
@@ -1,15 +1,37 @@
|
||||
{{ define "footer" }}
|
||||
<p class="text-slate-400 py-8 [&>*]:mx-1.5 flex">
|
||||
<span>
|
||||
<a target="_blank" style="margin-left: 0 !important;" class="text-gray-500 hover:text-white inline-flex" href="https://github.com/thomiceli/opengist">
|
||||
<span class="mr-1">Source</span>
|
||||
</a>
|
||||
</span> ⋅
|
||||
<span class="text-gray-500">Load: <span class="font-bold">{{ loadedTime .loadStartTime }}</span></span>
|
||||
{{ if false }}
|
||||
{{/* prevent IDE errors */}}
|
||||
<html lang="en"><body><div><div>
|
||||
{{ end }}
|
||||
|
||||
</p>
|
||||
{{ define "footer" }}
|
||||
<div class="inline-flex py-8">
|
||||
<p class="text-slate-600 dark:text-slate-400 [&>*]:mx-1.5 flex">
|
||||
<span>
|
||||
<a target="_blank" style="margin-left: 0 !important;" class="text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 inline-flex" href="https://github.com/thomiceli/opengist">
|
||||
<span class="mr-1">{{ .locale.Tr "footer.powered-by" "<span class=\"font-bold dark:text-slate-300\">Opengist</span>" }} </span>
|
||||
</a>
|
||||
</span>⋅
|
||||
<span>Load: <span class="font-bold dark:text-slate-300">{{ loadedTime .loadStartTime }}</span></span>⋅
|
||||
</p>
|
||||
<div class="ml-1.5 cursor-pointer relative inline-block">
|
||||
<span id="language-btn" class="text-slate-600 font-bold dark:text-slate-300"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mb-1 w-5 h-5 inline-flex">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
|
||||
</svg>
|
||||
{{ .localeName }}
|
||||
</span>
|
||||
|
||||
<div id="language-list" class="hidden absolute bottom-0 z-10 mb-10 mt-2 origin-bottom-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-800 dark:ring-gray-700" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="py-1" role="none">
|
||||
{{ range .allLocales }}
|
||||
<a href="?lang={{ .Code }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-4 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1" id="menu-item-0">{{ .Name }}</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="{{ asset "hljs.ts" }}"></script>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
237
templates/base/base_header.html
vendored
237
templates/base/base_header.html
vendored
@@ -3,10 +3,39 @@
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset "favicon.svg" }}" />
|
||||
{{ if .NoIndex }}
|
||||
<meta name="robots" content="noindex, follow">
|
||||
{{ end }}
|
||||
|
||||
<script>
|
||||
const checkTheme = () => {
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
|
||||
checkTheme()
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', ({matches}) => {
|
||||
checkTheme()
|
||||
}
|
||||
)
|
||||
|
||||
</script>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset "favicon-32.png" }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="{{ asset "main.css" }}" />
|
||||
<script type="module" src="{{ asset "main.ts" }}"></script>
|
||||
|
||||
{{ if dev }}
|
||||
<script type="module" src="{{ asset "@vite/client" }}"></script>
|
||||
<link rel="stylesheet" href="{{ asset "style.css" }}" />
|
||||
<script type="module" src="{{ asset "main.ts" }}"></script>
|
||||
{{ else }}
|
||||
<link rel="stylesheet" href="{{ asset "main.css" }}" />
|
||||
<script type="module" src="{{ asset "main.ts" }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{ if .htmlTitle }}
|
||||
<title>{{ .htmlTitle }} - Opengist</title>
|
||||
@@ -15,14 +44,14 @@
|
||||
{{ end }}
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<div id="app" class="text-white min-h-full bg-gray-900">
|
||||
<div id="app" class="text-gray-700 dark:text-white min-h-full bg-white dark:bg-gray-900">
|
||||
<div class="min-h-full">
|
||||
<nav class="bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<nav class="dark:bg-gray-800 bg-gray-50">
|
||||
<div class="max-w-5xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<div class="relative flex items-center justify-between h-16">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
<!-- Mobile menu button-->
|
||||
<button id="main-menu-button" type="button" class="inline-flex items-center justify-center p-2 rounded-md text-slate-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
|
||||
<button id="main-menu-button" type="button" class="inline-flex items-center justify-center p-2 rounded-md text-slate-600 dark:text-slate-400 hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="main-menu-open" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
@@ -32,66 +61,171 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-shrink-0 items-center hidden sm:flex">
|
||||
<a href="{{ $.c.ExternalUrl }}/">
|
||||
<img src="{{ asset "opengist.svg" }}" class="object-cover h-12 w-12">
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<a href="/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex text-primary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="flex-shrink-0 items-center flex sm:hidden">
|
||||
<a href="{{ $.c.ExternalUrl }}/">
|
||||
<img src="{{ asset "opengist.svg" }}" class="object-cover h-12 w-12">
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden sm:block sm:ml-6">
|
||||
<div class="flex space-x-4">
|
||||
<a href="/all" class="text-slate-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium" aria-current="page">All</a>
|
||||
<a href="/" class="text-slate-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">New</a>
|
||||
{{ if .userLogged }}
|
||||
<a href="/{{ .userLogged.Username }}" class="text-slate-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">My gists</a>
|
||||
{{ end }}
|
||||
<a href="{{ $.c.ExternalUrl }}/all" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">{{ .locale.Tr "header.menu.all" }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ if not .userLogged }}login{{ end }}" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">{{ .locale.Tr "header.menu.new" }}</a>
|
||||
<div class="flex flex-1 items-center justify-center px-2 lg:ml-6 lg:justify-end">
|
||||
<div class="w-full max-w-lg lg:max-w-xs">
|
||||
<label for="search" class="sr-only">{{ .locale.Tr "header.menu.search" }}</label>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<form action="/search" method="GET">
|
||||
<input id="search" name="q" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="Search" type="search" value="{{ .searchQuery }}">
|
||||
<input type="submit" hidden="hidden">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
{{ if .userLogged }}
|
||||
{{ if .userLogged.IsAdmin }}
|
||||
<a href="/admin" class="hidden sm:block text-slate-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium" aria-current="page">Admin</a>
|
||||
{{ end }}
|
||||
<a href="/settings" class="hidden sm:block text-slate-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium" aria-current="page">Settings</a>
|
||||
|
||||
<a href="/logout" id="logged-button" class="inline-flex text-slate-300 hover:bg-rose-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
||||
<p class="text-slate-300 mr-1 username">{{ .userLogged.Username }}</p>
|
||||
<p class="text-slate-300 mr-1 logout hidden">Logout</p>
|
||||
<span class="sr-only">User</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<div id="user-btn" class="hidden sm:flex items-center ml-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md px-3 py-2">
|
||||
<div class="inline-flex">
|
||||
<p class="hidden sm:block text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white rounded-md text-sm font-medium mr-2">{{ .userLogged.Username }}</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="h-5 w-5 inline-block">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden relative sm:inline-block text-left">
|
||||
<div id="user-menu" class="hidden w-32 font-medium absolute right-0 z-10 mt-12 origin-top-right divide-y dark:divide-gray-600 divide-gray-100 rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none">
|
||||
<div class="py-1" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
</svg>
|
||||
{{ .locale.Tr "header.menu.my-gists" }}
|
||||
</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}/liked" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
|
||||
</svg>
|
||||
{{ .locale.Tr "header.menu.liked" }}
|
||||
</a>
|
||||
</div>
|
||||
{{ if .userLogged.IsAdmin }}
|
||||
<div class="py-1" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
|
||||
</svg>
|
||||
{{ .locale.Tr "header.menu.admin" }}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="py-1" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/settings" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
{{ .locale.Tr "header.menu.settings" }}
|
||||
</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/logout" class="dark:text-rose-400 text-rose-500 group flex items-center px-3 py-1.5 text-sm w-full hover:text-rose-600 dark:hover:text-rose-500" role="menuitem" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 dark:text-rose-400 text-rose-500 group-hover:text-rose-600 dark:group-hover:text-rose-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||
</svg>
|
||||
{{ .locale.Tr "header.menu.logout" }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{ else }}
|
||||
{{ if not .signupDisabled }}
|
||||
<a href="/register" class="inline-flex text-slate-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
||||
<p class="text-slate-300 mr-1">Register</p>
|
||||
{{ if not .DisableSignup }}
|
||||
<a href="{{ $.c.ExternalUrl }}/register" class="inline-flex text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
||||
<p class="text-slate-700 dark:text-slate-300 mr-1">{{ .locale.Tr "header.menu.register" }}</p>
|
||||
</a>
|
||||
{{ end }}
|
||||
<a href="/login" class="inline-flex text-slate-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
||||
<p class="text-slate-300 mr-1">Login</p>
|
||||
<a href="{{ $.c.ExternalUrl }}/login" class="inline-flex text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
||||
<p class="text-slate-700 dark:text-slate-300 mr-1">{{ .locale.Tr "header.menu.login" }}</p>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
<div class="hidden sm:block ml-2 border-l-1 border-gray-200 dark:border-gray-600 rounded-md"><br /></div>
|
||||
<div id="theme-btn" class="hidden sm:flex items-center ml-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md px-3 py-2">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-primary-500 dark:hidden">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-primary-500 hidden dark:block">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="hidden relative sm:inline-block text-left">
|
||||
<div id="theme-menu" class="hidden font-medium absolute right-0 z-10 mt-12 origin-top-right divide-y dark:divide-gray-600 divide-gray-100 rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none">
|
||||
<div class="py-1" role="none">
|
||||
<!-- Active: "bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900", Not Active: "text-gray-300 dark:text-gray-700" -->
|
||||
<button id="light-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||
</svg>
|
||||
{{ .locale.Tr "header.menu.light" }}
|
||||
</button>
|
||||
<button id="dark-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||
</svg>
|
||||
{{ .locale.Tr "header.menu.dark" }}
|
||||
</button>
|
||||
<button id="system-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
|
||||
</svg>
|
||||
{{ .locale.Tr "header.menu.system" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div class="sm:hidden hidden" id="mobile-menu">
|
||||
<div class="mx-2">
|
||||
<label for="searchmobile" class="sr-only">{{ .locale.Tr "header.menu.search" }}</label>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<form action="/search" method="GET">
|
||||
<input id="searchmobile" name="q" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="Search" type="search" value="{{.searchQuery}}">
|
||||
<input type="submit" hidden="hidden">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<a href="/all" class="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium" aria-current="page">All</a>
|
||||
<a href="/" class="text-slate-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">New</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/all" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.all" }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ if not .userLogged }}login{{ end }}" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.new" }}</a>
|
||||
{{ if .userLogged }}
|
||||
<a href="/{{ .userLogged.Username }}" class="text-slate-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">My gists</a>
|
||||
<a href="/settings" class="text-slate-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">Settings</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.my-gists" }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/settings" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.settings" }}</a>
|
||||
|
||||
{{ if .userLogged.IsAdmin }}
|
||||
<a href="/admin" class="text-slate-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">Admin</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.admin" }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -103,38 +237,41 @@
|
||||
|
||||
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-slate-300">
|
||||
|
||||
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-slate-700 dark:text-slate-300">
|
||||
<div>
|
||||
{{range .flashErrors}}
|
||||
<div class="mt-4 rounded-md bg-gray-800 border-l-4 border-rose-400 p-4">
|
||||
<div class="mt-4 rounded-md bg-gray-50 dark:bg-gray-800 border-l-4 border-rose-400 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-rose-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<svg class="h-5 w-5 text-rose-600 dark:text-rose-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-rose-400">{{.}}</p>
|
||||
<p class="text-sm text-rose-600 dark:text-rose-400">{{.}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{range .flashSuccess}}
|
||||
<div class="mt-4 rounded-md bg-gray-800 border-l-4 border-primary-400 p-4">
|
||||
<div class="mt-4 rounded-md bg-gray-50 dark:bg-gray-800 border-l-4 border-primary-500 dark:border-primary-400 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary-500 dark:text-primary-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-primary-400">{{.}}</p>
|
||||
<p class="text-sm text-primary-500 dark:text-primary-400">{{.}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if false }}
|
||||
{{/* prevent IDE errors */}}
|
||||
</div></div></body></html>
|
||||
{{ end }}
|
||||
|
||||
5
templates/base/gist_footer.html
vendored
5
templates/base/gist_footer.html
vendored
@@ -1,3 +1,8 @@
|
||||
{{ if false }}
|
||||
{{/* prevent IDE errors */}}
|
||||
<div><main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "gist_footer" }}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
101
templates/base/gist_header.html
vendored
101
templates/base/gist_header.html
vendored
@@ -4,84 +4,84 @@
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold leading-tight break-all">
|
||||
<a href="/{{ .gist.User.Username }}">{{ .gist.User.Username }}</a> <span class="text-slate-300">/</span> <a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}">{{ .gist.Title }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}">{{ .gist.User.Username }}</a> <span class="text-slate-700 dark:text-slate-300">/</span> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}">{{ .gist.Title }}</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="lg:flex-row flex py-2 lg:py-0 lg:ml-auto">
|
||||
{{ if .userLogged }}
|
||||
<form id="like" class="flex items-center" method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/like?redirecturl={{ .currentUrl }}">
|
||||
{{ .csrfHtml }}
|
||||
<button type="submit" class="focus-within:z-10 text-slate-300 relative inline-flex items-center space-x-2 rounded-l-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
|
||||
<button type="submit" class="focus-within:z-10 text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-l-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 not .hasLiked }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
||||
</svg>
|
||||
Like
|
||||
{{ .locale.Tr "gist.header.like" }}
|
||||
{{ else }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 mr-2">
|
||||
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
|
||||
</svg>
|
||||
Unlike
|
||||
{{ .locale.Tr "gist.header.unlike" }}
|
||||
{{ end }}
|
||||
</button>
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/likes" class="text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-700 bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/likes" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px 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">
|
||||
{{ .gist.NbLikes }}
|
||||
</a>
|
||||
</form>
|
||||
{{ if ne .userLogged.ID .gist.User.ID }}
|
||||
<form id="fork" class="ml-2 flex items-center " method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/fork">
|
||||
{{ .csrfHtml }}
|
||||
<button type="submit" class="ml-auto focus-within:z-10 text-slate-300 relative inline-flex items-center space-x-2 rounded-l-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
|
||||
<button type="submit" class="ml-auto focus-within:z-10 text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-l-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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
|
||||
</svg>
|
||||
Fork
|
||||
{{ .locale.Tr "gist.header.fork" }}
|
||||
</button>
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/forks" class="text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-700 bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/forks" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px 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">
|
||||
{{ .gist.NbForks }}
|
||||
</a>
|
||||
</form>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<div class="lg:flex-row flex lg:py-0 lg:ml-auto flex items-center">
|
||||
<a href="/login" type="submit" class="ml-auto focus-within:z-10 text-slate-300 relative inline-flex items-center space-x-2 rounded-l-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
|
||||
<a href="{{ $.c.ExternalUrl }}/login" type="submit" class="ml-auto focus-within:z-10 text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-l-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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
||||
</svg>
|
||||
Like
|
||||
{{ .locale.Tr "gist.header.like" }}
|
||||
</a>
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/likes" class="text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-700 bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/likes" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px 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">
|
||||
{{ .gist.NbLikes }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-2 flex items-center">
|
||||
<a href="/login" type="submit" class="ml-auto focus-within:z-10 text-slate-300 relative inline-flex items-center space-x-2 rounded-l-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
|
||||
<a href="{{ $.c.ExternalUrl }}/login" type="submit" class="ml-auto focus-within:z-10 text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-l-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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
|
||||
</svg>
|
||||
Fork
|
||||
{{ .locale.Tr "gist.header.fork" }}
|
||||
</a>
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/forks" class="text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-700 bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/forks" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px 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">
|
||||
{{ .gist.NbForks }}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .userLogged }}{{ if eq .gist.User.Username .userLogged.Username }}
|
||||
<div class="ml-2 flex items-center">
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/edit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/edit" class="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">
|
||||
<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.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
Edit
|
||||
{{ .locale.Tr "gist.header.edit" }}
|
||||
</a>
|
||||
</div>
|
||||
<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">
|
||||
{{ .csrfHtml }}
|
||||
<button type="submit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-rose-400 hover:bg-rose-700 hover:border-rose-600 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
|
||||
<button type="submit" class="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-rose-600 dark:text-rose-400 hover:bg-rose-500 hover:text-white dark:hover:bg-rose-600 hover:border-rose-600 dark:hover:border-rose-700 dark:hover:text-white focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Delete
|
||||
{{ .locale.Tr "gist.header.delete" }}
|
||||
</button>
|
||||
</form>
|
||||
{{ end }}{{ end }}
|
||||
@@ -89,73 +89,72 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ if .gist.Forked }}
|
||||
<p class="mt-1 max-w-2xl text-sm text-slate-500">Forked from <a href="/{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Uuid }}">{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}</a></p>
|
||||
<p class="mt-1 max-w-2xl text-sm text-slate-500">{{ .locale.Tr "gist.header.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-700 text-slate-300"> Unlisted </span>{{ end }}
|
||||
|
||||
<p class="mt-1 max-w-2xl text-sm text-slate-500">{{ .locale.Tr "gist.header.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"> {{ visibilityStr .gist.Private false }} </span>{{ end }}
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-slate-300">{{ .gist.Description }}</p>
|
||||
<p class="mt-3 max-w-2xl text-slate-700 dark:text-slate-300">{{ .gist.Description }}</p>
|
||||
</header>
|
||||
<main class="mt-4">
|
||||
|
||||
<div class="my-4">
|
||||
<div class="sm:hidden">
|
||||
<label for="gist-tabs" class="sr-only">Select a tab</label>
|
||||
<select id="gist-tabs" name="tabs" class="block bg-gray-800 w-full pl-3 pr-10 py-2 text-base border-gray-700 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-md">
|
||||
<option {{ if eq .page "code"}}selected{{end}} data-url="/{{ .gist.User.Username }}/{{ .gist.Uuid }}">Code</option>
|
||||
<option {{ if eq .page "revisions"}}selected{{end}} data-url="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/revisions">Revisions ({{ if .nbCommits }}{{ .nbCommits }}{{else}}0{{ end }})</option>
|
||||
<label for="gist-tabs" class="sr-only">{{ .locale.Tr "gist.header.select-tab" }}</label>
|
||||
<select id="gist-tabs" name="tabs" class="block bg-gray-50 dark:bg-gray-800 w-full pl-3 pr-10 py-2 text-base border-gray-200 dark:border-gray-700 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-md">
|
||||
<option {{ if eq .page "code"}}selected{{end}} data-url="/{{ .gist.User.Username }}/{{ .gist.Uuid }}">{{ .locale.Tr "gist.header.code" }}</option>
|
||||
<option {{ if eq .page "revisions"}}selected{{end}} data-url="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/revisions">{{ .locale.Tr "gist.header.revisions" }} ({{ if .nbCommits }}{{ .nbCommits }}{{else}}0{{ end }})</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<div class="border-b flex border-gray-700">
|
||||
<div class="border-b flex border-gray-200 dark:border-gray-700">
|
||||
<nav class="-mb-px flex-auto space-x-4" aria-label="Tabs">
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}" class="inline-flex items-center text-slate-300 {{ if eq .page "code"}}border-slate-300 {{else}}border-transparent hover:border-gray-300{{end}} hover:text-slate-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm" aria-current="page">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}" class="inline-flex items-center text-slate-700 dark:text-slate-300 {{ if eq .page "code"}}border-slate-500 dark:border-slate-300 {{else}}border-transparent hover:border-gray-700 dark:hover:border-gray-200{{end}} hover:text-slate-700 dark:hover:text-slate-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm" aria-current="page">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
|
||||
</svg>
|
||||
Code
|
||||
{{ .locale.Tr "gist.header.code" }}
|
||||
</a>
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/revisions" class="inline-flex items-center text-slate-300 {{ if eq .page "revisions"}}border-slate-300 {{else}}border-transparent hover:border-gray-300{{end}} hover:text-slate-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/revisions" class="inline-flex items-center text-slate-700 dark:text-slate-300 {{ if eq .page "revisions"}}border-slate-500 dark:border-slate-300 {{else}}border-transparent hover:border-gray-700 dark:hover:border-gray-200{{end}} hover:text-slate-700 dark:hover:text-slate-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
Revisions
|
||||
<span class="inline-flex items-center ml-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-700 text-slate-300"> {{ if .nbCommits }}{{ .nbCommits }}{{else}}0{{ end }} </span>
|
||||
{{ .locale.Tr "gist.header.revisions" }}
|
||||
<span class="inline-flex items-center ml-2 px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ if .nbCommits }}{{ .nbCommits }}{{else}}0{{ end }} </span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="float-right inline-flex items-center space-x-2">
|
||||
<div>
|
||||
<div class="flex rounded-md shadow-sm">
|
||||
<div class="relative">
|
||||
<button type="button" id="gist-menu-toggle" class="relative text-xs inline-flex items-center space-x-2 rounded-l-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-sm font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3 focus-within:z-10 -mr-px">
|
||||
<button type="button" id="gist-menu-toggle" class="relative text-xs inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-sm 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 focus-within:z-10 -mr-px">
|
||||
<span id="gist-menu-title" class="whitespace-nowrap"></span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="absolute left-0 z-10 mt-2 w-56 origin-top-left bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="py-1 cursor-pointer border-1 rounded-md border-gray-700 hidden" id="gist-menu-copy" role="none">
|
||||
<div class="absolute left-0 z-10 mt-2 w-56 origin-top-left bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="py-1 cursor-pointer border-1 rounded-md border-gray-200 dark:border-gray-700 hidden" id="gist-menu-copy" role="none">
|
||||
{{ if .httpCloneUrl }}
|
||||
<div class="text-slate-300 block px-4 py-2 text-sm hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-http" data-link="{{ .httpCloneUrl }}"><p>Clone via {{ .httpProtocol }}</p>
|
||||
<p class="text-xs font-normal text-gray-400">Clone with Git using HTTP basic authentication.</p>
|
||||
<div class="text-slate-700 dark:text-slate-300 block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-http" data-link="{{ .httpCloneUrl }}"><p>{{ .locale.Tr "gist.header.clone-http" .httpProtocol }}</p>
|
||||
<p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.clone-http-help" }}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .sshCloneUrl }}
|
||||
<div class="text-slate-300 block px-4 py-2 text-sm hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-ssh" data-link="{{ .sshCloneUrl }}"><p>Clone via SSH</p>
|
||||
<p class="text-xs font-normal text-gray-400">Clone with Git using an SSH key.</p>
|
||||
<div class="text-slate-700 dark:text-slate-300 block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-ssh" data-link="{{ .sshCloneUrl }}"><p>{{ .locale.Tr "gist.header.clone-ssh" }}</p>
|
||||
<p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.clone-ssh-help" }}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="text-slate-300 block px-4 py-2 text-sm hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-share" data-link="{{ .httpCopyUrl }}"><p>Share</p>
|
||||
<p class="text-xs font-normal text-gray-400">Copy shareable link for this gist.</p>
|
||||
<div class="text-slate-700 dark:text-slate-300 block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-share" data-link="{{ .httpCopyUrl }}"><p>{{ .locale.Tr "gist.header.share" }}</p>
|
||||
<p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.share-help" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex flex-grow items-stretch focus-within:z-10">
|
||||
<input id="gist-menu-input" value="" class="block code bg-gray-900 w-full rounded-none border border-gray-600 focus:border-primary-500 focus:ring-primary-500 focus:outline-none focus:ring-1 text-xs px-2">
|
||||
<input id="gist-menu-input" value="" class="block code bg-white dark:bg-gray-900 w-full rounded-none border border-gray-200 dark:border-gray-600 focus:border-primary-500 focus:ring-primary-500 focus:outline-none focus:ring-1 text-xs px-2">
|
||||
</div>
|
||||
<button id="gist-menu-button-copy" type="button" class="relative text-xs -ml-px inline-flex items-center space-x-2 rounded-r-md border border-gray-600 bg-gray-800 px-2 py-1 text-sm font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
|
||||
<button id="gist-menu-button-copy" type="button" class="relative text-xs -ml-px inline-flex items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1 text-sm 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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
@@ -164,14 +163,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/archive/{{ .revision }}" class="whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-800 px-2.5 py-2 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
Download ZIP</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/archive/{{ .revision }}" class="whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "gist.header.download-zip" }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .revision }} {{ if ne .revision "HEAD" }}
|
||||
<p class="italic text-xs mt-3">Revision <span class="revision-text">{{ .revision }}</span></p>
|
||||
<p class="italic text-xs mt-3">{{ .locale.Tr "gist.header.revision" }} <span class="revision-text">{{ .revision }}</span></p>
|
||||
{{ end }} {{ end }}
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if false }}
|
||||
{{/* prevent IDE errors */}}
|
||||
</main></div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
10
templates/base/pagination.html
vendored
10
templates/base/pagination.html
vendored
@@ -1,31 +1,31 @@
|
||||
{{ define "pagination" }}
|
||||
<div class="flex justify-center space-x-2">
|
||||
{{ if .prevPage }}
|
||||
<a href="/{{ .urlPage }}?page={{ .prevPage }}{{ .urlParams }}" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-900 bg-gray-900 px-2 py-1.5 font-medium text-slate-300 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?page={{ .prevPage }}{{ .urlParams }}" class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-slate-700 dark:text-slate-300 hover:border-gray-200 dark:hover:border-gray-400 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-1 w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
|
||||
{{ .prevLabel }}</a>
|
||||
{{ else }}
|
||||
<span class="relative inline-flex items-center space-x-2 rounded-md border border-gray-900 bg-gray-900 px-2 py-1.5 font-medium text-gray-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">
|
||||
<span class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-gray-200 dark:text-gray-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-1 w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
{{ .prevLabel }}</span>
|
||||
{{ end }}
|
||||
{{ if .nextPage }}
|
||||
<a href="/{{ .urlPage }}?page={{ .nextPage }}{{ .urlParams }}" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-900 bg-gray-900 px-2 py-1.5 font-medium text-slate-300 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">{{ .nextLabel }}
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?page={{ .nextPage }}{{ .urlParams }}" class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-slate-700 dark:text-slate-300 hover:border-gray-200 dark:hover:border-gray-400 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">{{ .nextLabel }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="ml-1 w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</a>
|
||||
{{ else }}
|
||||
<span class="relative inline-flex items-center space-x-2 rounded-md border border-gray-900 bg-gray-900 px-2 py-1.5 font-medium text-gray-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">{{ .nextLabel }}
|
||||
<span class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-gray-200 dark:text-gray-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">{{ .nextLabel }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="ml-1 w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
6
templates/fs_embed.go
vendored
Normal file
6
templates/fs_embed.go
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package templates
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed */*.html
|
||||
var Files embed.FS
|
||||
117
templates/pages/admin_config.html
vendored
Normal file
117
templates/pages/admin_config.html
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
{{ template "header" .}}
|
||||
{{ template "admin_header" .}}
|
||||
|
||||
<div class="grid gap-4 grid-cols-1 md:grid-cols-2">
|
||||
<div class="p-6 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-700">
|
||||
<p class="italic text-xs text-gray-400 dark:text-gray-400 mb-4">{{ .locale.Tr "admin.config-link" (join "<a target=\"_blank\" href=\"https://github.com/thomiceli/opengist/blob/master/docs/configuration/index.md#configuration\">" (toStr (.locale.Tr "admin.config-link-overriden")) "</a>") }}</p>
|
||||
<dl class="dl-config">
|
||||
<div class="relative col-span-3">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<span class="bg-gray-50 dark:bg-gray-800 px-2 text-sm text-slate-700 dark:text-slate-300 font-bold">General</span>
|
||||
</div>
|
||||
</div>
|
||||
<dt>Log level</dt><dd>{{ .c.LogLevel }}</dd>
|
||||
<dt>External URL</dt><dd>{{ .c.ExternalUrl }}</dd>
|
||||
<dt>Opengist home</dt><dd>{{ .c.OpengistHome }}</dd>
|
||||
<dt>DB filename</dt><dd>{{ .c.DBFilename }}</dd>
|
||||
<dt>SQLite Journal Mode</dt><dd>{{ .c.SqliteJournalMode }}</dd>
|
||||
<div class="relative col-span-3 mt-4">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<span class="bg-gray-50 dark:bg-gray-800 px-2 text-sm text-slate-700 dark:text-slate-300 font-bold">HTTP</span>
|
||||
</div>
|
||||
</div>
|
||||
<dt>HTTP host</dt><dd>{{ .c.HttpHost }}</dd>
|
||||
<dt>HTTP port</dt><dd>{{ .c.HttpPort }}</dd>
|
||||
<dt>HTTP Git enabled</dt><dd>{{ .c.HttpGit }}</dd>
|
||||
<div class="relative col-span-3 mt-4">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<span class="bg-gray-50 dark:bg-gray-800 px-2 text-sm text-slate-700 dark:text-slate-300 font-bold">SSH</span>
|
||||
</div>
|
||||
</div>
|
||||
<dt>SSH Git enabled</dt><dd>{{ .c.SshGit }}</dd>
|
||||
<dt>SSH host</dt><dd>{{ .c.SshHost }}</dd>
|
||||
<dt>SSH port</dt><dd>{{ .c.SshPort }}</dd>
|
||||
<dt>SSH external domain</dt><dd>{{ .c.SshExternalDomain }}</dd>
|
||||
<dt>SSH Keygen</dt><dd>{{ .c.SshKeygen }}</dd>
|
||||
<div class="relative col-span-3 mt-4">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<span class="bg-gray-50 dark:bg-gray-800 px-2 text-sm text-slate-700 dark:text-slate-300 font-bold">OAuth</span>
|
||||
</div>
|
||||
</div>
|
||||
<dt>Github Client key</dt><dd>{{ .c.GithubClientKey }}</dd>
|
||||
<dt>Github Secret</dt><dd>{{ .c.GithubSecret }}</dd>
|
||||
<dt>Gitea client Key</dt><dd>{{ .c.GiteaClientKey }}</dd>
|
||||
<dt>Gitea Secret</dt><dd>{{ .c.GiteaSecret }}</dd>
|
||||
<dt>Gitea URL</dt><dd>{{ .c.GiteaUrl }}</dd>
|
||||
<dt>OIDC client Key</dt><dd>{{ .c.OIDCClientKey }}</dd>
|
||||
<dt>OIDC Secret</dt><dd>{{ .c.OIDCSecret }}</dd>
|
||||
<dt>OIDC Discovery URL</dt><dd>{{ .c.OIDCDiscoveryUrl }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div>
|
||||
<ul role="list" class="divide-y divide-slate-300 dark:divide-gray-200 px-4 py-2 sm:px-6 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-700">
|
||||
<li class="list-none gap-x-4 py-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<span class="text-sm font-medium leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.disable-signup" }}</span>
|
||||
<span class="text-sm text-gray-400 dark:text-gray-400">{{ .locale.Tr "admin.disable-signup_help" }}</span>
|
||||
</span>
|
||||
<button type="button" id="disable-signup" data-bool="{{ .DisableSignup }}" class="toggle-button {{ if .DisableSignup }}bg-primary-600{{else}}bg-gray-300 dark:bg-gray-400{{end}} relative inline-flex h-6 w-11 ml-4 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description">
|
||||
<span aria-hidden="true" class="{{ if .DisableSignup }}translate-x-5{{else}}translate-x-0{{end}} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-none gap-x-4 py-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<span class="text-sm font-medium leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.require-login" }}</span>
|
||||
<span class="text-sm text-gray-400 dark:text-gray-400">{{ .locale.Tr "admin.require-login_help" }}</span>
|
||||
</span>
|
||||
<button type="button" id="require-login" data-bool="{{ .RequireLogin }}" class="toggle-button {{ if .RequireLogin }}bg-primary-600{{else}}bg-gray-300 dark:bg-gray-400{{end}} relative inline-flex h-6 w-11 ml-4 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description">
|
||||
<span aria-hidden="true" class="{{ if .RequireLogin }}translate-x-5{{else}}translate-x-0{{end}} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-none gap-x-4 py-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<span class="text-sm font-medium leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.disable-login" }}</span>
|
||||
<span class="text-sm text-gray-400 dark:text-gray-400">{{ .locale.Tr "admin.disable-login_help" }}</span>
|
||||
</span>
|
||||
<button type="button" id="disable-login-form" data-bool="{{ .DisableLoginForm }}" class="toggle-button {{ if .DisableLoginForm }}bg-primary-600{{else}}bg-gray-300 dark:bg-gray-400{{end}} relative inline-flex h-6 w-11 ml-4 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description">
|
||||
<span aria-hidden="true" class="{{ if .DisableLoginForm }}translate-x-5{{else}}translate-x-0{{end}} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-none gap-x-4 py-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
<span class="text-sm font-medium leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.disable-gravatar" }}</span>
|
||||
<span class="text-sm text-gray-400 dark:text-gray-400">{{ .locale.Tr "admin.disable-gravatar_help" }}</span>
|
||||
</span>
|
||||
<button type="button" id="disable-gravatar" data-bool="{{ .DisableGravatar }}" class="toggle-button {{ if .DisableGravatar }}bg-primary-600{{else}}bg-gray-300 dark:bg-gray-400{{end}} relative inline-flex h-6 w-11 ml-4 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description">
|
||||
<span aria-hidden="true" class="{{ if .DisableGravatar }}translate-x-5{{else}}translate-x-0{{end}} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{{ .csrfHtml }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="{{ asset "admin.ts" }}"></script>
|
||||
|
||||
{{ template "admin_footer" .}}
|
||||
{{ template "footer" .}}
|
||||
40
templates/pages/admin_gists.html
vendored
40
templates/pages/admin_gists.html
vendored
@@ -1,36 +1,36 @@
|
||||
{{ template "header" .}}
|
||||
{{ template "admin_header" .}}
|
||||
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8 bg-gray-800 rounded-md border border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-500">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-slate-300 dark:divide-gray-500">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-bold text-slate-300 sm:pl-0">ID</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-300">Title</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-300">User</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-300">Private ?</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-300"># files</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-300"># likes</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-300">Created at</th>
|
||||
<th scope="col" class="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-bold text-slate-700 dark:text-slate-300 sm:pl-0">{{ .locale.Tr "admin.id" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.gists.title" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.user" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.gists.private" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.gists.nb-files" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.gists.nb-likes" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.created_at" }}</th>
|
||||
<th scope="col" class="relative whitespace-nowrap py-3.5 pl-3 pr-4 sm:pr-0">
|
||||
<span class="sr-only">Delete</span>
|
||||
<span class="sr-only">{{ .locale.Tr "admin.delete" }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-500">
|
||||
<tbody class="divide-y divide-slate-300 dark:divide-gray-500">
|
||||
{{ range $gist := .data }}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-slate-300 sm:pl-0">{{ $gist.ID }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-300"><a href="/{{ $gist.User.Username }}/{{ $gist.Uuid }}">{{ $gist.Title }}</a></td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-300"><a href="/{{ $gist.User.Username }}">{{ $gist.User.Username }}</a></td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-300">{{ $gist.Private }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-300">{{ $gist.NbFiles }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-300">{{ $gist.NbLikes }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-300"><span class="moment-timestamp-date">{{ $gist.CreatedAt }}</span></td>
|
||||
<td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-slate-700 dark:text-slate-300 sm:pl-0">{{ $gist.ID }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Uuid }}">{{ $gist.Title }}</a></td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}">{{ $gist.User.Username }}</a></td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.Private }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.NbFiles }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.NbLikes }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><span class="moment-timestamp-date">{{ $gist.CreatedAt }}</span></td>
|
||||
<td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||
<form action="/admin/gists/{{ $gist.ID }}/delete" method="POST" onsubmit="return confirm('Do you want to delete this gist ?')">
|
||||
<form action="/admin-panel/gists/{{ $gist.ID }}/delete" method="POST" onsubmit="return confirm('{{ $.locale.Tr "admin.gists.delete_confirm" }}')">
|
||||
{{ $.csrfHtml }}
|
||||
<button type="submit" class="text-rose-500 hover:text-rose-600">Delete</button>
|
||||
<button type="submit" class="text-rose-500 hover:text-rose-600">{{ $.locale.Tr "admin.delete" }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
54
templates/pages/admin_index.html
vendored
54
templates/pages/admin_index.html
vendored
@@ -3,23 +3,23 @@
|
||||
|
||||
<div class="sm:flex sm:space-x-4 space-y-4 sm:space-y-0">
|
||||
<div class="sm:overflow-hidden ">
|
||||
<div class="space-y-2 bg-gray-800 py-6 px-6 rounded-md border border-gray-700">
|
||||
<div class="space-y-2 bg-gray-50 dark:bg-gray-800 py-6 px-6 rounded-md border border-gray-200 dark:border-gray-700">
|
||||
<div>
|
||||
<span class="text-base font-bold leading-6 text-slate-300">Versions</span>
|
||||
<span class="text-base font-bold leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.versions" }}</span>
|
||||
</div>
|
||||
<table class="table-fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-300 ">Opengist</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-300">{{ .opengistVersion }}</td>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-700 dark:text-slate-300 ">Opengist</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .opengistVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-300 ">Go</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-300">{{ .goVersion }}</td>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-700 dark:text-slate-300 ">Go</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .goVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-300 ">Git</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-300">{{ .gitVersion }} </td>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-700 dark:text-slate-300 ">Git</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .gitVersion }} </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -27,23 +27,23 @@
|
||||
</div>
|
||||
|
||||
<div class="sm:overflow-hidden ">
|
||||
<div class="space-y-2 bg-gray-800 py-6 px-6 rounded-md border border-gray-700">
|
||||
<div class="space-y-2 bg-gray-50 dark:bg-gray-800 py-6 px-6 rounded-md border border-gray-200 dark:border-gray-700">
|
||||
<div>
|
||||
<span class="text-base font-bold leading-6 text-slate-300">Stats</span>
|
||||
<span class="text-base font-bold leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.stats" }}</span>
|
||||
</div>
|
||||
<table class="table-fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-300 ">Users</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-300">{{ .countUsers }}</td>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-700 dark:text-slate-300 ">{{ .locale.Tr "admin.users" }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .countUsers }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-300 ">Gists</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-300">{{ .countGists }}</td>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-700 dark:text-slate-300 ">{{ .locale.Tr "admin.gists" }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .countGists }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-300 ">SSH keys</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-300">{{ .countKeys }}</td>
|
||||
<td class="whitespace-nowrap py-2 pr-3 text-sm text-slate-700 dark:text-slate-300 ">{{ .locale.Tr "admin.ssh_keys" }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .countKeys }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -51,21 +51,27 @@
|
||||
</div>
|
||||
|
||||
<div class="sm:overflow-hidden ">
|
||||
<div class="space-y-2 bg-gray-800 py-6 px-6 rounded-md border border-gray-700">
|
||||
<div class="space-y-2 bg-gray-50 dark:bg-gray-800 py-6 px-6 rounded-md border border-gray-200 dark:border-gray-700">
|
||||
<div>
|
||||
<span class="text-base font-bold leading-6 text-slate-300">Actions</span>
|
||||
<span class="text-base font-bold leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.actions" }}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<form action="/admin/sync-fs" method="POST">
|
||||
<form action="/admin-panel/sync-fs" method="POST">
|
||||
{{ .csrfHtml }}
|
||||
<button type="submit" {{ if .syncReposFromFS }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-600 bg-gray-800 px-2.5 py-2 text-xs font-medium text-white shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
Synchorize gists from filesystem
|
||||
<button type="submit" {{ if .syncReposFromFS }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "admin.actions.sync-fs" }}
|
||||
</button>
|
||||
</form>
|
||||
<form action="/admin/sync-db" method="POST">
|
||||
<form action="/admin-panel/sync-db" method="POST">
|
||||
{{ .csrfHtml }}
|
||||
<button type="submit" {{ if .syncReposFromDB }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-300{{ if .syncReposFromDB }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-600 bg-gray-800 px-2.5 py-2 text-xs font-medium text-white shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
Synchorize gists from database
|
||||
<button type="submit" {{ if .syncReposFromDB }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromDB }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "admin.actions.sync-db" }}
|
||||
</button>
|
||||
</form>
|
||||
<form action="/admin-panel/gc-repos" method="POST">
|
||||
{{ .csrfHtml }}
|
||||
<button type="submit" {{ if .gitGcRepos }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .gitGcRepos }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "admin.actions.git-gc" }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
24
templates/pages/admin_users.html
vendored
24
templates/pages/admin_users.html
vendored
@@ -1,28 +1,28 @@
|
||||
{{ template "header" .}}
|
||||
{{ template "admin_header" .}}
|
||||
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8 bg-gray-800 rounded-md border border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-500">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-slate-300 dark:divide-gray-500">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-bold text-slate-300 sm:pl-0">ID</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-300">Username</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-300">Created</th>
|
||||
<th scope="col" class="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-bold text-slate-700 dark:text-slate-300 sm:pl-0">{{ .locale.Tr "admin.id" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.user" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.created_at" }}</th>
|
||||
<th scope="col" class="relative whitespace-nowrap py-3.5 pl-3 pr-4 sm:pr-0">
|
||||
<span class="sr-only">Delete</span>
|
||||
<span class="sr-only">{{ .locale.Tr "admin.delete" }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-500">
|
||||
<tbody class="divide-y divide-slate-300 dark:divide-gray-500">
|
||||
{{ range $user := .data }}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-slate-300 sm:pl-0">{{ $user.ID }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-300"><a href="/{{ $user.Username }}">{{ $user.Username }}</a></td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-300"><span class="moment-timestamp-date">{{ $user.CreatedAt }}</span></td>
|
||||
<td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-slate-700 dark:text-slate-300 sm:pl-0">{{ $user.ID }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $user.Username }}">{{ $user.Username }}</a></td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><span class="moment-timestamp-date">{{ $user.CreatedAt }}</span></td>
|
||||
<td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||
<form action="/admin/users/{{ $user.ID }}/delete" method="POST" onsubmit="return confirm('Do you want to delete this user ?')">
|
||||
<form action="/admin-panel/users/{{ $user.ID }}/delete" method="POST" onsubmit="return confirm('{{ $.locale.Tr "admin.users.delete_confirm" }}')">
|
||||
{{ $.csrfHtml }}
|
||||
<button type="submit" class="text-rose-500 hover:text-rose-600">Delete</button>
|
||||
<button type="submit" class="text-rose-500 hover:text-rose-600">{{ $.locale.Tr "admin.delete" }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
208
templates/pages/all.html
vendored
208
templates/pages/all.html
vendored
@@ -1,93 +1,155 @@
|
||||
{{ template "header" .}}
|
||||
<div class="py-10">
|
||||
<header class="pb-4 flex ">
|
||||
<div class="flex-auto">
|
||||
{{if .fromUser}}
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl .fromUser.MD5Hash }}" alt="">
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold leading-tight">{{.fromUser.Username}}</h1>
|
||||
<p class="text-sm text-slate-500">Joined <span class="moment-timestamp">{{.fromUser.CreatedAt}}</span></p>
|
||||
<header class="pb-4 ">
|
||||
<div class="flex">
|
||||
<div class="flex-auto">
|
||||
{{if .fromUser}}
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-12 w-12 rounded-md mr-2 border border-gray-200 dark:border-gray-700" src="{{ avatarUrl .fromUser .DisableGravatar }}" alt="{{ .fromuser.Username }}'s Avatar">
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold leading-tight">{{.fromUser.Username}}</h1>
|
||||
<p class="text-sm text-slate-500">{{ .locale.Tr "gist.list.joined" }} <span class="moment-timestamp">{{.fromUser.CreatedAt}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
{{ if eq .mode "all" }}
|
||||
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "gist.list.all" }}</h1>
|
||||
{{ else if eq .mode "search" }}
|
||||
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "gist.list.search-results" }}</h1>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<h1 class="text-2xl font-bold leading-tight">All gists</h1>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<div class="relative inline-block text-left">
|
||||
<div>
|
||||
<button type="button" class="whitespace-nowrap inline-flex text-slate-300 rounded border border-gray-600 bg-gray-800 px-2.5 py-2 text-xs font-medium text-white shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 leading-3" id="sort-gists-button">
|
||||
<span class="text-gray-300">Sort : <span class="text-slate-300">{{.order}} {{.sort}}</span></span>
|
||||
<svg class="-mr-1 ml-2 h-3 w-3" viewBox="0 0 20 20" fill="currentColor" 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>
|
||||
<div id="sort-gists-dropdown" class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right divide-y divide-gray-700 rounded-md rounded border border-gray-600 bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="" role="none">
|
||||
<a href="/{{if .fromUser}}{{.fromUser.Username}}{{else}}all{{end}}?sort=created&order=desc" class="text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-700 hover:text-white hover:bg-primary-500 hover:rounded-t-md" role="menuitem">
|
||||
Recently created
|
||||
</a>
|
||||
<div class="align-middle inline-flex items-center">
|
||||
<div class="relative text-left">
|
||||
<div>
|
||||
<button type="button" class="whitespace-nowrap inline-flex text-slate-700 dark:text-slate-300 rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 leading-3" id="sort-gists-button">
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ .locale.Tr "gist.list.sort" }} : <span class="text-slate-700 dark:text-slate-300">{{.order}} {{.sort}}</span></span>
|
||||
<svg class="-mr-1 ml-2 h-3 w-3" viewBox="0 0 20 20" fill="currentColor" 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>
|
||||
<div class="" role="none">
|
||||
<a href="/{{if .fromUser}}{{.fromUser.Username}}{{else}}all{{end}}?sort=created&order=asc" class="text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-700 hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
Least recently created
|
||||
</a>
|
||||
</div><div class="" role="none">
|
||||
<a href="/{{if .fromUser}}{{.fromUser.Username}}{{else}}all{{end}}?sort=updated&order=desc" class="text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-700 hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
Recently updated
|
||||
</a>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<a href="/{{if .fromUser}}{{.fromUser.Username}}{{else}}all{{end}}?sort=updated&order=asc" class="text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-700 hover:text-white hover:bg-primary-500 hover:rounded-b-md" role="menuitem">
|
||||
Least recently updated
|
||||
</a>
|
||||
<div id="sort-gists-dropdown" class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right divide-y divide-gray-200 dark:divide-gray-700 rounded-md rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=created&order=desc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-t-md" role="menuitem">
|
||||
{{ .locale.Tr "gist.list.order-by-desc" }} {{ .locale.Tr "gist.list.sort-by-created" }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=created&order=asc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
{{ .locale.Tr "gist.list.order-by-asc" }} {{ .locale.Tr "gist.list.sort-by-created" }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=updated&order=desc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
{{ .locale.Tr "gist.list.order-by-desc" }} {{ .locale.Tr "gist.list.sort-by-updated" }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=updated&order=asc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-b-md" role="menuitem">
|
||||
{{ .locale.Tr "gist.list.order-by-asc" }} {{ .locale.Tr "gist.list.sort-by-updated" }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{ if and (ne .mode "all") (ne .mode "search") }}
|
||||
<div class="mt-4">
|
||||
<div class="sm:hidden">
|
||||
<label for="tabs" class="sr-only">{{ .locale.Tr "gist.list.select-tab" }}</label>
|
||||
<select id="gist-tabs" name="tabs" class="block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 sm:text-sm dark:bg-gray-800 dark:border-gray-700">
|
||||
<option {{if eq .mode "fromUser"}}selected {{end}}data-url="/{{ .fromUser.Username }}">{{ .locale.Tr "gist.list.all" }} ({{ .countFromUser }})</option>
|
||||
{{ if ne .countLiked 0 }}<option {{if eq .mode "liked"}}selected {{end}}data-url="/{{ .fromUser.Username }}/liked">{{ .locale.Tr "gist.list.liked" }} ({{ .countLiked }})</option>{{end}}
|
||||
{{ if ne .countForked 0 }}<option {{if eq .mode "forked"}}selected {{end}}data-url="/{{ .fromUser.Username }}/forked">{{ .locale.Tr "gist.list.forked" }} ({{ .countForked }})</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 flex">
|
||||
<div class="flex-auto">
|
||||
<nav class="-mb-px flex space-x-6" aria-label="Tabs">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .fromUser.Username }}" class="{{if eq .mode "fromUser"}}border-primary-500 font-bold {{else}}border-transparent hover:border-gray-200 hover:text-gray-700{{end}} text-slate-700 dark:text-slate-300 inline-flex items-center whitespace-nowrap border-b-2 py-2 px-1 text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
|
||||
</svg>
|
||||
{{ .locale.Tr "gist.list.all" }}
|
||||
<span class="bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-slate-300 ml-2 hidden rounded-full py-0.5 px-2.5 text-xs font-medium md:inline-block">{{ .countFromUser }}</span>
|
||||
</a>
|
||||
{{ if ne .countLiked 0 }}
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .fromUser.Username }}/liked" class="{{if eq .mode "liked"}}border-primary-500 font-bold {{else}}border-transparent hover:border-gray-200 hover:text-gray-700{{end}} text-slate-700 dark:text-slate-300 inline-flex items-center whitespace-nowrap border-b-2 py-2 px-1 text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 mr-1">
|
||||
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
|
||||
</svg>
|
||||
{{ .locale.Tr "gist.list.liked" }}
|
||||
<span class="bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-slate-300 ml-2 hidden rounded-full py-0.5 px-2.5 text-xs font-medium md:inline-block">{{ .countLiked }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if ne .countForked 0 }}
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .fromUser.Username }}/forked" class="{{if eq .mode "forked"}}border-primary-500 font-bold {{else}}border-transparent hover:border-gray-200 hover:text-gray-700{{end}} text-slate-700 dark:text-slate-300 inline-flex items-center whitespace-nowrap border-b-2 py-2 px-1 text-sm" aria-current="page">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
|
||||
</svg>
|
||||
{{ .locale.Tr "gist.list.forked" }}
|
||||
<span class="bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-slate-300 ml-2 hidden rounded-full py-0.5 px-2.5 text-xs font-medium md:inline-block">{{ .countForked }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</header>
|
||||
<main>
|
||||
<div>
|
||||
{{ if ne (len .gists) 0 }}
|
||||
{{ range $gist := .gists }}
|
||||
<div class="mb-8">
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<h4 class="text-md leading-tight break-all py-1 flex-auto">
|
||||
<a href="/{{ $gist.User.Username }}">{{ $gist.User.Username }}</a> <span class="text-slate-300">/</span> <a class="font-bold" href="/{{ $gist.User.Username }}/{{ $gist.Uuid }}">{{ $gist.Title }}</a>
|
||||
</h4>
|
||||
<div class="flex space-x-4 lg:flex-row flex py-1 lg:py-0 lg:ml-auto text-slate-500">
|
||||
<div class="flex items-center float-right text-xs">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
||||
</svg>
|
||||
<span class="whitespace-nowrap">{{ $gist.NbLikes }} likes</span>
|
||||
</div>
|
||||
<div class="flex items-center float-right text-xs">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
|
||||
</svg>
|
||||
<span class="whitespace-nowrap">{{ $gist.NbForks }} forks</span>
|
||||
</div>
|
||||
<div class="flex items-center float-right text-xs">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
|
||||
</svg>
|
||||
<span class="whitespace-nowrap">{{ $gist.NbFiles }} files</span>
|
||||
</div>
|
||||
<div class="flex ">
|
||||
<div class="div">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}">
|
||||
<img class="h-10 w-10 rounded-md mr-2 border border-gray-200 dark:border-gray-700 my-1" src="{{ avatarUrl $gist.User $.DisableGravatar }}" alt="{{ $gist.User.Username }}'s Avatar">
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-auto">
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<h4 class="text-md leading-tight break-all py-1 flex-auto">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}">{{ $gist.User.Username }}</a> <span class="text-slate-700 dark:text-slate-300">/</span> <a class="font-bold" href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Uuid }}">{{ $gist.Title }}</a>
|
||||
</h4>
|
||||
<div class="flex space-x-4 lg:flex-row flex py-1 lg:py-0 lg:ml-auto text-slate-500">
|
||||
<div class="flex items-center float-right text-xs">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
||||
</svg>
|
||||
<span class="whitespace-nowrap">{{ $gist.NbLikes }} {{ $.locale.Tr "gist.list.likes" }}</span>
|
||||
</div>
|
||||
<div class="flex items-center float-right text-xs">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
|
||||
</svg>
|
||||
<span class="whitespace-nowrap">{{ $gist.NbForks }} {{ $.locale.Tr "gist.list.forks" }}</span>
|
||||
</div>
|
||||
<div class="flex items-center float-right text-xs">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
|
||||
</svg>
|
||||
<span class="whitespace-nowrap">{{ $gist.NbFiles }} {{ $.locale.Tr "gist.list.files" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<h5 class="text-sm text-slate-500 pb-1">{{ $.locale.Tr "gist.list.last-active" }} <span class="moment-timestamp">{{ $gist.UpdatedAt }}</span>
|
||||
{{ if $gist.Forked }} • {{ $.locale.Tr "gist.list.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"> {{ visibilityStr $gist.Private false }} </span>{{ end }}</h5>
|
||||
<h6 class="text-xs text-slate-700 dark:text-slate-300 py-1">{{ $gist.Description }}</h6>
|
||||
</div>
|
||||
</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="/{{ $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-700 text-slate-300"> Unlisted </span>{{ end }}</h5>
|
||||
<h5 class="text-xs text-slate-300 py-1">{{ $gist.Description }}</h5>
|
||||
<a href="/{{ $gist.User.Username }}/{{ $gist.Uuid }}" class="text-slate-300 hover:text-slate-300">
|
||||
<div class="rounded-md border border-1 border-gray-700 overflow-auto hover:border-primary-600">
|
||||
<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">
|
||||
<div class="code overflow-auto">
|
||||
{{ if isMarkdown $gist.PreviewFilename }}
|
||||
<div class="markdown markdown-body p-8">{{ $gist.Preview }}</div>
|
||||
@@ -116,10 +178,10 @@
|
||||
{{ template "pagination" . }}
|
||||
{{ else }}
|
||||
<div class="text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-600 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-300">No gists</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.list.no-gists" }}</h3>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
56
templates/pages/auth_form.html
vendored
56
templates/pages/auth_form.html
vendored
@@ -2,52 +2,82 @@
|
||||
<div class="py-10">
|
||||
<header>
|
||||
|
||||
<h1 class="text-2xl font-bold leading-tight text-slate-300">
|
||||
<h1 class="text-2xl font-bold leading-tight text-slate-700 dark:text-slate-300">
|
||||
{{ .title }}
|
||||
</h1>
|
||||
|
||||
</header>
|
||||
<main class="mt-4">
|
||||
{{ if and .signupDisabled (ne .title "Login") }}
|
||||
<p class="italic">Administrator has disabled signing up</p>
|
||||
{{ if and .DisableSignup (ne .title "Login") }}
|
||||
<p class="italic">{{ .locale.Tr "auth.signup-disabled" }}</p>
|
||||
{{ else }}
|
||||
<div class="sm:col-span-6">
|
||||
<div class="mt-8 sm:w-full sm:max-w-md">
|
||||
<div class="bg-gray-900 rounded-md border border-1 border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
|
||||
{{ if not .disableForm }}
|
||||
<form class="space-y-6" action="#" method="post">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-300"> Username </label>
|
||||
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "auth.username" }} </label>
|
||||
<div class="mt-1">
|
||||
<input id="username" name="username" type="text" required class="bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-700 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
<input id="username" name="username" type="text" required class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<label for="password" class="block text-sm font-medium text-slate-300"> Password </label>
|
||||
<label for="password" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "auth.password" }} </label>
|
||||
<div class="mt-1">
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required class="bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-700 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
{{ if eq .title "Login" }}
|
||||
<div class="flex">
|
||||
<div class="flex-auto">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">Login</button>
|
||||
<button type="submit" 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-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">{{ .locale.Tr "auth.login" }}</button>
|
||||
</div>
|
||||
{{ if not .signupDisabled }}
|
||||
<span class="float-right text-sm py-2 underline"><a href="/register">Register instead →</a></span>
|
||||
{{ if not .DisableSignup }}
|
||||
<span class="float-right text-sm py-2 underline"><a href="{{ $.c.ExternalUrl }}/register">{{ .locale.Tr "auth.register-instead" }} →</a></span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="flex">
|
||||
<div class="flex-auto">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">Register</button>
|
||||
<button type="submit" 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-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">{{ .locale.Tr "auth.signup" }}</button>
|
||||
</div>
|
||||
<span class="float-right text-sm py-2 underline"><a href="/login">Login instead →</a></span>
|
||||
<span class="float-right text-sm py-2 underline"><a href="{{ $.c.ExternalUrl }}/login">{{ .locale.Tr "auth.login-instead" }} →</a></span>
|
||||
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ .csrfHtml }}
|
||||
</form>
|
||||
{{ end }}
|
||||
{{ if or .githubOauth .giteaOauth .oidcOauth }}
|
||||
{{ if not .disableForm }}
|
||||
<div class="relative my-4">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-gray-200 dark:border-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
{{ end }}
|
||||
<div>
|
||||
{{ if .githubOauth }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "auth.github-oauth" }}
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if .giteaOauth }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "auth.gitea-oauth" }}
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if .oidcOauth }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
Continue with OpenID account
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
56
templates/pages/create.html
vendored
56
templates/pages/create.html
vendored
@@ -2,8 +2,8 @@
|
||||
<div class="py-10">
|
||||
<header>
|
||||
|
||||
<h1 class="text-2xl font-bold leading-tight text-slate-300">
|
||||
New Gist
|
||||
<h1 class="text-2xl font-bold leading-tight text-slate-700 dark:text-slate-300">
|
||||
{{ .locale.Tr "gist.new.new_gist"}}
|
||||
</h1>
|
||||
|
||||
</header>
|
||||
@@ -12,40 +12,40 @@
|
||||
<div class="grid grid-cols-12 gap-x-4">
|
||||
<div class="col-span-8 sm:col-span-4">
|
||||
<div class="mt-1">
|
||||
<input type="text" placeholder="Title" name="title" id="title" class="bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-700 rounded-md">
|
||||
<input type="text" placeholder="{{ .locale.Tr "gist.new.title" }}" name="title" id="title" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-8">
|
||||
<div class="mt-1">
|
||||
<input type="text" placeholder="Description" name="description" id="description" class="bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-700 rounded-md">
|
||||
<input type="text" placeholder="{{ .locale.Tr "gist.new.description" }}" name="description" id="description" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="editors" class="space-y-4">
|
||||
<div class="rounded-md border border-1 border-gray-700 editor">
|
||||
<div class="border-b-1 border-gray-700 bg-gray-800 my-auto flex">
|
||||
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 editor">
|
||||
<div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto flex">
|
||||
<p class="mx-2 my-2 inline-flex">
|
||||
<input type="text" name="name" placeholder="Filename with extension" style="line-height: 0.05em" class="form-filename bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-700 rounded-md">
|
||||
<input type="text" name="name" placeholder="{{ .locale.Tr "gist.new.filename-with-extension" }}" style="line-height: 0.05em" class="form-filename bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
|
||||
</p>
|
||||
<div class="hidden mx-2 my-2 sm:inline-flex ml-auto space-x-2">
|
||||
<select class="editor-indent-type whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="Indent mode">
|
||||
<option value="space">Space</option>
|
||||
<option value="tab">Tabs</option>
|
||||
<select class="editor-indent-type whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="{{ .locale.Tr "gist.new.indent-mode" }}">
|
||||
<option value="space">{{ .locale.Tr "gist.new.indent-mode-space" }}</option>
|
||||
<option value="tab">{{ .locale.Tr "gist.new.indent-mode-tab" }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<select class="editor-indent-size whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="Indent size">
|
||||
<select class="editor-indent-size whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="{{ .locale.Tr "gist.new.indent-size" }}">
|
||||
<option value="2">2</option>
|
||||
<option value="4">4</option>
|
||||
<option value="8">8</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<select class="editor-wrap-mode whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="Wrap mode">
|
||||
<option value="no">No wrap</option>
|
||||
<option value="soft">Soft wrap</option>
|
||||
<select class="editor-wrap-mode whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="{{ .locale.Tr "gist.new.wrap-mode" }}">
|
||||
<option value="no">{{ .locale.Tr "gist.new.wrap-mode-no" }}</option>
|
||||
<option value="soft">{{ .locale.Tr "gist.new.wrap-mode-soft" }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
@@ -55,9 +55,25 @@
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<button type="button" id="add-file" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm text-white bg-gray-600 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-700 text-sm font-medium rounded-md shadow-sm text-white bg-gray-600 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-700 text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">Create public gist</button>
|
||||
<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">{{ .locale.Tr "gist.new.add-file" }}</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">{{ .locale.Tr "gist.new.create-public-button" }}</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">
|
||||
<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-btntext="{{ .locale.Tr "gist.new.create-public-button" }}" data-visibility="0" role="menuitem">{{ .locale.Tr "gist.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-btntext="{{ .locale.Tr "gist.new.create-unlisted-button" }}" data-visibility="1" role="menuitem">{{ .locale.Tr "gist.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-btntext="{{ .locale.Tr "gist.new.create-private-button" }}" data-visibility="2" role="menuitem">{{ .locale.Tr "gist.private" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ .csrfHtml }}
|
||||
</form>
|
||||
|
||||
67
templates/pages/edit.html
vendored
67
templates/pages/edit.html
vendored
@@ -3,35 +3,34 @@
|
||||
<header>
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold leading-tight text-slate-300">
|
||||
Editing {{ .gist.Title }}
|
||||
<h1 class="text-2xl font-bold leading-tight text-slate-700 dark:text-slate-300">
|
||||
{{ .locale.Tr "gist.edit.editing" }} {{ .gist.Title }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="lg:flex-row flex py-2 lg:py-0 lg:ml-auto">
|
||||
<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 text-slate-300 relative inline-flex items-center space-x-2 rounded-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 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
|
||||
<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 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 }}
|
||||
{{ .locale.Tr "gist.edit.change-visibility" }} {{ 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">
|
||||
{{ .csrfHtml }}
|
||||
<button type="submit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-rose-400 hover:bg-rose-700 hover:border-rose-600 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
|
||||
<button type="submit" class="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-rose-600 dark:text-rose-400 hover:bg-rose-500 hover:text-white dark:hover:bg-rose-600 hover:border-rose-600 dark:hover:border-rose-700 dark:hover:text-white focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Delete
|
||||
{{ .locale.Tr "gist.edit.delete" }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -42,46 +41,46 @@
|
||||
<div class="grid grid-cols-12 gap-x-4">
|
||||
<div class="col-span-8 sm:col-span-4">
|
||||
<div class="mt-1">
|
||||
<input type="text" value="{{ .gist.Title }}" placeholder="Title" name="title" id="title" class="bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-700 rounded-md">
|
||||
<input type="text" value="{{ .gist.Title }}" placeholder="{{ .locale.Tr "gist.new.title" }}" name="title" id="title" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-8">
|
||||
<div class="mt-1">
|
||||
<input type="text" value="{{ .gist.Description }}" placeholder="Description" name="description" id="description" class="bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-700 rounded-md">
|
||||
<input type="text" value="{{ .gist.Description }}" placeholder="{{ .locale.Tr "gist.new.description" }}" name="description" id="description" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="editors" class="space-y-4">
|
||||
{{ range $file := .files }}
|
||||
<div class="rounded-md border border-1 border-gray-700 editor">
|
||||
<div class="border-b-1 border-gray-700 bg-gray-800 my-auto flex">
|
||||
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 editor">
|
||||
<div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto flex">
|
||||
<p class="mx-2 my-2 inline-flex">
|
||||
<input type="text" value="{{ $file.Filename }}" name="name" placeholder="Filename with extension" style="line-height: 0.05em; z-index: 99999" class="form-filename bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-700 rounded-l-md">
|
||||
<button style="line-height: 0.05em" class="delete-file -ml-px relative inline-flex items-center space-x-2 px-4 py-2 border border-gray-700 text-sm font-medium rounded-r-md text-slate-300 bg-gray-800 hover:bg-gray-900 focus:outline-none" type="button">
|
||||
<input type="text" value="{{ $file.Filename }}" name="name" placeholder="Filename with extension" style="line-height: 0.05em; z-index: 99999" class="form-filename bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-l-md">
|
||||
<button style="line-height: 0.05em" class="delete-file -ml-px relative inline-flex items-center space-x-2 px-4 py-2 border border-gray-200 dark:border-gray-700 text-sm font-medium rounded-r-md text-slate-700 dark:text-slate-300 bg-gray-50 dark:bg-gray-800 hover:bg-white dark:hover:bg-gray-900 focus:outline-none" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</p>
|
||||
<div class="hidden mx-2 my-2 sm:inline-flex ml-auto space-x-2">
|
||||
<select class="editor-indent-type whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="Indent mode">
|
||||
<option value="space">Space</option>
|
||||
<option value="tab">Tabs</option>
|
||||
<select class="editor-indent-type whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="{{ $.locale.Tr "gist.new.indent-mode" }}">
|
||||
<option value="space">{{ $.locale.Tr "gist.new.indent-mode-space" }}</option>
|
||||
<option value="tab">{{ $.locale.Tr "gist.new.indent-mode-tab" }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<select class="editor-indent-size whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="Indent size">
|
||||
<select class="editor-indent-size whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="{{ $.locale.Tr "gist.new.indent-size" }}">
|
||||
<option value="2">2</option>
|
||||
<option value="4">4</option>
|
||||
<option value="8">8</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<select class="editor-wrap-mode whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="Wrap mode">
|
||||
<option value="no">No wrap</option>
|
||||
<option value="soft">Soft wrap</option>
|
||||
<select class="editor-wrap-mode whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
|
||||
<optgroup label="{{ $.locale.Tr "gist.new.wrap-mode" }}">
|
||||
<option value="no">{{ $.locale.Tr "gist.new.wrap-mode-no" }}</option>
|
||||
<option value="soft">{{ $.locale.Tr "gist.new.wrap-mode-soft" }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
@@ -92,9 +91,9 @@
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<button type="button" id="add-file" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">Add file</button>
|
||||
<a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}" type="submit" name="private" value="1" class="ml-auto inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 text-rose-400 hover:text-rose-400">Cancel</a>
|
||||
<button type="submit" name="private" value="0" class="ml-2 inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">Save</button>
|
||||
<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">{{ .locale.Tr "gist.new.add-file" }}</button>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}" 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 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 text-rose-600 dark:text-rose-400 hover:text-rose-700">{{ .locale.Tr "gist.edit.cancel" }}</a>
|
||||
<button type="submit" 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">{{ .locale.Tr "gist.edit.save" }}</button>
|
||||
</div>
|
||||
{{ .csrfHtml }}
|
||||
</form>
|
||||
|
||||
8
templates/pages/error.html
vendored
8
templates/pages/error.html
vendored
@@ -1,14 +1,14 @@
|
||||
{{ template "header" .}}
|
||||
|
||||
<div class="mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-12 w-12 text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-12 w-12 text-slate-600 dark:text-slate-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
|
||||
<h1 class="mt-2 text-3xl font-medium text-slate-300">Error {{ .error.Code }}</h1>
|
||||
<h3 class="mt-2 text-md font-medium text-slate-300">{{ httpStatusText .error.Code }}</h3>
|
||||
<h1 class="mt-2 text-3xl font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "error" }} {{ .error.Code }}</h1>
|
||||
<h3 class="mt-2 text-md font-medium text-slate-700 dark:text-slate-300">{{ httpStatusText .error.Code }}</h3>
|
||||
{{ if lt .error.Code 500 }}
|
||||
<p class="mt-2 text-sm font-medium text-slate-300">{{ .error.Message }}</p>
|
||||
<p class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .error.Message }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ template "footer" .}}
|
||||
|
||||
20
templates/pages/forks.html
vendored
20
templates/pages/forks.html
vendored
@@ -2,24 +2,24 @@
|
||||
{{ template "gist_header" .}}
|
||||
{{ if ne (len .forks) 0 }}
|
||||
<div class="mx-auto max-w-xl">
|
||||
<h3 class="text-xl font-bold leading-tight break-all py-2">Forks</h3>
|
||||
<h3 class="text-xl font-bold leading-tight break-all py-2">{{ .locale.Tr "gist.forks" }}</h3>
|
||||
|
||||
<ul role="list" class="divide-y divide-gray-700">
|
||||
<ul role="list" class="divide-y divide-gray-300 dark:divide-gray-700">
|
||||
{{ range $gist := .forks }}
|
||||
<li class="flex py-4">
|
||||
<a href="/{{ $gist.User.Username }}">
|
||||
<img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl $gist.User.MD5Hash }}" alt="">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}">
|
||||
<img class="h-12 w-12 rounded-md mr-2 border border-gray-200 dark:border-gray-700" src="{{ avatarUrl $gist.User $.DisableGravatar }}" alt="{{ $gist.User.Username }}'s Avatar">
|
||||
</a>
|
||||
<div>
|
||||
<a href="/{{ $gist.User.Username }}" class="text-sm font-medium text-slate-300">{{ $gist.User.Username }}</a>
|
||||
<p class="text-sm text-slate-500">Forked <span class="moment-timestamp">{{ $gist.CreatedAt }}</span></p>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}" class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ $gist.User.Username }}</a>
|
||||
<p class="text-sm text-slate-500">{{ $.locale.Tr "gist.list.forked" }} <span class="moment-timestamp">{{ $gist.CreatedAt }}</span></p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<a class="ml-auto text-slate-300 relative inline-flex items-center space-x-2 rounded-md border border-gray-600 bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-300 hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3" href="/{{ $gist.User.Username }}/{{ $gist.Uuid }}">
|
||||
<a class="ml-auto text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-md border border-gray-300 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-200 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" href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Uuid }}">
|
||||
<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="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
|
||||
</svg>
|
||||
View fork
|
||||
{{ $.locale.Tr "gist.forks.view" }}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
@@ -28,11 +28,11 @@
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mx-auto h-12 w-12 text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mx-auto h-12 w-12 text-slate-600 dark:text-slate-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
|
||||
</svg>
|
||||
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-300">No public forks</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.forks.no" }}</h3>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "gist_footer" .}}
|
||||
|
||||
44
templates/pages/gist.html
vendored
44
templates/pages/gist.html
vendored
@@ -4,26 +4,42 @@
|
||||
<div class="grid gap-y-4">
|
||||
{{ range $file := .files }}
|
||||
{{ $csv := csvFile $file }}
|
||||
<div class="rounded-md border border-1 border-gray-700 overflow-auto">
|
||||
<div class="border-b-1 border-gray-700 bg-gray-800 my-auto block">
|
||||
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto">
|
||||
<div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto block">
|
||||
<div class="ml-4 py-1.5 flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 flex text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
<span class="flex-auto ml-2 text-sm text-slate-300 filename" id="file-{{ slug $file.Filename }}"><a href="#file-{{ slug $file.Filename }}" class="text-slate-300 hover:text-white">{{ $file.Filename }}</a></span>
|
||||
|
||||
<button class="float-right mx-2 px-2.5 py-0.5 leading-4 rounded-md text-xs font-medium bg-gray-600 border border-gray-500 hover:bg-gray-700 hover:text-slate-300 select-none copy-gist-btn"> Copy </button>
|
||||
<a href="/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/raw/{{ $.commit }}/{{$file.Filename}}" class="text-slate-300 float-right mr-2 px-2.5 py-0.5 leading-4 rounded-md text-xs font-medium bg-gray-600 border border-gray-500 hover:bg-gray-700 hover:text-slate-300 select-none"> Raw </a>
|
||||
<span class="flex-auto inline-flex items-center text-sm text-slate-700 dark:text-slate-300 filename" id="file-{{ slug $file.Filename }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-700 dark:text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
<a href="{{ $.c.ExternalUrl }}#file-{{ slug $file.Filename }}" class="text-slate-700 dark:text-slate-300 hover:text-black dark:hover:text-white ml-2">{{ $file.Filename }}</a></span>
|
||||
|
||||
<span class="isolate inline-flex rounded-md shadow-sm mr-2">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/raw/{{ $.commit }}/{{$file.Filename}}" class="relative inline-flex items-center rounded-l-md bg-white text-gray-500 dark:text-slate-300 float-right px-2.5 py-1 leading-4 text-xs font-medium dark:bg-gray-600 border border-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-slate-700 dark:hover:text-slate-300 select-none">
|
||||
{{ $.locale.Tr "gist.raw" }}
|
||||
</a>
|
||||
<button type="button" class="relative -ml-px inline-flex items-center bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700 copy-gist-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
</button>
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/download/{{ $.commit }}/{{$file.Filename}}" class="relative -ml-px inline-flex items-center rounded-r-md bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<div class="hidden gist-content">{{ $file.Content }}</div>
|
||||
</div>
|
||||
{{ if $file.Truncated }}
|
||||
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-700">
|
||||
This file has been truncated. <a href="/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/raw/{{ $.commit }}/{{$file.Filename}}">View the full file.</a>
|
||||
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700">
|
||||
{{ .locale.Tr "gist.file-truncated" }} <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/raw/{{ $.commit }}/{{$file.Filename}}">{{ .locale.Tr "gist.watch-full-file" }}.</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if and (not $csv) (isCsv $file.Filename) }}
|
||||
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-700">
|
||||
This file is not a valid CSV file.
|
||||
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700">
|
||||
{{ .locale.Tr "gist.file-not-valid" }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -68,10 +84,10 @@
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-600 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-300">No content</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.no-content" }}</h3>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "gist_footer" .}}
|
||||
|
||||
14
templates/pages/likes.html
vendored
14
templates/pages/likes.html
vendored
@@ -1,15 +1,15 @@
|
||||
{{ template "header" .}}
|
||||
{{ template "gist_header" .}}
|
||||
{{ if ne (len .likers) 0 }}
|
||||
<h3 class="text-xl font-bold leading-tight break-all py-2">Likes</h3>
|
||||
<h3 class="text-xl font-bold leading-tight break-all py-2">{{ .locale.Tr "gist.likes" }}</h3>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-5">
|
||||
{{ range $user := .likers }}
|
||||
<div class="relative flex items-center space-x-3 rounded-lg border border-gray-600 bg-gray-800 px-6 py-5 shadow-sm focus-within:ring-1 focus-within:border-primary-500 focus-within:ring-primary-500 hover:border-gray-400">
|
||||
<div class="relative flex items-center space-x-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-6 py-5 shadow-sm focus-within:ring-1 focus-within:border-primary-500 focus-within:ring-primary-500 hover:border-gray-600 dark:hover:border-gray-400">
|
||||
<div class="min-w-0 flex">
|
||||
<img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl $user.MD5Hash }}" alt="">
|
||||
<a href="/{{ $user.Username }}" class="focus:outline-none">
|
||||
<img class="h-12 w-12 rounded-md mr-2 border border-gray-200 dark:border-gray-700" src="{{ avatarUrl $user $.DisableGravatar }}" alt="{{ $user.Username }}'s Avatar">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ $user.Username }}" class="focus:outline-none">
|
||||
<span class="absolute inset-0" aria-hidden="true"></span>
|
||||
<p class="text-sm font-medium text-slate-300 align-middle">{{ $user.Username }}</p>
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300 align-middle">{{ $user.Username }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,11 +20,11 @@
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mx-auto h-12 w-12 text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mx-auto h-12 w-12 text-slate-600 dark:text-slate-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
||||
</svg>
|
||||
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-300">No likes yet</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.likes.no" }}</h3>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "gist_footer" .}}
|
||||
|
||||
31
templates/pages/revisions.html
vendored
31
templates/pages/revisions.html
vendored
@@ -10,8 +10,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
</svg>
|
||||
<img class="h-5 w-5 rounded-full inline" src="{{ avatarUrl (emailToMD5 $commit.AuthorEmail) }}" alt="" />
|
||||
<span class="font-bold">{{ $commit.AuthorName }}</span> revised this gist <span class="moment-timestamp font-bold">{{ $commit.Timestamp }}</span>. <a href="/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/rev/{{ $commit.Hash }}">Go to revision</a></h3>
|
||||
{{ $user := (index $.emails $commit.AuthorEmail) }}
|
||||
<img class="h-5 w-5 rounded-full inline" src="{{if $user }}{{ avatarUrl $user $.DisableGravatar }}{{else}}{{defaultAvatar}}{{end}}" {{if $user }}alt="{{ $user.Username }}'s Avatar"{{end}} />
|
||||
<span class="font-bold">{{if $user}}<a href="{{ $.c.ExternalUrl }}/{{$user.Username}}" class="text-slate-300 hover:text-slate-300 hover:underline">{{ $commit.AuthorName }}</a>{{else}}{{ $commit.AuthorName }}{{end}}</span> {{ $.locale.Tr "gist.revision.revised" }} <span class="moment-timestamp font-bold">{{ $commit.Timestamp }}</span>. <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/rev/{{ $commit.Hash }}">{{ $.locale.Tr "gist.revision.go-to-revision" }}</a></h3>
|
||||
{{ if ne $commit.Changed "" }}
|
||||
<p class="text-sm float-right py-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline-flex">
|
||||
@@ -24,30 +25,30 @@
|
||||
<div class="grid gap-y-4">
|
||||
{{ if ne (len $commit.Files) 0 }}
|
||||
{{ range $file := $commit.Files }}
|
||||
<div class="rounded-md border border-1 border-gray-700 overflow-auto">
|
||||
<div class="border-b-1 border-gray-700 bg-gray-800 my-auto">
|
||||
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto">
|
||||
<div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto">
|
||||
<p class="ml-4 mt-2 inline-flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 flex text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 flex text-slate-700 dark:text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
{{ if $file.IsCreated }}
|
||||
<span class="flex text-sm ml-2 text-slate-300">{{ $file.Filename }}<span class="italic text-gray-400 ml-1">(file created)</span></span>
|
||||
<span class="flex text-sm ml-2 text-slate-700 dark:text-slate-300">{{ $file.Filename }}<span class="italic text-gray-600 dark:text-gray-400 ml-1">({{ $.locale.Tr "gist.revision.file-created" }})</span></span>
|
||||
{{ else if $file.IsDeleted }}
|
||||
<span class="flex text-sm ml-2 text-slate-300">{{ $file.Filename }} <span class="italic text-gray-400 ml-1">(file deleted)</span></span>
|
||||
<span class="flex text-sm ml-2 text-slate-700 dark:text-slate-300">{{ $file.Filename }} <span class="italic text-gray-600 dark:text-gray-400 ml-1">({{ $.locale.Tr "gist.revision.file-deleted" }})</span></span>
|
||||
{{ else if ne $file.OldFilename "" }}
|
||||
<span class="flex text-sm ml-2 text-slate-300">{{ $file.OldFilename }} <span class="italic text-gray-400 mx-1">renamed to</span> {{ $file.Filename }}</span>
|
||||
<span class="flex text-sm ml-2 text-slate-700 dark:text-slate-300">{{ $file.OldFilename }} <span class="italic text-gray-600 dark:text-gray-400 mx-1">{{ $.locale.Tr "gist.revision.file-renamed" }}</span> {{ $file.Filename }}</span>
|
||||
{{ else }}
|
||||
<span class="flex text-sm ml-2 text-slate-300">{{ $file.Filename }}</span>
|
||||
<span class="flex text-sm ml-2 text-slate-700 dark:text-slate-300">{{ $file.Filename }}</span>
|
||||
{{ end }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="overflow-auto">
|
||||
{{ if $file.Truncated }}
|
||||
<p class="m-2 ml-4 text-sm">Diff truncated because its too large to be shown</p>
|
||||
<p class="m-2 ml-4 text-sm">{{ .locale.Tr "gist.revision.diff-truncated" }}</p>
|
||||
{{ else if and (eq $file.Content "") (ne $file.OldFilename "") }}
|
||||
<p class="m-2 ml-4 text-sm">File renamed without changes</p>
|
||||
<p class="m-2 ml-4 text-sm">{{ .locale.Tr "gist.revision.file-renamed-no-changes" }}</p>
|
||||
{{ else if eq $file.Content "" }}
|
||||
<p class="m-2 ml-4 text-sm">Empty file</p>
|
||||
<p class="m-2 ml-4 text-sm">{{ .locale.Tr "gist.revision.empty-file" }}</p>
|
||||
{{ else }}
|
||||
<table class="code table-code w-full whitespace-pre" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0">
|
||||
<tbody>
|
||||
@@ -90,7 +91,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p class="text-left text-sm text-slate-300 italic">No changes</p>
|
||||
<p class="text-left text-sm text-slate-700 dark:text-slate-300 italic">{{ .locale.Tr "gist.revision.no-changes" }}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,10 +102,10 @@
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-600 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-300">No revisions to show</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.revision.no-revisions" }}</h3>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
116
templates/pages/settings.html
vendored
116
templates/pages/settings.html
vendored
@@ -2,75 +2,125 @@
|
||||
<div class="py-10">
|
||||
<header class="pb-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold leading-tight">Settings</h1>
|
||||
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "settings" }}</h1>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="space-y-4">
|
||||
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-16">
|
||||
<div class="sm:grid {{ if or .githubOauth .giteaOauth .oidcOauth }}grid-cols-3{{else}}grid-cols-2{{end}} gap-x-4 md:gap-x-8">
|
||||
<div class="w-full">
|
||||
<div class="bg-gray-900 rounded-md border border-1 border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<h2 class="text-md font-bold text-slate-300">
|
||||
Email
|
||||
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
|
||||
{{ .locale.Tr "settings.email" }}
|
||||
</h2>
|
||||
<h3 class="text-sm text-gray-400 italic mb-4">
|
||||
Used for commits and Gravatar
|
||||
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
|
||||
{{ .locale.Tr "settings.email-help" }}
|
||||
</h3>
|
||||
<form class="space-y-6" action="/settings/email" method="post">
|
||||
<div>
|
||||
<div class="mt-1">
|
||||
<input id="email" name="email" value="{{ .userLogged.Email }}" type="email" required autocomplete="off" class="bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-700 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
<input id="email" name="email" value="{{ .userLogged.Email }}" type="email" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">Set email</button>
|
||||
<button type="submit" 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-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">{{ .locale.Tr "settings.email-set" }}</button>
|
||||
{{ .csrfHtml }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{ if or .githubOauth .giteaOauth .oidcOauth }}
|
||||
<div class="w-full">
|
||||
<div class="bg-gray-900 rounded-md border border-1 border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<h2 class="text-md font-bold text-slate-300">
|
||||
Delete account
|
||||
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300 mb-2">
|
||||
{{ .locale.Tr "settings.link-accounts" }}
|
||||
</h2>
|
||||
<form class="space-y-6" action="/settings/account" method="post" onsubmit="return confirm('Are you sure you want to delete your account ?')">
|
||||
<div class="gap-y-2">
|
||||
|
||||
{{ if .githubOauth }}
|
||||
{{ if .userLogged.GithubID }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
|
||||
onclick="return confirm('Are you sure you want to unlink your GitHub account? You may lose access to Opengist if it\'s your only way to log in.')">
|
||||
{{ .locale.Tr "settings.unlink-github-account" }}
|
||||
</a>
|
||||
{{ else }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "settings.link-github-account" }}
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .giteaOauth }}
|
||||
{{ if .userLogged.GiteaID }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
|
||||
onclick="return confirm('Are you sure you want to unlink your Gitea account? You may lose access to Opengist if it\'s your only way to log in.')">
|
||||
{{ .locale.Tr "settings.unlink-gitea-account" }}
|
||||
</a>
|
||||
{{ else }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "settings.link-gitea-account" }}
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if .oidcOauth }}
|
||||
{{ if .userLogged.OIDCID }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
|
||||
onclick="return confirm('Are you sure you want to unlink your OpenID account? You may lose access to Opengist if it\'s your only way to log in.')">
|
||||
Unlink OpenID account
|
||||
</a>
|
||||
{{ else }}
|
||||
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
Link OpenID account
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="w-full">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
|
||||
{{ .locale.Tr "settings.delete-account" }}
|
||||
</h2>
|
||||
<form class="space-y-6" action="/settings/account" method="post" onsubmit="return confirm('{{ .locale.Tr "settings.delete-account-confirm" }}')">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 mt-2">Delete account</button>
|
||||
<button type="submit" 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-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 mt-2">{{ .locale.Tr "settings.delete-account" }}</button>
|
||||
{{ .csrfHtml }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-16">
|
||||
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8">
|
||||
<div class="w-full">
|
||||
<div class="bg-gray-900 rounded-md border border-1 border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<h2 class="text-md font-bold text-slate-300">
|
||||
Add SSH Key
|
||||
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
|
||||
{{ .locale.Tr "settings.add-ssh-key" }}
|
||||
</h2>
|
||||
<h3 class="text-sm text-gray-400 italic mb-4">
|
||||
Used only to push gists using Git via SSH
|
||||
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
|
||||
{{ .locale.Tr "settings.add-ssh-key-help" }}
|
||||
</h3>
|
||||
<form class="space-y-6" action="/settings/ssh-keys" method="post">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-slate-300"> Title </label>
|
||||
<label for="title" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.add-ssh-key-title" }} </label>
|
||||
<div class="mt-1">
|
||||
<input id="title" name="title" type="text" required autocomplete="off" class="bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-700 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
<input id="title" name="title" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<label for="sshkey" class="block text-sm font-medium text-slate-300"> Key </label>
|
||||
<label for="sshkey" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.add-ssh-key-content" }} </label>
|
||||
<div class="mt-1">
|
||||
<textarea id="sshkey" required autocomplete="off" name="content" class="bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-700 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"></textarea>
|
||||
<textarea id="sshkey" required autocomplete="off" name="content" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-700 text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">Add key</button>
|
||||
<button type="submit" 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-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">{{ .locale.Tr "settings.add-ssh-key" }}</button>
|
||||
{{ .csrfHtml }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mt-6 flow-root">
|
||||
<ul role="list" class="-my-5 divide-y divide-gray-700 list-none">
|
||||
<ul role="list" class="-my-5 divide-y divide-gray-300 dark:divide-gray-700 list-none">
|
||||
{{ if .sshKeys }}
|
||||
{{ range $key := .sshKeys }}
|
||||
<li class="py-5">
|
||||
@@ -79,20 +129,20 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-300">{{ .Title }}</h3>
|
||||
<p class="mt-1 text-xs text-slate-400 line-clamp-2 code" style="overflow-wrap: anywhere">SHA256:{{.SHA}}</p>
|
||||
<p class="text-xs text-gray-500 line-clamp-2">Added <span class="moment-timestamp-date">{{ .CreatedAt }}</span></p>
|
||||
<h3 class="text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .Title }}</h3>
|
||||
<p class="mt-1 text-xs text-slate-600 dark:text-slate-400 line-clamp-2 code" style="overflow-wrap: anywhere">SHA256:{{.SHA}}</p>
|
||||
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-added-at" }} <span class="moment-timestamp-date">{{ .CreatedAt }}</span></p>
|
||||
{{ if eq .LastUsedAt 0 }}
|
||||
<p class="text-xs text-gray-500 line-clamp-2">Never used</p>
|
||||
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-never-used" }}</p>
|
||||
{{ else }}
|
||||
<p class="text-xs text-gray-500 line-clamp-2">Last used <span class="moment-timestamp">{{ .LastUsedAt }}</span></p>
|
||||
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-last-used" }} <span class="moment-timestamp">{{ .LastUsedAt }}</span></p>
|
||||
{{ end }}
|
||||
</div>
|
||||
<form action="/settings/ssh-keys/{{.ID}}" method="post" class="inline-block" onsubmit="return confirm('Confirm deletion of SSH key')">
|
||||
<form action="/settings/ssh-keys/{{.ID}}" method="post" class="inline-block" onsubmit="return confirm('{{ $.locale.Tr "settings.delete-ssh-key-confirm" }}')">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
{{ $.csrfHtml }}
|
||||
|
||||
<button type="submit" class="align-middle items-center leading-2 ml-2 px-3 py-1 border border-transparent border-gray-700 text-xs font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">Delete</button>
|
||||
<button type="submit" class="align-middle items-center leading-2 ml-2 px-3 py-1 border border-transparent border-gray-200 dark:border-gray-700 text-xs font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">{{ $.locale.Tr "settings.delete-ssh-key" }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
7
vite.config.js
vendored
7
vite.config.js
vendored
@@ -9,7 +9,8 @@ export default defineConfig({
|
||||
assetsDir: 'assets',
|
||||
manifest: true,
|
||||
rollupOptions: {
|
||||
input: ['./public/main.ts', './public/editor.ts']
|
||||
}
|
||||
input: ['./public/main.ts', './public/editor.ts', './public/admin.ts', './public/hljs.ts']
|
||||
},
|
||||
assetsInlineLimit: 0,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user