Author SHA1 Message Date
Floriansylvain 4a6209a8db feat: api serve frontend, build workflow, router readyness 2025-12-24 01:05:58 +01:00
Floriansylvain aee595dee9 feat: primevue base 2025-12-23 23:14:04 +01:00
Floriansylvain 9ecaa80fcf feat: initial vuejs frontend commit 2025-12-22 17:45:19 +01:00
Floriansylvain fbbce403e2 fix: allowed register condition 2025-12-22 03:07:30 +01:00
Floriansylvain 9c80cb9885 feat: unit testing mocked repos / domain 2025-12-22 02:49:57 +01:00
Floriansylvain ce787d26ff fix: remove low file code from domain 2025-12-22 02:41:09 +01:00
Floriansylvain 76245c47c9 fix: added some business rules to article creation 2025-12-22 01:18:50 +01:00
Floriansylvain 5282770caa refac: better error handling & moved IsOnline logic to cleaner place 2025-12-22 00:59:36 +01:00
Floriansylvain b50efae8a8 refac: mappers, file names 2025-12-22 00:06:17 +01:00
Floriansylvain e0a6706d4e fix: removed domain article coupling with infra 2025-12-21 21:53:29 +01:00
Floriansylvain 0eba18e367 fix: removed useless file 2025-12-21 21:34:07 +01:00
Floriansylvain 61a84201d8 refac: remove frontend & clearer DDD 2025-12-21 21:31:30 +01:00
Floriansylvain 6796ce63cb fix: missing verification code handling 2025-12-13 18:53:30 +01:00
Floriansylvain 99812ddba4 fix: update file path separator in .air.toml and improve server start message 2025-12-13 18:53:17 +01:00
Florian Sylvain 0238c7511d refac: useless little refactor for clean verificationCode placement 2025-12-09 22:27:59 +01:00
Florian Sylvain d7ee0a488c refac: cleaner DI function names 2025-11-17 20:24:51 +01:00
Florian Sylvain c57394d4fd mod: upgraded Go version and all dependencies to latest stable versions 2025-11-17 20:05:50 +01:00
Florian Sylvain f47f145b6c fix: unused param and missing return 2025-11-17 19:51:38 +01:00
Florian Sylvain 83a1dec8ae refac: renamed 'GoCMS' -> 'RenewCMS' 2025-11-17 19:46:53 +01:00
Florian Sylvain b470bfcc67 refac: renamed 'post' -> 'article' 2025-11-17 19:27:48 +01:00
Florian Sylvain cb9a431488 fix: reverted the bourbier done to DI, reflection is bad 2025-03-29 01:52:22 +01:00
Florian Sylvain 2292a8ef34 fix: DI wrong approach wasn't working 2025-03-29 01:45:48 +01:00
Florian Sylvain a77a5ad314 refac: fixed, better, smarter, correct dependency injection 2025-03-29 00:51:01 +01:00
Florian Sylvain d895477b8c tech: refact build script & added arm64 builds 2025-03-13 21:49:59 +01:00
Florian Sylvain 6563db43dc tech: updated go version on action 2025-03-13 19:50:31 +01:00
Florian Sylvain 6fb666fc89 tech: updated dependencies 2025-03-13 19:47:06 +01:00
Florian Sylvain 4d6054e22b refac: separated api content into sub-packages & renamed usecases 2025-03-13 19:43:12 +01:00
Florian Sylvain 7e29c08ea9 fix: hide offline posts from get route 2024-06-14 16:46:16 +02:00
Florian Sylvain c69876a704 feat: draft/online post field #26 2024-06-14 16:40:01 +02:00
Florian Sylvain 0759ab6d70 feat: integration page #41 2024-06-14 15:23:51 +02:00
Florian Sylvain e4c2230b64 feat: password reset #32 2024-06-14 13:46:39 +02:00
Florian Sylvain 727418c8e6 feat: lists, visualblocks and custom toolbar in TinyMCE editor 2024-06-14 08:37:13 +02:00
Florian Sylvain f46737399f fix: removed direct access to static folders 2024-06-13 14:25:38 +02:00
Florian Sylvain 26984980b7 fix: «{{.Post.ID}} appears in a JS template literal» 2024-06-13 14:03:21 +02:00
Florian Sylvain c25fb1b0a3 feat: images 2024-06-13 13:34:10 +02:00
Florian Sylvain eb98a1003e fix: replaced post name with its id in URLs 2024-06-12 16:23:33 +02:00
Florian Sylvain 46b816a8af tech: upgraded deps to fix vulnerabilities 2024-06-12 11:33:43 +02:00
Florian Sylvain ad1692d168 fix: typos and posts dates format 2024-06-11 11:57:28 +02:00
Florian Sylvain bcf3c19702 fix: added tinymce license_key 2024-05-22 15:01:08 +02:00
Florian Sylvain bfc310aeab tech: updated tinymce to 7.1.1 2024-05-22 14:44:55 +02:00
Florian Sylvain adcd6fc08a fix: added cache on static files 2024-05-22 14:14:04 +02:00
Florian Sylvain ed5d52a506 fix: standardized ID retrieval in routes with IDs 2024-05-20 12:42:12 +02:00
Florian Sylvain 5e9098aa20 Merge pull request #34 from Floriansylvain/feature/postDeletion
feat: post deletion
2024-05-19 17:20:26 +02:00
Florian Sylvain 7d2795a9e0 feat: post deletion 2024-05-19 17:12:48 +02:00
Florian Sylvain 1b6690802f Merge pull request #33 from Floriansylvain/feature/postCreation
Feature/post creation
2024-05-18 12:48:08 +02:00
Florian Sylvain c1317e9f21 vcs: removed bootstrap svg icons 2024-05-18 12:46:42 +02:00
Florian Sylvain 92c6d06d00 feat: post edition save 2024-05-18 12:24:54 +02:00
Florian Sylvain 900ad3e8b8 feat: post list and edition redirect 2024-05-17 16:38:41 +02:00
Florian Sylvain 24eefb743b feat: post creation WIP 2024-05-16 19:30:33 +02:00
Florian Sylvain 29fce9a241 brand: GohCMS -> GoCMS, project-wide 2024-05-15 19:34:38 +02:00
Florian Sylvain 7097d71f45 Merge pull request #31 from Floriansylvain/feature/registerCancel
feat: complete email verification flow
2024-05-15 19:19:43 +02:00
Florian Sylvain a35e7c4283 feat: complete email verification flow 2024-05-15 19:16:55 +02:00
Florian Sylvain aabdbfae95 Merge pull request #30 from Floriansylvain/feature/authMailing
Feature/auth mailing
2024-05-15 13:53:20 +02:00
Florian Sylvain 6c54c9607c Update go.yml 2024-05-15 13:48:49 +02:00
Florian Sylvain 9c4d4fa87c Update go.yml 2024-05-15 13:43:59 +02:00
Florian Sylvain a331c9173b Update go.yml 2024-05-15 13:22:35 +02:00
Florian Sylvain 830b062a76 Update go.yml 2024-05-15 13:07:13 +02:00
Florian Sylvain 3b57961946 Update go.yml 2024-05-15 12:12:53 +02:00
Florian Sylvain c929acd4ab feat: email verification flow (not 100% complete) 2024-05-15 12:04:30 +02:00
Florian Sylvain 37647e60d9 fix: changed my mind on hash cost 2024-05-15 08:38:05 +02:00
Florian Sylvain 1a75615be4 fix: reduced too expensive passwords hash cost 2024-05-15 08:32:01 +02:00
Florian Sylvain 4d6a799c5d fix: comparing verificationCode hashes 2024-05-14 20:41:33 +02:00
Florian Sylvain ec14eee439 feat: WIP mail verification flow 2024-05-14 19:37:44 +02:00
Florian Sylvain 8015639db5 feat: WIP auth mailing implementation 2024-05-13 19:22:56 +02:00
Florian Sylvain d146363246 tech: updated dependencies 2024-05-13 13:28:24 +02:00
Florian Sylvain 00a4e464ff clean: refactor auth & login page auth 2024-05-13 11:11:58 +02:00
Florian Sylvain dca17a744d doc: added .env example file and e.g. for DOCKER_DB_FOLDER 2024-04-26 07:56:18 +02:00
Florian Sylvain 2eae066a24 fix: added recursion on linguist-vendored attribute 2024-04-26 07:42:42 +02:00
Florian Sylvain c90fa26c73 tech: removed api/static/* from github lang analysis 2024-04-26 07:39:43 +02:00
Florian Sylvain 33cd947958 clean: refactor DI and container initialization 2024-04-26 06:58:43 +02:00
Florian Sylvain 2805425317 fix: removed localhost from server hostname 2023-12-04 16:28:13 +01:00
Florian Sylvain 411adc02b0 fix: copy .env into container 2023-12-04 15:31:24 +01:00
Florian Sylvain c663bcf4a9 Update: Improved UI and functionality of posts and post-edit pages
This change includes both UI and functionality enhancements in 'posts.html' and 'postEdit.html' within the web templates directory. For posts.html, the caption under the table of posts has been replaced with a paragraph tag for better readability. A more comprehensive redesign has been done on postEdit.html, ensuring a more user-friendly page layout with enhanced post edition capabilities. In addition, minor updates were made to the go.sum and go.mod file as a result of new dependencies and versions required by these changes.
2023-10-31 16:07:45 +01:00
Florian Sylvain 777666a92f clean: removed unwanted build file 2023-10-16 10:02:04 +02:00
Florian Sylvain 55d9e3bfc1 clean: english
The changes in this commit were made to translate the website's text content from French to English, as part of efforts to ensure the platform is more accessible to a wider audience. This includes translation of user-facing instructions, button labels, form fields, and messages across multiple templates including home.html, setup1.html, setup2.html, and login.html among others.
2023-10-16 09:47:04 +02:00
Florian Sylvain 7d071ebc2e clean: refactor templates to use a common head section
This commit introduces a refactoring in various page templates to use a central, common 'head' section, stored under utilsHead.html. This head section contains common metadata, styles, and scripts. This change enhances code reusability and standardizes the head section across different pages for consistent user experience and ease of future potential changes. Also, some minimal adjustments were made in the navigation and link routing for more user-friendly URL paths. Lastly, the server's listening interface was updated to only listen on the localhost.
2023-10-11 20:28:00 +02:00
Florian Sylvain c2b64bf0fa feat: post pages placeholders + tinymce lib 2023-09-06 04:48:16 +02:00
Florian Sylvain ea942355ab feat: templ_static_files_validator
super_build.bat script was removed because it became unnecessary after transitioning to a Docker-based build process which provides cross-platform build capability more efficiently (and Windows scripting sucks). Also added templ_static_files_validator.sh that validates existence of files linked in HTML within the specified directory. Can be useful when working on frontend to make sure static files will load properly.
2023-09-06 04:36:14 +02:00
Florian Sylvain e113c79a6d fix: all remaining "article" replaced by "post"
All instances of the term "article" were replaced with "post" in variable names, type names, function names, etc. This was primarily to ensure consistent naming and reduce confusion going forward. The term "post" was chosen as it's a more commonly used term for the involved operations in the context of content management and blogging platforms. All tests were also updated to reflect these changes.
2023-09-06 03:38:52 +02:00
Florian Sylvain 509329951f clean: Article to Post in post.go
Changes was made to keep consistency in terminology across the application.
2023-09-06 03:32:44 +02:00
Florian Sylvain 5bd4b4769f feat: Posts page impl and associated routes
Implemented a new 'Posts' page which has been added to the main navigation. This includes:
- Adding the 'posts.html' template with basic HTML and related CSS/JS
- Updating the 'componentNavbar.html' template to include a link to the new Posts page
- Writing the 'GetPostsPage' function in 'api/page.go' to handle requests for the Posts page
- Modifying the 'home.html' template to provide a link towards the Posts Page
- Adding an associated route in 'api/page.go'.
2023-09-06 02:20:27 +02:00
Florian Sylvain 554fdc1989 clean: Article to Post in domain and related use cases
Renamed the 'Article' domain to 'Post' in order to better reflect the purpose and utility of the object. The change involved renaming all related use cases, classes, and other references from 'Article' to 'Post' and updating all functions and data types to work with Posts instead of Articles.
2023-09-06 01:56:27 +02:00
Florian Sylvain 46491a688b clean: refacto User and Article repos for clarity
Simplified the code for creating and returning user and article entities in userRepository.go and articleRepository.go. Direct function calls to domain.FromDb(...) were replaced with the more descriptive mapping functions mapUserToDomain(...) and mapArticleToDomain(...). This ensures better code readability and understanding and provides a single, consistent way to map database entities to domain entities.
2023-08-17 05:55:31 +02:00
Florian Sylvain d91dc256a2 feat: docker and docker-compose support
Introduced a Dockerfile and compose.yml to allow the application to run inside a Docker container. This removes the need for local system-level dependencies and offers a more predictable environment for the app. README.md was updated to include Docker-related instructions and an additional environment variable `DOCKER_DB_FOLDER` was added on top of existing ones for Docker support.
2023-08-13 23:40:12 +02:00
Florian Sylvain 60e25cea2f fix: set "DB_FILE" environment variable in tests
This commit adds a line to set the "DB_FILE" environment variable in tests. This ensures that tests are always using "test.db" for database related operations, aligning testing environment accurately which guarantees consistent results.
2023-08-13 21:41:04 +02:00
Florian Sylvain a785483220 fix: error messages and modify password validation
This commit enhances the user experience in two ways:

1. The feedback messages within the setup1.html file have been updated for both the username and password fields. The new messages better inform users about the rules for valid input— stating that the username must be 3-20 characters long and the password must contain at least eight characters.

2. The password validation process has been adjusted. Now, only the confirm password field gets marked as invalid when the entered passwords don't match. This helps users understand exactly which field needs correction.
2023-08-13 21:40:17 +02:00
Florian Sylvain b414a07f6d fix: added missing DB_FILE env var 2023-08-13 04:36:48 +02:00
Florian Sylvain 245bf2fbf4 feat: environment specific database handling
Changes have been made to support environment specific database file handling. By introducing a new environment variable "DB_FILE", the application's runtime now reads the database file path from the environment. In addition, subdirectories will now be automatically created if they don't exist and more detailed error messages will be printed in case of a failure. Environment variable setup and project building instructions were also updated in the README file.
2023-08-13 04:34:30 +02:00
Florian Sylvain 64ecbc6995 clean: removed ignored err var declaration 2023-08-13 03:21:47 +02:00
Florian Sylvain c6bd7731cc clean: removed use of "." in imports 2023-08-13 02:48:19 +02:00
Florian Sylvain 2525a64d0a feat: client form validation on register page 2023-08-13 01:42:01 +02:00
Florian Sylvain 04ebbfda87 feat: setup/register flow impl and login refactor 2023-08-13 00:24:50 +02:00
Florian Sylvain 5c61796313 clean: made pageError struct type 2023-08-12 22:38:01 +02:00
Florian Sylvain e575062209 clean: removed too deeply nested tests 2023-08-12 20:52:32 +02:00
Florian Sylvain 1539e06ec8 clean: moved funcs to specific files + GetPage usecase/repo 2023-08-12 20:42:43 +02:00
Florian Sylvain 869fb3f145 fix: home.html to api dir, remove empty style tag 2023-08-12 19:51:22 +02:00
Florian Sylvain 7e8b415cab feat: Add build scripts, refactor for binary embedding 2023-08-12 19:42:27 +02:00
Florian Sylvain 047e14df60 fix: made .env file not mendatory 2023-08-12 02:50:01 +02:00
Florian Sylvain 8faf3f8d9d fix: added env vars 2023-08-12 02:44:41 +02:00
Florian Sylvain 542bcded3e fix: panic on test.db rm err 2023-08-12 02:39:42 +02:00
Florian Sylvain 46daadb8e4 feat: tests (article) 2023-08-12 02:33:40 +02:00
Florian Sylvain 0466da2d4b Merge remote-tracking branch 'origin/main' 2023-08-11 21:12:44 +02:00
Florian Sylvain 318fd0f53a feat: cors origins env var 2023-08-11 21:12:31 +02:00
Florian Sylvain 2ddabe2222 feat: go.yml for GitHub build 2023-08-11 13:57:17 +02:00
Florian Sylvain d953a66f22 feat: env variables introduction 2023-08-11 04:02:18 +02:00
Florian Sylvain 4b918aa2fa feat: GitHub link and style adjustments 2023-08-11 02:05:28 +02:00
Florian Sylvain 7611738b48 feat: style improvements 2023-08-11 01:19:55 +02:00
Florian Sylvain 366ae190ff feat: username recovery on login post 2023-08-11 01:19:40 +02:00
Florian Sylvain 96a64097c4 feat: added *.exe to .gitignore 2023-08-10 23:45:54 +02:00
Florian Sylvain 0c139b1f72 feat: added navbar system with some refactors 2023-08-10 23:44:55 +02:00
Florian Sylvain 209555498f clean: moved http.SetCookie into its own func 2023-08-10 23:43:41 +02:00
Florian Sylvain 61f221e49c cean: fixed html style 2023-08-09 18:20:56 +02:00
Florian Sylvain 1d1adfdfc7 feat: validation on articles 2023-08-09 18:13:03 +02:00
Florian Sylvain 836847e9b2 clean: removed useless usecases declaration 2023-08-09 18:07:31 +02:00
Florian Sylvain e3246e337d clean: merged similar usecases to getUser 2023-08-09 17:06:40 +02:00
Florian Sylvain dad0c906f0 feat: first frontend implementation 2023-08-08 05:14:31 +02:00
Florian Sylvain 91d82b0ad7 feat: logout route 2023-08-07 12:36:54 +02:00
Florian Sylvain f33f53ca76 feat: users & authentication implementation 2023-08-07 12:31:29 +02:00
Florian Sylvain e258dccc1c feat: migrated prisma -> gorm 2023-08-06 19:13:49 +02:00
Florian Sylvain 195c247235 feat: go-chi and first routes setup 2023-08-06 05:00:15 +02:00
Florian Sylvain 5b4b5a8622 clean: first clean archi sandbox implementation 2023-08-05 14:37:53 +02:00
Florian Sylvain 4187f0b2e2 clean: removed .idea and added IDEs specifics to .gitignore 2023-08-02 01:35:48 +02:00
Florian Sylvain 1328758984 clean: removed everything to set up 2.0 2023-08-02 01:31:35 +02:00
192 changed files with 3639 additions and 9266 deletions
+51
View File
@@ -0,0 +1,51 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "tmp/main.exe"
cmd = "go build -o ./tmp/main.exe ./cmd/server"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "api\\static\\tinymce", "api\\static\\bootstrap-icons", "api\\static\\uploadedImages"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true
+12
View File
@@ -0,0 +1,12 @@
ENVIRONMENT=development
HOST=example.com
PORT=8080
JWT_SECRET=abc123
CORS_ALLOWED_ORIGINS=*
DB_FILE=./renewcms.db
DOCKER_DB_FOLDER=./data
UPLOAD_DIR=./api/static/uploadedImages/
SMTP_EMAIL=a@b.c
SMTP_PASSWORD=12341234
SMTP_HOST=mail.service.abc
SMTP_PORT=465
+1
View File
@@ -0,0 +1 @@
api/static/** linguist-vendored
+40
View File
@@ -0,0 +1,40 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
environment: test
env:
ENVIRONMENT: ${{ secrets.ENVIRONMENT }}
HOST: ${{ secrets.HOST }}
PORT: ${{ secrets.PORT }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
CORS_ALLOWED_ORIGINS: ${{ secrets.CORS_ALLOWED_ORIGINS }}
DB_FILE: ${{ secrets.DB_FILE }}
DOCKER_DB_FOLDER: ${{ secrets.DOCKER_DB_FOLDER }}
SMTP_EMAIL: ${{ secrets.SMTP_EMAIL }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.23.0"
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
+16 -29
View File
@@ -1,31 +1,18 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
# IDE
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vscode
mongodbdata
.env
# DB
*.db
# Build
bin
dist/
# Environment
.env
# Generated files
tmp/
uploadedImages/
*.exe
+16
View File
@@ -0,0 +1,16 @@
FROM golang:1.20.5-alpine
WORKDIR /app
COPY go.mod go.sum ./
COPY adapters ./adapters
COPY api ./api
COPY domain ./domain
COPY main ./main
COPY useCases ./useCases
COPY .env ./.env
RUN go mod download
RUN go build -o startServer ./main
CMD ["./startServer"]
-14
View File
@@ -1,14 +0,0 @@
FROM golang:1.19
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
RUN go mod download
ADD cmd ./cmd/
ADD internal ./internal/
COPY .env ./
RUN go build -o ./GohCMS/ ./...
CMD ["./GohCMS/cmd"]
-12
View File
@@ -1,12 +0,0 @@
FROM node:latest as build-stage
WORKDIR /app
COPY ./web/admin-gui/package*.json ./
COPY ./.env ./
RUN npm install
COPY ./web/admin-gui/ .
RUN npm run build
FROM nginx as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY ./web/admin-gui/nginx.conf /etc/nginx/nginx.conf
+39 -16
View File
@@ -1,4 +1,4 @@
# GohCMS
# RenewCMS
## 🚧 This project is under development !
@@ -10,29 +10,48 @@ Don't forget to setup the [Environment variables](#environment-variables)!
### Use with Docker
To **run** the app, run the command
#### Compose
```shell
docker-compose up
The easiest way to run the project is to use docker-compose.
You still need to set up the environment variables, but you can use the `.env` file for that.
Note that some supplementary variables are required for docker-compose to work, like `DOCKER_DB_FOLDER`.
```bash
docker compose up --build -d
```
### Build and run it yourself
TODO
#### Build
There are 2 scripts to build the project:
- `super_build.sh`
- `super_build.bat`
They will build the project for windows, linux and macOS. The binaries will be in the `bin` folder.
You can also really just use `go build` to build the project for your current OS.
#### Run
Once the project is built, you can simply run the binary for your OS. Setting the environment variables is
of course required, but not necessarily via the `.env` file.
### Environment variables
- ./env:
```
APP_API_ADDRESS=http://example.com
APP_FRONT_ADDRESS=http://example.com
APP_BASE_API_PATH=/example
APP_BASE_FRONT_PATH=/example
APP_API_PORT=1234
APP_FRONT_PORT=1234
APP_JWT_SECRET=secret
```
| Name | Type | Description | Comment |
|----------------------|--------|:----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| ENVIRONMENT | string | environment the API is running in | required, `development` or `production` |
| HOST | string | the host domain name for emails and callbacks | required, if `localhost`: add the port (e.g. `localhost:8080`) |
| PORT | int | port the API will use | required |
| JWT_SECRET | string | secret for the jwt auth | required |
| CORS_ALLOWED_ORIGINS | string | allowed origins for CORS | required, semicolon separated list |
| DB_FILE | string | path to the sqlite db file | required, can be at the root but name still required (e.g. `./RenewCMS.db`) ; have to end up with `.db` |
| DOCKER_DB_FOLDER | string | path to the sqlite db file's folder | required with docker, this will basically be the host machine folder (e.g. `./data`) that contains the sqlite db file |
| SMTP_EMAIL | string | sender email | required |
| SMTP_PASSWORD | string | smtp account password | required |
| SMTP_HOST | string | smtp server address | required |
| SMTP_PORT | int | smtp server port | required |
## API Usage
@@ -41,3 +60,7 @@ TODO
## Demo
TODO
## TODOs
- Cancel button when email validation pending or expired
+93
View File
@@ -0,0 +1,93 @@
package api
import (
domainArticle "RenewCMS/internal/domain/article"
domainImage "RenewCMS/internal/domain/image"
domainMail "RenewCMS/internal/domain/mail"
domainUser "RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/mailer"
"RenewCMS/internal/infrastructure/persistence/models"
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
"RenewCMS/internal/infrastructure/useCases"
"os"
"path/filepath"
"github.com/glebarez/sqlite"
"github.com/go-chi/jwtauth/v5"
"gorm.io/gorm"
)
type UseCases struct {
Token *jwtauth.JWTAuth
CreateArticleUseCase *useCases.CreateArticleUseCase
GetArticleUseCase *useCases.GetArticleUseCase
ListArticlesUseCase *useCases.ListArticlesUseCase
UpdateArticleUseCase *useCases.UpdateArticleUseCase
DeleteArticleUseCase *useCases.DeleteArticleUseCase
GetUserUseCase *useCases.GetUserUseCase
CreateUserUseCase *useCases.CreateUserUseCase
UpdateUserUseCase *useCases.UpdateUserUseCase
DeleteUserUseCase *useCases.DeleteUserUseCase
ListUsersUseCase *useCases.ListUsersUseCase
SendMailUseCase *useCases.SendMailUseCase
CreateImageUseCase *useCases.CreateImageUseCase
DeleteImageUseCase *useCases.DeleteImageUseCase
}
type Repositories struct {
ArticleRepo domainArticle.Repository
UserRepo domainUser.Repository
ImageRepo domainImage.Repository
MailRepo domainMail.Repository
}
func getDb() *gorm.DB {
dbName := os.Getenv("DB_FILE")
if err := os.MkdirAll(filepath.Dir(dbName), os.ModePerm); err != nil {
panic("Unable to create necessary subdirectories: " + err.Error())
}
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
if err != nil {
panic("Unable to open the database: " + err.Error())
}
if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil {
panic("Failed to migrate database: " + err.Error())
}
return db
}
func getRepositories(db *gorm.DB) *Repositories {
return &Repositories{
ArticleRepo: persistence.NewArticleRepository(db),
UserRepo: persistence.NewUserRepository(db),
ImageRepo: persistence.NewImageRepository(db),
MailRepo: mailer.NewMailRepository(),
}
}
func getUseCases(repos *Repositories) *UseCases {
return &UseCases{
Token: jwtauth.New("HS256", []byte(os.Getenv("JWT_SECRET")), nil),
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo),
GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo),
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo),
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo),
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo),
GetUserUseCase: useCases.NewGetUserUseCase(repos.UserRepo),
CreateUserUseCase: useCases.NewCreateUserUseCase(repos.UserRepo),
UpdateUserUseCase: useCases.NewUpdateUserUseCase(repos.UserRepo),
DeleteUserUseCase: useCases.NewDeleteUserUseCase(repos.UserRepo),
ListUsersUseCase: useCases.NewListUsersUseCase(repos.UserRepo),
SendMailUseCase: useCases.NewSendMailUseCase(repos.MailRepo),
CreateImageUseCase: useCases.NewCreateImageUseCase(repos.ImageRepo),
DeleteImageUseCase: useCases.NewDeleteImageUseCase(repos.ImageRepo),
}
}
func InitContainer() *UseCases {
db := getDb()
repos := getRepositories(db)
return getUseCases(repos)
}
+131
View File
@@ -0,0 +1,131 @@
package article
import (
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
domain "RenewCMS/internal/domain/article"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
type Handler struct {
CreateUseCase *useCases.CreateArticleUseCase
GetUseCase *useCases.GetArticleUseCase
ListUseCase *useCases.ListArticlesUseCase
DeleteUseCase *useCases.DeleteArticleUseCase
}
type PostArticle struct {
Title string `json:"title" validate:"required,min=3,max=50"`
Body string `json:"body" validate:"required,max=10000"`
}
func NewHandler(create *useCases.CreateArticleUseCase, get *useCases.GetArticleUseCase, list *useCases.ListArticlesUseCase, delete *useCases.DeleteArticleUseCase) *Handler {
return &Handler{
CreateUseCase: create,
GetUseCase: get,
ListUseCase: list,
DeleteUseCase: delete,
}
}
func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
article, err := h.GetUseCase.GetOnlineArticle(uint32(id))
if err != nil {
if errors.Is(err, domain.ErrArticleNotFound) {
http.Error(w, "Article not found", http.StatusNotFound)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
articleJson, err := json.Marshal(article)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if _, err := w.Write(articleJson); err != nil {
fmt.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
var localArticle PostArticle
if err := json.NewDecoder(r.Body).Decode(&localArticle); err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
if err := validator.Validate.Struct(localArticle); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
createdArticle, err := h.CreateUseCase.CreateArticle(useCases.CreateArticleCommand{
Title: localArticle.Title,
Body: localArticle.Body,
})
if err != nil {
http.Error(w, "Failed to create article", http.StatusInternalServerError)
return
}
articleJson, err := json.Marshal(createdArticle)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
if _, err := w.Write(articleJson); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) ListArticles(w http.ResponseWriter, _ *http.Request) {
articles, err := h.ListUseCase.ListOnlineArticles()
if err != nil {
http.Error(w, "Failed to retrieve articles", http.StatusInternalServerError)
return
}
articlesJson, err := json.Marshal(articles)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if _, err := w.Write(articlesJson); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) DeleteArticle(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
if err := h.DeleteUseCase.DeleteArticle(uint32(id)); err != nil {
http.Error(w, "Failed to delete article", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
+196
View File
@@ -0,0 +1,196 @@
package auth
import (
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"net/http"
"os"
"time"
"github.com/go-chi/jwtauth/v5"
"golang.org/x/crypto/bcrypt"
)
type Handler struct {
Token *jwtauth.JWTAuth
GetUseCase *useCases.GetUserUseCase
ListUseCase *useCases.ListUsersUseCase
CreateUseCase *useCases.CreateUserUseCase
}
type RegisterCredentials struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Password string `json:"password" validate:"required,min=8"`
Email string `json:"email" validate:"required,email"`
}
type LoginCredentials struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Password string `json:"password" validate:"required,min=8"`
}
func shouldCookieBeSecure() bool {
return os.Getenv("ENVIRONMENT") == "production"
}
func removeJwtCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "jwt",
Value: "",
Expires: time.Now(),
MaxAge: -1,
Secure: shouldCookieBeSecure(),
HttpOnly: true,
Path: "/",
})
}
func NewHandler(token *jwtauth.JWTAuth, get *useCases.GetUserUseCase, list *useCases.ListUsersUseCase, create *useCases.CreateUserUseCase) *Handler {
return &Handler{
Token: token,
GetUseCase: get,
ListUseCase: list,
CreateUseCase: create,
}
}
func (h *Handler) SetJwtCookie(w *http.ResponseWriter, userId uint32) error {
_, tokenString, err := h.Token.Encode(map[string]any{"user_id": userId})
if err != nil {
return err
}
http.SetCookie(*w, &http.Cookie{
Name: "jwt",
Value: tokenString,
Expires: time.Now().Add(24 * time.Hour),
Secure: shouldCookieBeSecure(),
HttpOnly: true,
Path: "/",
})
return nil
}
func (h *Handler) SomeUsersVerified() bool {
users := h.ListUseCase.ListUsers()
for _, localUser := range users {
if localUser.IsVerified {
return true
}
}
return false
}
func (h *Handler) IsUserTableEmpty() bool {
users := h.ListUseCase.ListUsers()
return len(users) == 0
}
func (h *Handler) IsLoggedIn(r *http.Request) bool {
token, err := jwtauth.VerifyRequest(
h.Token,
r,
jwtauth.TokenFromCookie,
jwtauth.TokenFromHeader,
jwtauth.TokenFromQuery)
return token != nil && err == nil
}
func (h *Handler) IsVerified(r *http.Request) bool {
token, err := jwtauth.VerifyRequest(
h.Token,
r,
jwtauth.TokenFromCookie,
jwtauth.TokenFromHeader,
jwtauth.TokenFromQuery)
if err != nil {
return false
}
userId := token.PrivateClaims()["user_id"].(float64)
currentUser, _ := h.GetUseCase.GetUser(uint32(userId))
return currentUser.IsVerified
}
func (h *Handler) GetUserFromCredentials(credentials LoginCredentials) (user.User, error) {
dbUser, err := h.GetUseCase.GetUserByUsername(credentials.Username)
if err != nil {
return user.User{}, err
}
err = bcrypt.CompareHashAndPassword([]byte(dbUser.Password), []byte(credentials.Password))
if err != nil {
return user.User{}, err
}
return dbUser, nil
}
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
var credentials LoginCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
err = validator.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
dbUser, err := h.GetUserFromCredentials(credentials)
if err != nil {
http.Error(w, apperr.LogsErrorMessage, http.StatusForbidden)
return
}
_ = h.SetJwtCookie(&w, dbUser.ID)
message, _ := json.Marshal(map[string]any{"message": "User logged in! HTTPonly jwt cookie created"})
_, _ = w.Write(message)
}
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
if h.SomeUsersVerified() {
http.Error(w, "You are not allowed to create a user. Log in or reset database.", http.StatusForbidden)
return
}
var credentials RegisterCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
err = validator.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
createdUser, err := h.CreateUseCase.CreateUser(useCases.CreateUserCommand{
Username: credentials.Username,
Password: credentials.Password,
Email: credentials.Email,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = h.SetJwtCookie(&w, createdUser.ID)
message, _ := json.Marshal(map[string]any{"message": "User registered! HTTPonly jwt cookie created"})
_, _ = w.Write(message)
}
func (h *Handler) Logout(w http.ResponseWriter, _ *http.Request) {
removeJwtCookie(w)
message, _ := json.Marshal(map[string]any{"message": "User logged out! HTTPonly jwt cookie deleted"})
_, _ = w.Write(message)
}
+79
View File
@@ -0,0 +1,79 @@
package frontend
import (
"RenewCMS/internal/infrastructure/assets"
"io"
"io/fs"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/go-chi/chi/v5"
)
type spaHandler struct {
staticFS fs.FS
modTime time.Time
}
func NewFrontendRouter() http.Handler {
r := chi.NewRouter()
distFS, _ := fs.Sub(assets.DistEmbed, "dist")
handler := &spaHandler{
staticFS: distFS,
modTime: time.Now(),
}
r.Get("/*", handler.ServeHTTP)
return r
}
func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := h.cleanPath(r.URL.Path)
if h.fileExists(path) {
h.serveAsset(w, r, path)
return
}
h.serveIndex(w, r)
}
func (h *spaHandler) cleanPath(path string) string {
return strings.TrimPrefix(filepath.Clean(path), "/")
}
func (h *spaHandler) fileExists(path string) bool {
if path == "" || path == "." {
return false
}
f, err := h.staticFS.Open(path)
if err != nil {
return false
}
defer f.Close()
return true
}
func (h *spaHandler) serveAsset(w http.ResponseWriter, r *http.Request, path string) {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
http.FileServer(http.FS(h.staticFS)).ServeHTTP(w, r)
}
func (h *spaHandler) serveIndex(w http.ResponseWriter, r *http.Request) {
f, err := h.staticFS.Open("index.html")
if err != nil {
http.Error(w, "Index not found", http.StatusNotFound)
return
}
defer f.Close()
content, ok := f.(io.ReadSeeker)
if !ok {
http.Error(w, "File not seekable", http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
http.ServeContent(w, r, "index.html", h.modTime, content)
}
+59
View File
@@ -0,0 +1,59 @@
package image
import (
"RenewCMS/internal/domain/image"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
type Handler struct {
CreateUseCase *useCases.CreateImageUseCase
UpdateUseCase *useCases.UpdateArticleUseCase
}
func NewHandler(create *useCases.CreateImageUseCase, update *useCases.UpdateArticleUseCase) *Handler {
return &Handler{
CreateUseCase: create,
UpdateUseCase: update,
}
}
func (h *Handler) PostImage(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
id := chi.URLParam(r, "id")
idInt, _ := strconv.Atoi(id)
file, fileHeader, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
newImage, err := h.CreateUseCase.CreateImage(image.ImageInput{
Content: file,
Filename: fileHeader.Filename,
Size: fileHeader.Size,
ContentType: fileHeader.Header.Get("Content-Type"),
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = h.UpdateUseCase.AddImage(uint32(idInt), newImage.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
newJson := map[string]any{"location": newImage.Path}
newJsonBytes, _ := json.Marshal(newJson)
_, _ = w.Write(newJsonBytes)
}
+10
View File
@@ -0,0 +1,10 @@
package middleware
import "net/http"
func JsonContentTypeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
+5
View File
@@ -0,0 +1,5 @@
package apperr
const LogsErrorMessage = "Access to the requested resource is forbidden due to incorrect password and/or username."
const BodyErrorMessage = "The request cannot be processed due to a mismatch in the format of the body."
const IdUint32ErrorMessage = "The server expects the ID to be in the format of an unsigned 32-bit integer (uint32)."
+9
View File
@@ -0,0 +1,9 @@
package validator
import "github.com/go-playground/validator/v10"
var Validate *validator.Validate
func InitValidator() {
Validate = validator.New()
}
+97
View File
@@ -0,0 +1,97 @@
package api
import (
"RenewCMS/api/handlers/article"
"RenewCMS/api/handlers/auth"
"RenewCMS/api/handlers/frontend"
"RenewCMS/api/middleware"
"encoding/json"
"net/http"
"os"
"strings"
"github.com/MadAppGang/httplog"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/go-chi/jwtauth/v5"
)
func enableLoggingIfDev(r *chi.Mux, routerName string) {
if os.Getenv("ENVIRONMENT") == "development" {
r.Use(httplog.LoggerWithName(routerName))
}
}
func GetHelloWorld(w http.ResponseWriter, _ *http.Request) {
msg, _ := json.Marshal(map[string]string{"message": "Hello World"})
_, _ = w.Write(msg)
}
func NewArticleRouter(container *UseCases) http.Handler {
r := chi.NewRouter()
articleHandler := article.NewHandler(container.CreateArticleUseCase, container.GetArticleUseCase, container.ListArticlesUseCase, container.DeleteArticleUseCase)
r.Get("/{id}", articleHandler.GetArticle)
r.Post("/", articleHandler.PostArticle)
r.Get("/", articleHandler.ListArticles)
r.Delete("/{id}", articleHandler.DeleteArticle)
return r
}
func NewAuthRouter(container *UseCases) http.Handler {
r := chi.NewRouter()
authHandler := auth.NewHandler(container.Token, container.GetUserUseCase, container.ListUsersUseCase, container.CreateUserUseCase)
r.Post("/register", authHandler.Register)
r.Post("/login", authHandler.Login)
r.Post("/logout", authHandler.Logout)
return r
}
func InitBackendRoutes(container *UseCases) *chi.Mux {
r := chi.NewRouter()
enableLoggingIfDev(r, "backend")
r.Use(middleware.JsonContentTypeMiddleware)
r.Get("/", GetHelloWorld)
r.Group(func(r chi.Router) {
r.Use(jwtauth.Verifier(container.Token))
r.Use(jwtauth.Authenticator(container.Token))
r.Mount("/article", NewArticleRouter(container))
})
r.Mount("/auth", NewAuthRouter(container))
return r
}
func InitFrontendRoutes() *chi.Mux {
r := chi.NewRouter()
enableLoggingIfDev(r, "frontend")
r.Mount("/", frontend.NewFrontendRouter())
return r
}
func InitRoutes(container *UseCases) *chi.Mux {
backend := InitBackendRoutes(container)
frontend := InitFrontendRoutes()
apiRouter := chi.NewRouter()
apiRouter.Use(cors.Handler(cors.Options{
AllowedOrigins: strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ";"),
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300,
}))
apiRouter.Mount("/v1", backend)
apiRouter.Mount("/", frontend)
return apiRouter
}
-99
View File
@@ -1,99 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/Floriansylvain/GohCMS/internal/api"
"github.com/Floriansylvain/GohCMS/internal/articles"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
var (
ginMode string
apiPort string
apiBaseUrl string
frontAddress string
)
func initEnvVariables() {
if godotenv.Load() != nil {
panic("Error loading .env file.")
}
ginMode = os.Getenv("APP_GIN_MODE")
apiPort = os.Getenv("APP_API_PORT")
apiBaseUrl = os.Getenv("APP_BASE_API_PATH")
frontAddress = os.Getenv("APP_FRONT_ADDRESS")
}
func initJWT() {
errInit := api.AuthMiddleware.MiddlewareInit()
if errInit != nil {
fmt.Printf(errInit.Error())
}
}
func initBasicRoutes(r *gin.RouterGroup) {
r.POST("/login/", api.AuthMiddleware.LoginHandler)
r.POST("/logout/", api.AuthMiddleware.LogoutHandler)
r.GET("/ping/", api.Ping)
}
func corsMiddleware(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", frontAddress)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
func jwtProxyMiddleware(c *gin.Context) {
jwtToken, _ := c.Cookie("jwt")
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %v", jwtToken))
c.Next()
}
func initArticlesRoutes(r *gin.RouterGroup) {
articlesRouter := r.Group("/articles")
articlesRouter.Use(corsMiddleware, api.AuthMiddleware.MiddlewareFunc())
articlesRouter.GET("/", articles.Get)
articlesRouter.GET("/:id", articles.GetUnique)
articlesRouter.POST("/:id", articles.Add)
articlesRouter.PUT("/:id", articles.Edit)
articlesRouter.DELETE("/:id", articles.Delete)
}
func initGin() {
r := gin.Default()
r.Use(jwtProxyMiddleware, corsMiddleware)
var router *gin.RouterGroup
if apiBaseUrl != "" {
router = r.Group(apiBaseUrl)
} else {
router = &r.RouterGroup
}
if ginMode == "release" {
gin.SetMode(ginMode)
api.AuthMiddleware.SecureCookie = true
}
initBasicRoutes(router)
initArticlesRoutes(router)
r.Run(":" + apiPort)
}
func main() {
initEnvVariables()
initGin()
initJWT()
}
+69
View File
@@ -0,0 +1,69 @@
package main
import (
"RenewCMS/api"
validator "RenewCMS/api/pkg/validator"
"fmt"
"net/http"
"os"
"github.com/go-chi/chi/v5"
"github.com/joho/godotenv"
)
var possibleEnvFileLocations = []string{".env", "../.env"}
var envVarsToLoad = []string{
"HOST",
"PORT",
"JWT_SECRET",
"ENVIRONMENT",
"CORS_ALLOWED_ORIGINS",
"DB_FILE",
"UPLOAD_DIR",
"SMTP_EMAIL",
"SMTP_PASSWORD",
"SMTP_HOST",
"SMTP_PORT",
}
func initEnvVariables() {
var err error
for _, envLocation := range possibleEnvFileLocations {
err = godotenv.Load(envLocation)
if err == nil {
break
}
}
if err != nil {
fmt.Println("WARNING: Could not load any .env file")
}
for _, envVar := range envVarsToLoad {
if _, ok := os.LookupEnv(envVar); !ok {
panic(fmt.Sprintf("Environment variable %s is not set", envVar))
}
}
}
func initServer() *chi.Mux {
initEnvVariables()
container := api.InitContainer()
validator.InitValidator()
return api.InitRoutes(container)
}
func startServer(router *chi.Mux) error {
fmt.Println("Server starting on http://" + os.Getenv("HOST"))
err := http.ListenAndServe(":"+os.Getenv("PORT"), router)
return err
}
func main() {
router := initServer()
err := startServer(router)
if err != nil {
panic(err)
}
}
+12
View File
@@ -0,0 +1,12 @@
services:
api:
build: .
ports:
- "${PORT}:8080"
volumes:
- ${DOCKER_DB_FOLDER}:/app/db/
environment:
- PORT=8080
- DB_FILE=/app/db/RenewCMS.db
env_file:
- ./.env
-38
View File
@@ -1,38 +0,0 @@
services:
backend:
build:
context: ./
dockerfile: ./Dockerfile.backend
image: gohcms-backend
env_file:
- .env
ports:
- "${APP_API_PORT}:${APP_API_PORT}"
networks:
- back-net
frontend:
build:
context: ./
dockerfile: ./Dockerfile.frontend
image: gohcms-frontend
env_file:
- .env
ports:
- "${APP_FRONT_PORT}:80"
networks:
- front-net
db:
image: mongo:6.0.2
# ports:
# - "27017:27017"
environment:
- MONGO_INITDB_DATABASE=gohcms
volumes:
- ./mongodbdata:/data/db
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
networks:
- back-net
networks:
front-net:
back-net:
+44 -37
View File
@@ -1,47 +1,54 @@
module github.com/Floriansylvain/GohCMS
module RenewCMS
go 1.19
go 1.25.4
require (
github.com/appleboy/gin-jwt/v2 v2.9.1
github.com/gin-gonic/gin v1.9.0
github.com/MadAppGang/httplog v1.3.0
github.com/glebarez/sqlite v1.11.0
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.3.3
github.com/go-playground/validator/v10 v10.28.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
go.mongodb.org/mongo-driver v1.11.2
golang.org/x/crypto v0.44.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/gorm v1.31.1
)
require (
github.com/bytedance/sonic v1.8.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.10 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
golang.org/x/arch v0.2.0 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.2.1 // indirect
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
modernc.org/libc v1.67.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.40.1 // indirect
)
require (
github.com/go-chi/chi/v5 v5.2.3
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.38.0 // indirect
)
+114 -202
View File
@@ -1,221 +1,133 @@
github.com/appleboy/gin-jwt/v2 v2.9.1 h1:l29et8iLW6omcHltsOP6LLk4s3v4g2FbFs0koxGWVZs=
github.com/appleboy/gin-jwt/v2 v2.9.1/go.mod h1:jwcPZJ92uoC9nOUTOKWoN/f6JZOgMSKlFSHw5/FrRUk=
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/MadAppGang/httplog v1.3.0 h1:1XU54TO8kiqTeO+7oZLKAM3RP/cJ7SadzslRcKspVHo=
github.com/MadAppGang/httplog v1.3.0/go.mod h1:gpYEdkjh/Cda6YxtDy4AB7KY+fR7mb3SqBZw74A5hJ4=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
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/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/jwtauth/v5 v5.3.3 h1:50Uzmacu35/ZP9ER2Ht6SazwPsnLQ9LRJy6zTZJpHEo=
github.com/go-chi/jwtauth/v5 v5.3.3/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
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/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
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.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/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.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.13 h1:NFn1Wr8cfnenSJSA46lLq4wHCcBzKTSjnBIexDMMOV0=
github.com/klauspost/compress v1.15.13/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
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/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/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/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0=
github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8=
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
go.mongodb.org/mongo-driver v1.11.2 h1:+1v2rDQUWNcGW7/7E0Jvdz51V38XXxJfhzbV17aNHCw=
go.mongodb.org/mongo-driver v1.11.2/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.2.0 h1:W1sUEHXiJTfjaFJ5SLo0N6lZn+0eO5gWD1MFeTGqQEY=
golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/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.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
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/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
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=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.0 h1:QzL4IrKab2OFmxA3/vRYl0tLXrIamwrhD6CKD4WBVjQ=
modernc.org/libc v1.67.0/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-58
View File
@@ -1,58 +0,0 @@
package api
import (
"errors"
"net/http"
"os"
"time"
"github.com/Floriansylvain/GohCMS/internal/database"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
)
type User struct {
Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
}
var UsersLocation = database.Location{Database: "gohcms", Collection: "users"}
var AuthMiddleware, _ = jwt.New(&jwt.GinJWTMiddleware{
Realm: "GohCMS",
Key: []byte(os.Getenv("APP_JWT_SECRET")),
SendCookie: true,
CookieHTTPOnly: true,
CookieSameSite: http.SameSiteStrictMode,
Timeout: time.Hour,
MaxRefresh: time.Hour,
LoginResponse: JWTLoginResponse,
Authenticator: JWTAuthenticator,
})
func JWTLoginResponse(c *gin.Context, code int, message string, expire time.Time) {
if code == http.StatusOK {
c.JSON(code, gin.H{"code": code, "message": "Successfully logged in!", "expire": expire.Format(time.RFC3339)})
} else {
c.JSON(code, gin.H{"code": code, "message": "Something wrong has happened."})
}
}
func JWTAuthenticator(c *gin.Context) (interface{}, error) {
var user = User{}
err := c.BindJSON(&user)
if err != nil {
return nil, errors.New("wrong credentials json format.")
}
_, err = database.GetUniqueDocument(UsersLocation, bson.D{
{Key: "email", Value: user.Email},
{Key: "password", Value: user.Password},
})
if err != nil {
return nil, errors.New("wrong email or password.")
}
return gin.H{"email": user.Email}, nil
}
-19
View File
@@ -1,19 +0,0 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
)
func SendBadRequest(c *gin.Context, message string) {
c.JSON(http.StatusBadRequest, gin.H{"message": message, "code": 400})
}
func SendOk(c *gin.Context, message string) {
c.JSON(http.StatusOK, gin.H{"message": message, "code": 200})
}
func SendForbidden(c *gin.Context, message string) {
c.JSON(http.StatusForbidden, gin.H{"message": message, "code": 403})
}
-11
View File
@@ -1,11 +0,0 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
)
func Ping(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
}
-39
View File
@@ -1,39 +0,0 @@
package articles
import (
"fmt"
"github.com/Floriansylvain/GohCMS/internal/api"
"github.com/Floriansylvain/GohCMS/internal/database"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
)
func Add(c *gin.Context) {
var article Article
article.TitleID = c.Params.ByName("id")
if c.BindJSON(&article) != nil {
api.SendBadRequest(c, "Could not correctly parse the article.")
return
}
document, err := bson.Marshal(article)
if err != nil {
api.SendBadRequest(c, "Could not correctly marshal the article.")
return
}
documents, _ := database.GetDocuments(articlesLocation, map[string]any{})
if IsArticleIdAlreadyUsed(article.TitleID, documents) {
api.SendBadRequest(c, fmt.Sprintf("Article ID '%v' already used.", article.TitleID))
return
}
err = database.PushDocument(articlesLocation, document)
if err != nil {
api.SendBadRequest(c, fmt.Sprintf(`Could not insert document(s) into DB: %v`, err.Error()))
return
}
api.SendOk(c, "Article successfully added!")
}
-41
View File
@@ -1,41 +0,0 @@
package articles
import (
"github.com/Floriansylvain/GohCMS/internal/database"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
)
type Article struct {
TitleID string `json:"titleID" bson:"titleID"`
Title string `json:"title" bson:"title"`
Date int64 `json:"date" bson:"date"`
Content gin.H `json:"content" bson:"content"`
Tags []string `json:"tags" bson:"tags"`
Online bool `json:"online" bson:"online"`
}
var articlesLocation = database.Location{Database: "gohcms", Collection: "articles"}
func ParseArticlesFromBytesToArray(documents [][]byte) []Article {
var articles = []Article{}
for i := 0; i < len(documents); i++ {
var newArticle Article
bson.Unmarshal(documents[i], &newArticle)
articles = append(articles, newArticle)
}
return articles
}
func IsArticleIdAlreadyUsed(id string, documents [][]byte) bool {
for i := 0; i < len(documents); i++ {
var newArticle Article
bson.Unmarshal(documents[i], &newArticle)
if newArticle.TitleID == id {
return true
}
}
return false
}
-46
View File
@@ -1,46 +0,0 @@
package articles
import (
"log"
"testing"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
)
var Article1, _ = bson.Marshal(Article{
TitleID: "test_article_1",
Date: 1671482492,
Content: gin.H{},
Tags: []string{"blog"},
Online: false,
})
var Article2, _ = bson.Marshal(Article{
TitleID: "test_article_2",
Date: 1671899112,
Content: gin.H{},
Tags: []string{"blog"},
Online: false,
})
var BSONConvertedArticles = [][]byte{Article1, Article2}
func TestGetAllArticlesBusiness(t *testing.T) {
documents := ParseArticlesFromBytesToArray(BSONConvertedArticles)
article := documents[0]
if article.TitleID != "test_article_1" {
log.Fatalf(`Excepted "test_article_1" as IdName, found "%v"`, article.TitleID)
} else if article.Date != 1671482492 {
log.Fatalf(`Excepted 1671482492 as Date, found "%v"`, article.Date)
}
}
func TestIsArticleIdAlreadyUsed(t *testing.T) {
if IsArticleIdAlreadyUsed("test_article_1", BSONConvertedArticles) == false {
log.Fatalf(`Excepted true, found false`)
} else if IsArticleIdAlreadyUsed("test_article_3", BSONConvertedArticles) == true {
log.Fatalf(`Excepted false, found true`)
}
}
-25
View File
@@ -1,25 +0,0 @@
package articles
import (
"fmt"
"github.com/Floriansylvain/GohCMS/internal/api"
"github.com/Floriansylvain/GohCMS/internal/database"
"github.com/gin-gonic/gin"
)
func Delete(c *gin.Context) {
id := c.Params.ByName("id")
deleteCount, err := database.DeleteDocument(articlesLocation, map[string]any{"titleID": id})
if err != nil {
api.SendBadRequest(c, fmt.Sprintf(`Could not delete document(s) from DB: %v`, err.Error()))
return
}
if deleteCount != 0 {
api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully deleted!", deleteCount))
} else {
api.SendOk(c, "No articles were deleted.")
}
}
-29
View File
@@ -1,29 +0,0 @@
package articles
import (
"fmt"
"github.com/Floriansylvain/GohCMS/internal/api"
"github.com/Floriansylvain/GohCMS/internal/database"
"github.com/gin-gonic/gin"
)
func Edit(c *gin.Context) {
id := c.Params.ByName("id")
var articleUpdate database.DocumentUpdate
articleUpdate.Filter = map[string]any{"titleID": id}
c.BindJSON(&articleUpdate.Update)
editCount, err := database.EditDocument(articlesLocation, articleUpdate)
if err != nil {
api.SendBadRequest(c, fmt.Sprintf(`Could not edit document(s) from DB: %v`, err.Error()))
return
}
if editCount != 0 {
api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully edited!", editCount))
} else {
api.SendOk(c, "No articles were edited.")
}
}
-102
View File
@@ -1,102 +0,0 @@
package articles
import (
"fmt"
"math"
"net/http"
"os"
"strconv"
"github.com/Floriansylvain/GohCMS/internal/api"
"github.com/Floriansylvain/GohCMS/internal/database"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
)
func getApiFullUrl() string {
return fmt.Sprintf("%v%v", os.Getenv("APP_API_ADDRESS"), os.Getenv("APP_BASE_API_PATH"))
}
func getArticleSkipTakeFullUrl(skip uint64, take uint64) string {
return fmt.Sprintf("%varticles?skip=%v&take=%v", getApiFullUrl(), skip+take, take)
}
func getBuiltGetResponse(articles []Article, skip uint64, take uint64) map[string]any {
articlesCap := uint64(len(articles))
var normTake uint64
if skip+take > articlesCap {
normTake = articlesCap
} else {
normTake = skip + take
}
slicedArticles := articles[skip:normTake]
total := len(slicedArticles)
return map[string]any{
"content": slicedArticles,
"total": total,
"pagination": map[string]any{
"skip": skip,
"take": take,
"links": map[string]any{
"next": getArticleSkipTakeFullUrl(skip+take, take),
"previous": getArticleSkipTakeFullUrl(skip-take, take),
},
},
"last_page": math.Ceil(float64(articlesCap) / float64(take)),
}
}
func parseUintQueryParam(c *gin.Context, param string, defaultValue uint64) (uint64, error) {
if c.Query(param) == "" {
return defaultValue, nil
}
value, err := strconv.ParseUint(c.Query(param), 10, 0)
return value, err
}
func getArticleIdFilter(titleID string) map[string]any {
filter := map[string]any{}
if titleID != "" {
filter["titleID"] = titleID
}
return filter
}
func Get(c *gin.Context) {
skip, skipErr := parseUintQueryParam(c, "skip", 0)
take, takeErr := parseUintQueryParam(c, "take", 10)
if skipErr != nil || takeErr != nil {
api.SendBadRequest(c, "Take and Skip query parameters must be positive numbers.")
return
}
documents, _ := database.GetDocuments(articlesLocation, map[string]any{})
articlesArray := ParseArticlesFromBytesToArray(documents)
articlesArrayLength := uint64(len(articlesArray))
if skip >= articlesArrayLength || take == 0 {
c.JSON(http.StatusOK, map[string]any{
"content": []string{},
"total": "0",
})
return
}
c.JSON(http.StatusOK, getBuiltGetResponse(articlesArray, skip, take))
}
func GetUnique(c *gin.Context) {
titleID := c.Params.ByName("id")
article, err := database.GetUniqueDocument(articlesLocation, getArticleIdFilter(titleID))
if err != nil {
api.SendBadRequest(c, fmt.Sprintf("The ID '%v' doesn't match any article.", titleID))
return
}
var parsedArticle Article
bson.Unmarshal(article, &parsedArticle)
c.JSON(http.StatusOK, parsedArticle)
}
-23
View File
@@ -1,23 +0,0 @@
package database
import (
"context"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Location struct {
Database string
Collection string
}
func GetNewClient() *mongo.Client {
client, err := mongo.Connect(
context.TODO(),
options.Client().ApplyURI("mongodb://db:27017/"))
if err != nil {
panic(err)
}
return client
}
-19
View File
@@ -1,19 +0,0 @@
package database
import (
"context"
"errors"
)
func DeleteDocument(location Location, filter interface{}) (int64, error) {
client := GetNewClient()
collection := client.Database(location.Database).Collection(location.Collection)
defer client.Disconnect(context.TODO())
result, err := collection.DeleteOne(context.TODO(), filter)
if err != nil {
return 0, errors.New("something is wrong with filter to delete the document.")
}
return result.DeletedCount, nil
}
-29
View File
@@ -1,29 +0,0 @@
package database
import (
"context"
"errors"
"github.com/gin-gonic/gin"
)
type DocumentUpdate struct {
Filter gin.H `json:"filter"`
Update gin.H `json:"update"`
}
func EditDocument(location Location, jsons DocumentUpdate) (int64, error) {
client := GetNewClient()
collection := client.Database(location.Database).Collection(location.Collection)
defer client.Disconnect(context.TODO())
result, err := collection.UpdateOne(context.TODO(), jsons.Filter, gin.H{"$set": jsons.Update})
if err != nil {
return 0, err
}
if result.MatchedCount == 0 {
return 0, errors.New("cannot find document matching filter.")
}
return result.ModifiedCount, nil
}
-35
View File
@@ -1,35 +0,0 @@
package database
import (
"context"
"errors"
)
func GetDocuments(location Location, filter interface{}) ([][]byte, error) {
client := GetNewClient()
collection := client.Database(location.Database).Collection(location.Collection)
defer client.Disconnect(context.TODO())
var results [][]byte
cursor, err := collection.Find(context.TODO(), filter)
if err != nil {
return results, errors.New("something is wrong with filter to find the document.")
}
for cursor.TryNext(context.TODO()) {
results = append(results, cursor.Current)
}
return results, nil
}
func GetUniqueDocument(location Location, filter interface{}) ([]byte, error) {
client := GetNewClient()
collection := client.Database(location.Database).Collection(location.Collection)
defer client.Disconnect(context.TODO())
singleResult := collection.FindOne(context.TODO(), filter)
if singleResult.Err() != nil {
return nil, errors.New("no document was found, check the filter or the location.")
}
return singleResult.DecodeBytes()
}
-15
View File
@@ -1,15 +0,0 @@
package database
import "context"
func PushDocument(location Location, document interface{}) error {
client := GetNewClient()
collection := client.Database(location.Database).Collection(location.Collection)
defer client.Disconnect(context.TODO())
_, err := collection.InsertOne(context.TODO(), document)
if err != nil {
return err
}
return nil
}
+23
View File
@@ -0,0 +1,23 @@
package article
import (
domain "RenewCMS/internal/domain/image"
"errors"
"time"
)
var (
ErrInvalidTitle = errors.New("article's title length should be between 3 and 200 caracters")
ErrArticleNotFound = errors.New("article not found")
ErrArticleOffline = errors.New("article is offline")
)
type Article struct {
ID uint32 `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Images []*domain.Image `json:"images"`
IsOnline bool `json:"is_online"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -0,0 +1,17 @@
package article
type Filters struct {
IsOnline *bool
}
type Repository interface {
FindByFilters(filters Filters) ([]Article, error)
Get(id uint32) (Article, error)
GetByName(name string) (Article, error)
GetAll() []Article
Create(post Article) (Article, error)
UpdateBody(id uint32, body string) (Article, error)
UpdateIsOnline(id uint32, isOnline bool) (Article, error)
Delete(id uint32) error
AddImage(postId uint32, imageId uint32) error
}
+21
View File
@@ -0,0 +1,21 @@
package image
import (
"io"
"time"
)
type ImageInput struct {
Content io.Reader
Filename string
Size int64
ContentType string
}
type Image struct {
ID uint32 `json:"id"`
Path string `json:"path"`
ArticleID uint32 `json:"article_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -0,0 +1,6 @@
package image
type Repository interface {
Create(input ImageInput) (Image, error)
Delete(id uint32) error
}
@@ -0,0 +1,5 @@
package mail
type Repository interface {
Send(receiverAddress string, templateName string, data any) error
}
+18
View File
@@ -0,0 +1,18 @@
package user
import (
"time"
)
type User struct {
ID uint32 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
PasswordResetCode string `json:"password_reset_code"`
Email string `json:"email"`
IsVerified bool `json:"is_verified"`
VerificationCode string `json:"verification_code"`
VerificationExpiration time.Time `json:"verification_expiration"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -0,0 +1,13 @@
package user
type Repository interface {
Get(id uint32) (User, error)
GetByUsername(username string) (User, error)
GetByEmail(email string) (User, error)
GetAll() []User
Create(user User) (User, error)
Delete(id uint32) error
UpdateVerificationStatus(userId uint32, isVerified bool) (User, error)
UpdatePassword(userId uint32, password string) (User, error)
UpdatePasswordResetCode(userId uint32, code string) (User, error)
}
+6
View File
@@ -0,0 +1,6 @@
package assets
import "embed"
//go:embed all:dist
var DistEmbed embed.FS
@@ -0,0 +1,64 @@
package mailer
import (
domain "RenewCMS/internal/domain/mail"
"bytes"
"embed"
"html/template"
"log"
"os"
"strconv"
"gopkg.in/gomail.v2"
)
type MailRepository struct{}
//go:embed templates/*
var mailTemplateFiles embed.FS
func NewMailRepository() *MailRepository {
return &MailRepository{}
}
func (m MailRepository) Send(receiverAddress string, templateName string, data any) error {
from := os.Getenv("SMTP_EMAIL")
password := os.Getenv("SMTP_PASSWORD")
smtpHost := os.Getenv("SMTP_HOST")
smtpPort, _ := strconv.Atoi(os.Getenv("SMTP_PORT"))
d := gomail.NewDialer(smtpHost, smtpPort, from, password)
tmpl, err := template.ParseFS(mailTemplateFiles, "mail/templates/"+templateName+".html")
if err != nil {
log.Println("error when template.ParseFS:", err)
return err
}
var body bytes.Buffer
err = tmpl.Execute(&body, data)
if err != nil {
log.Println("error when tmpl.Execute:", err)
return err
}
msg := gomail.NewMessage()
msg.SetHeaders(map[string][]string{
"From": {"RenewCMS <" + from + ">"},
"To": {receiverAddress},
"MIME-version": {"1.0"},
"Content-Type": {"text/html"},
"charset": {"UTF-8"},
"Subject": {"RenewCMS | Action required"},
})
msg.SetBody("text/html", body.String())
if err := d.DialAndSend(msg); err != nil {
log.Println("error when sending email:", err)
return err
}
return nil
}
var _ domain.Repository = &MailRepository{}
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<body>
<h1>
<img src="{{.Host}}/static/renewcms-favicon-64.png" alt="G" style="width: 2.5rem"/>
RenewCMS
</h1>
<main>
<p>You or someone tried to create a RenewCMS admin account with this e-mail address. If you are not responsible for
this, please do not validate the account creation.</p>
<p><b>If you want to validate the account creation, please <a
href="{{.Host}}/register/validate?c={{.VerificationCode}}" target="_blank">click HERE</a>.</b>
</p>
<p>If you can't click the link:</p>
<p>{{.Host}}/register/validate?c={{.VerificationCode}}</p>
</main>
</body>
</html>
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<body>
<h1>
<img src="{{.Host}}/static/renewcms-favicon-64.png" alt="G" style="width: 2.5rem"/>
RenewCMS
</h1>
<main>
<p>You or someone tried to create reset your RenewCMS account with this e-mail address. If you are not responsible for
this, please do not validate the password reset.</p>
<p><b>If you want to reset your password, please <a
href="{{.Host}}/register/reset/validate?c={{.VerificationCode}}&email={{.Email}}" target="_blank">click HERE</a>.</b>
</p>
<p>If you can't click the link:</p>
<p>{{.Host}}/register/reset/validate?c={{.VerificationCode}}&email={{.Email}}</p>
</main>
</body>
</html>
@@ -0,0 +1,42 @@
package mappers
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/infrastructure/persistence/models"
)
func ArticleToDomain(m models.Article) article.Article {
domainImages := make([]*image.Image, len(m.Images))
for i, img := range m.Images {
domainImg := ImageToDomain(*img)
domainImages[i] = &domainImg
}
return article.Article{
ID: m.ID,
Title: m.Title,
Body: m.Body,
Images: domainImages,
IsOnline: m.IsOnline,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
}
func ArticleToModel(a article.Article) models.Article {
modelImages := make([]*models.Image, len(a.Images))
for i, img := range a.Images {
modelImage := ImageToModel(*img)
modelImages[i] = &modelImage
}
return models.Article{
Title: a.Title,
Body: a.Body,
Images: modelImages,
IsOnline: a.IsOnline,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
}
}
@@ -0,0 +1,25 @@
package mappers
import (
"RenewCMS/internal/domain/image"
"RenewCMS/internal/infrastructure/persistence/models"
)
func ImageToDomain(imageModel models.Image) image.Image {
return image.Image{
ID: imageModel.ID,
Path: imageModel.Path,
ArticleID: imageModel.ArticleID,
CreatedAt: imageModel.CreatedAt,
UpdatedAt: imageModel.UpdatedAt,
}
}
func ImageToModel(imageDomain image.Image) models.Image {
return models.Image{
Path: imageDomain.Path,
ArticleID: imageDomain.ArticleID,
CreatedAt: imageDomain.CreatedAt,
UpdatedAt: imageDomain.UpdatedAt,
}
}
@@ -0,0 +1,35 @@
package mappers
import (
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/persistence/models"
)
func UserToDomain(m models.User) user.User {
return user.User{
ID: m.ID,
Username: m.Username,
Password: m.Password,
PasswordResetCode: m.PasswordResetCode,
Email: m.Email,
IsVerified: m.IsVerified,
VerificationCode: m.VerificationCode,
VerificationExpiration: m.VerificationExpiration,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
}
func UserToModel(a user.User) models.User {
return models.User{
Username: a.Username,
Password: a.Password,
PasswordResetCode: a.PasswordResetCode,
Email: a.Email,
IsVerified: a.IsVerified,
VerificationCode: a.VerificationCode,
VerificationExpiration: a.VerificationExpiration,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
}
}
@@ -0,0 +1,18 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Article struct {
gorm.Model
ID uint32 `gorm:"primary_key;auto_increment;not_null"`
Title string
Body string
Images []*Image `gorm:"many2many:article_images;"`
IsOnline bool `gorm:"not_null;default:false"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
@@ -0,0 +1,16 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Image struct {
gorm.Model
ID uint32 `gorm:"primary_key;auto_increment;not_null"`
Path string
ArticleID uint32
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
@@ -0,0 +1,21 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
gorm.Model
ID uint32 `gorm:"primaryKey;autoIncrement"`
Username string `gorm:"unique;not null"`
Password string `gorm:"not null"`
PasswordResetCode string `gorm:"unique"`
Email string `gorm:"unique;not null"`
IsVerified bool `gorm:"default=false;not null"`
VerificationCode string `gorm:"unique"`
VerificationExpiration time.Time
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
@@ -0,0 +1,147 @@
package persistence
import (
domain "RenewCMS/internal/domain/article"
"RenewCMS/internal/infrastructure/persistence/mappers"
entity "RenewCMS/internal/infrastructure/persistence/models"
"gorm.io/gorm"
)
type ArticleRepository struct {
db *gorm.DB
}
func NewArticleRepository(db *gorm.DB) *ArticleRepository {
return &ArticleRepository{db}
}
func (a *ArticleRepository) FindByFilters(filters domain.Filters) ([]domain.Article, error) {
query := a.db.Model(&entity.Article{})
if filters.IsOnline != nil {
query = query.Where("is_online = ?", *filters.IsOnline)
}
var entityArticles []entity.Article
err := query.Find(&entityArticles).Error
domainArticles := make([]domain.Article, 0, len(entityArticles))
for _, entityArticle := range entityArticles {
domainArticles = append(domainArticles, mappers.ArticleToDomain(entityArticle))
}
return domainArticles, err
}
func (a *ArticleRepository) Get(id uint32) (domain.Article, error) {
var article entity.Article
err := a.db.Model(&entity.Article{}).Preload("Images").First(&article, id).Error
if err != nil {
return domain.Article{}, err
}
return mappers.ArticleToDomain(article), nil
}
func (a *ArticleRepository) GetByName(name string) (domain.Article, error) {
var article entity.Article
err := a.db.Model(&entity.Article{}).Where("title = ?", name).First(&article).Error
if err != nil {
return domain.Article{}, err
}
return mappers.ArticleToDomain(article), nil
}
func (a *ArticleRepository) Create(article domain.Article) (domain.Article, error) {
creationResult := a.db.Create(&entity.Article{
Title: article.Title,
Body: article.Body,
IsOnline: article.IsOnline,
})
if creationResult.Error != nil {
return domain.Article{}, creationResult.Error
}
var createdArticle entity.Article
creationResult.Scan(&createdArticle)
return mappers.ArticleToDomain(createdArticle), nil
}
func (a *ArticleRepository) GetAll() []domain.Article {
var articles []entity.Article
err := a.db.Model(&entity.Article{}).Find(&articles).Error
if err != nil {
return []domain.Article{}
}
var domainArticles = make([]domain.Article, 0)
for _, article := range articles {
domainArticles = append(domainArticles, mappers.ArticleToDomain(article))
}
return domainArticles
}
func (a *ArticleRepository) UpdateBody(id uint32, body string) (domain.Article, error) {
var localArticle entity.Article
err := a.db.Model(&entity.Article{}).First(&localArticle, id).Error
if err != nil {
return domain.Article{}, err
}
localArticle.Body = body
err = a.db.Save(&localArticle).Error
if err != nil {
return domain.Article{}, err
}
newArticle := mappers.ArticleToDomain(localArticle)
return newArticle, nil
}
func (a *ArticleRepository) UpdateIsOnline(id uint32, isOnline bool) (domain.Article, error) {
var localArticle entity.Article
err := a.db.Model(&entity.Article{}).First(&localArticle, id).Error
if err != nil {
return domain.Article{}, err
}
localArticle.IsOnline = isOnline
err = a.db.Save(&localArticle).Error
if err != nil {
return domain.Article{}, err
}
return mappers.ArticleToDomain(localArticle), nil
}
func (a *ArticleRepository) Delete(id uint32) error {
return a.db.Delete(&entity.Article{}, id).Error
}
func (a *ArticleRepository) AddImage(articleId uint32, imageId uint32) error {
var localArticle entity.Article
err := a.db.Model(&entity.Article{}).First(&localArticle, articleId).Error
if err != nil {
return err
}
var localImage entity.Image
err = a.db.Model(&entity.Image{}).First(&localImage, imageId).Error
if err != nil {
return err
}
err = a.db.Model(&localArticle).Association("Images").Append(&localImage)
if err != nil {
return err
}
return nil
}
var _ domain.Repository = &ArticleRepository{}
@@ -0,0 +1,80 @@
package persistence
import (
domain "RenewCMS/internal/domain/image"
"RenewCMS/internal/infrastructure/persistence/mappers"
entity "RenewCMS/internal/infrastructure/persistence/models"
"errors"
"io"
"os"
"path/filepath"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ImageRepository struct {
db *gorm.DB
}
func NewImageRepository(db *gorm.DB) *ImageRepository {
return &ImageRepository{db}
}
var contentTypeExtensions = map[string]string{
"image/png": ".png",
"image/jpeg": ".jpeg",
"image/webp": ".webp",
"image/svg+xml": ".svg",
}
func (i ImageRepository) Create(input domain.ImageInput) (domain.Image, error) {
uploadDir := os.Getenv("UPLOAD_DIR")
fileBytes, err := io.ReadAll(input.Content)
if err != nil {
return domain.Image{}, err
}
err = os.MkdirAll(uploadDir, 0755)
if err != nil {
return domain.Image{}, err
}
extension := contentTypeExtensions[input.ContentType]
if extension == "" {
return domain.Image{}, errors.New("the file must be a PNG, JPEG, WEBP, or SVG image")
}
newName := uuid.NewString() + extension
finalPath := filepath.Join(uploadDir, newName)
err = os.WriteFile(finalPath, fileBytes, 0666)
if err != nil {
return domain.Image{}, err
}
newImage := i.db.Create(&domain.Image{Path: "/static/uploadedImages/" + newName})
var createdImage entity.Image
newImage.Scan(&createdImage)
return mappers.ImageToDomain(createdImage), nil
}
func (i ImageRepository) Delete(id uint32) error {
uploadDir := os.Getenv("UPLOAD_DIR")
var entityImage entity.Image
err := i.db.Model(&entity.Image{}).First(&entityImage, id).Error
if err != nil {
return err
}
fileName := filepath.Base(entityImage.Path)
err = os.Remove(filepath.Join(uploadDir, fileName))
if err != nil {
return err
}
return i.db.Delete(&entity.Image{}, id).Error
}
var _ domain.Repository = &ImageRepository{}
@@ -0,0 +1,138 @@
package persistence
import (
domainUser "RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/persistence/mappers"
entity "RenewCMS/internal/infrastructure/persistence/models"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db}
}
func (u *UserRepository) Get(id uint32) (domainUser.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, id).Error
if err != nil {
return domainUser.User{}, err
}
return mappers.UserToDomain(localUser), nil
}
func (u *UserRepository) Create(user domainUser.User) (domainUser.User, error) {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
hashedVerificationCode, _ := bcrypt.GenerateFromPassword([]byte(user.VerificationCode), 12)
creationResult := u.db.Create(&entity.User{
Username: user.Username,
Password: string(hashedPassword),
Email: user.Email,
VerificationCode: string(hashedVerificationCode),
VerificationExpiration: user.VerificationExpiration,
})
if creationResult.Error != nil {
return domainUser.User{}, creationResult.Error
}
var createdUser entity.User
creationResult.Scan(&createdUser)
return mappers.UserToDomain(createdUser),
nil
}
func (u *UserRepository) Delete(id uint32) error {
return u.db.Delete(&domainUser.User{}, id).Error
}
func (u *UserRepository) GetAll() []domainUser.User {
var users []entity.User
u.db.Model(&entity.User{}).Find(&users)
var domainUsers []domainUser.User
for _, localUser := range users {
domainUsers = append(domainUsers, mappers.UserToDomain(localUser))
}
return domainUsers
}
func (u *UserRepository) GetByUsername(username string) (domainUser.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).Where("username = ?", username).First(&localUser).Error
if err != nil {
return domainUser.User{}, err
}
return mappers.UserToDomain(localUser), nil
}
func (u *UserRepository) GetByEmail(email string) (domainUser.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).Where("email = ?", email).First(&localUser).Error
if err != nil {
return domainUser.User{}, err
}
return mappers.UserToDomain(localUser), nil
}
func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool) (domainUser.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
if err != nil {
return domainUser.User{}, err
}
localUser.IsVerified = isVerified
err = u.db.Save(&localUser).Error
if err != nil {
return domainUser.User{}, err
}
return mappers.UserToDomain(localUser), nil
}
func (u *UserRepository) UpdatePassword(userId uint32, password string) (domainUser.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
if err != nil {
return domainUser.User{}, err
}
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
localUser.Password = string(hashedPassword)
err = u.db.Save(&localUser).Error
if err != nil {
return domainUser.User{}, err
}
return mappers.UserToDomain(localUser), nil
}
func (u *UserRepository) UpdatePasswordResetCode(userId uint32, code string) (domainUser.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
if err != nil {
return domainUser.User{}, err
}
hashedCode, _ := bcrypt.GenerateFromPassword([]byte(code), 12)
localUser.PasswordResetCode = string(hashedCode)
err = u.db.Save(&localUser).Error
if err != nil {
return domainUser.User{}, err
}
return mappers.UserToDomain(localUser), nil
}
var _ domainUser.Repository = &UserRepository{}
@@ -0,0 +1,30 @@
package useCases
import (
"RenewCMS/internal/domain/article"
)
type CreateArticleUseCase struct {
articleRepository article.Repository
}
type CreateArticleCommand struct {
Title string
Body string
}
func NewCreateArticleUseCase(articleRepository article.Repository) *CreateArticleUseCase {
return &CreateArticleUseCase{articleRepository}
}
func (g *CreateArticleUseCase) CreateArticle(createArticle CreateArticleCommand) (article.Article, error) {
if len(createArticle.Title) < 3 || len(createArticle.Title) > 200 {
return article.Article{}, article.ErrInvalidTitle
}
return g.articleRepository.Create(article.Article{
Title: createArticle.Title,
Body: createArticle.Body,
IsOnline: false,
})
}
@@ -0,0 +1,17 @@
package useCases
import (
"RenewCMS/internal/domain/image"
)
type CreateImageUseCase struct {
imageRepository image.Repository
}
func NewCreateImageUseCase(imageRepository image.Repository) *CreateImageUseCase {
return &CreateImageUseCase{imageRepository}
}
func (g *CreateImageUseCase) CreateImage(input image.ImageInput) (image.Image, error) {
return g.imageRepository.Create(input)
}
@@ -0,0 +1,42 @@
package useCases
import (
"RenewCMS/internal/domain/user"
"time"
"github.com/google/uuid"
)
type CreateUserUseCase struct {
userRepository user.Repository
}
type CreateUserCommand struct {
Username string
Password string
Email string
}
func NewCreateUserUseCase(userRepository user.Repository) *CreateUserUseCase {
return &CreateUserUseCase{userRepository}
}
func (g *CreateUserUseCase) CreateUser(createUser CreateUserCommand) (user.User, error) {
rawUuid := uuid.NewString()
createdUser, err := g.userRepository.Create(user.User{
Username: createUser.Username,
Password: createUser.Password,
PasswordResetCode: "",
Email: createUser.Email,
IsVerified: false,
VerificationCode: rawUuid,
VerificationExpiration: time.Now().Add(2 * time.Hour),
})
if err != nil {
return user.User{}, err
}
createdUser.VerificationCode = rawUuid
return createdUser, nil
}
@@ -0,0 +1,15 @@
package useCases
import "RenewCMS/internal/domain/article"
type DeleteArticleUseCase struct {
articleRepository article.Repository
}
func NewDeleteArticleUseCase(articleRepository article.Repository) *DeleteArticleUseCase {
return &DeleteArticleUseCase{articleRepository}
}
func (g *DeleteArticleUseCase) DeleteArticle(userId uint32) error {
return g.articleRepository.Delete(userId)
}
@@ -0,0 +1,15 @@
package useCases
import "RenewCMS/internal/domain/image"
type DeleteImageUseCase struct {
imageRepository image.Repository
}
func NewDeleteImageUseCase(imageRepository image.Repository) *DeleteImageUseCase {
return &DeleteImageUseCase{imageRepository}
}
func (g *DeleteImageUseCase) DeleteImage(imageId uint32) error {
return g.imageRepository.Delete(imageId)
}
@@ -0,0 +1,15 @@
package useCases
import "RenewCMS/internal/domain/user"
type DeleteUserUseCase struct {
userRepository user.Repository
}
func NewDeleteUserUseCase(userRepository user.Repository) *DeleteUserUseCase {
return &DeleteUserUseCase{userRepository}
}
func (g *DeleteUserUseCase) DeleteUser(userId uint32) error {
return g.userRepository.Delete(userId)
}
@@ -0,0 +1,32 @@
package useCases
import "RenewCMS/internal/domain/article"
type GetArticleUseCase struct {
articleRepository article.Repository
}
func NewGetArticleUseCase(articleRepository article.Repository) *GetArticleUseCase {
return &GetArticleUseCase{articleRepository}
}
func (u *GetArticleUseCase) GetOnlineArticle(id uint32) (article.Article, error) {
art, err := u.articleRepository.Get(id)
if err != nil {
return article.Article{}, err
}
if !art.IsOnline {
return article.Article{}, article.ErrArticleNotFound
}
return art, nil
}
func (u *GetArticleUseCase) GetArticle(id uint32) (article.Article, error) {
return u.articleRepository.Get(id)
}
func (g *GetArticleUseCase) GetArticleByName(name string) (article.Article, error) {
return g.articleRepository.GetByName(name)
}
@@ -0,0 +1,23 @@
package useCases
import "RenewCMS/internal/domain/user"
type GetUserUseCase struct {
userRepository user.Repository
}
func NewGetUserUseCase(userRepository user.Repository) *GetUserUseCase {
return &GetUserUseCase{userRepository}
}
func (g *GetUserUseCase) GetUser(id uint32) (user.User, error) {
return g.userRepository.Get(id)
}
func (g *GetUserUseCase) GetUserByUsername(username string) (user.User, error) {
return g.userRepository.GetByUsername(username)
}
func (g *GetUserUseCase) GetUserByEmail(email string) (user.User, error) {
return g.userRepository.GetByEmail(email)
}
@@ -0,0 +1,22 @@
package useCases
import "RenewCMS/internal/domain/article"
type ListArticlesUseCase struct {
articleRepository article.Repository
}
func NewListArticlesUseCase(articleRepository article.Repository) *ListArticlesUseCase {
return &ListArticlesUseCase{articleRepository}
}
func (u *ListArticlesUseCase) ListOnlineArticles() ([]article.Article, error) {
online := true
return u.articleRepository.FindByFilters(article.Filters{
IsOnline: &online,
})
}
func (u *ListArticlesUseCase) ListAllArticles() ([]article.Article, error) {
return u.articleRepository.FindByFilters(article.Filters{})
}
@@ -0,0 +1,15 @@
package useCases
import "RenewCMS/internal/domain/user"
type ListUsersUseCase struct {
userRepository user.Repository
}
func NewListUsersUseCase(userRepository user.Repository) *ListUsersUseCase {
return &ListUsersUseCase{userRepository}
}
func (g *ListUsersUseCase) ListUsers() []user.User {
return g.userRepository.GetAll()
}
@@ -0,0 +1,15 @@
package useCases
import "RenewCMS/internal/domain/mail"
type SendMailUseCase struct {
mailRepository mail.Repository
}
func NewSendMailUseCase(mailRepository mail.Repository) *SendMailUseCase {
return &SendMailUseCase{mailRepository}
}
func (g *SendMailUseCase) SendMail(receiverAddress string, templateName string, data any) error {
return g.mailRepository.Send(receiverAddress, templateName, data)
}
@@ -0,0 +1,23 @@
package useCases
import "RenewCMS/internal/domain/article"
type UpdateArticleUseCase struct {
articleRepository article.Repository
}
func NewUpdateArticleUseCase(articleRepository article.Repository) *UpdateArticleUseCase {
return &UpdateArticleUseCase{articleRepository}
}
func (g *UpdateArticleUseCase) UpdateBody(id uint32, body string) (article.Article, error) {
return g.articleRepository.UpdateBody(id, body)
}
func (g *UpdateArticleUseCase) AddImage(articleId uint32, imageId uint32) error {
return g.articleRepository.AddImage(articleId, imageId)
}
func (g *UpdateArticleUseCase) UpdateIsOnline(id uint32, isOnline bool) (article.Article, error) {
return g.articleRepository.UpdateIsOnline(id, isOnline)
}
@@ -0,0 +1,23 @@
package useCases
import "RenewCMS/internal/domain/user"
type UpdateUserUseCase struct {
userRepository user.Repository
}
func NewUpdateUserUseCase(userRepository user.Repository) *UpdateUserUseCase {
return &UpdateUserUseCase{userRepository}
}
func (g *UpdateUserUseCase) UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error) {
return g.userRepository.UpdateVerificationStatus(userId, isVerified)
}
func (g *UpdateUserUseCase) UpdatePasswordResetCode(userId uint32, code string) (user.User, error) {
return g.userRepository.UpdatePasswordResetCode(userId, code)
}
func (g *UpdateUserUseCase) UpdatePassword(userId uint32, password string) (user.User, error) {
return g.userRepository.UpdatePassword(userId, password)
}
-58
View File
@@ -1,58 +0,0 @@
const conn = new Mongo()
const db = conn.getDB("gohcms")
db.createCollection('articles', {
validator: {
$jsonSchema: {
required: ['titleID', 'title', 'content', 'date', 'tags', 'online'],
properties: {
_id: {
bsonType: 'objectId'
},
titleID: {
bsonType: 'string',
pattern: '^[-a-z]+$'
},
title: {
bsonType: 'string'
},
content: {
bsonType: 'object'
},
date: {
bsonType: 'number'
},
tags: {
bsonType: 'array'
},
online: {
bsonType: 'bool'
}
}
}
}
})
db.createCollection('users', {
validator: {
$jsonSchema: {
required: ['email', 'password'],
properties: {
_id: {
bsonType: 'objectId'
},
email: {
bsonType: 'string'
},
password: {
bsonType: 'string'
}
}
}
}
})
db.users.insertOne({
email: "root",
password: "root"
})
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
OUTPUT_DIR=./bin
GO_FILE_PATH="./cmd/server/main.go"
PROGRAM_NAME=RenewCMS
WEB_DIR="./web"
echo "Starting frontend build in $WEB_DIR..."
if [ -d "$WEB_DIR" ]; then
(cd "$WEB_DIR" && bun run build)
if [ $? -ne 0 ]; then
echo "Frontend build failed. Aborting process."
exit 1
fi
else
echo "Error: Directory $WEB_DIR not found."
exit 1
fi
echo "Frontend build completed successfully."
echo "----------------------------------------"
GOOS=$(go env GOOS)
GOARCH=$(go env GOARCH)
output_name="$OUTPUT_DIR/${PROGRAM_NAME}_${GOOS}_${GOARCH}"
if [ "$GOOS" = "windows" ]; then
output_name+='.exe'
fi
echo "Detected platform: $GOOS/$GOARCH"
echo "Building for $GOOS/$GOARCH..."
env GOOS=$GOOS GOARCH=$GOARCH go build -o "$output_name" "$GO_FILE_PATH"
if [ $? -eq 0 ]; then
echo "Compilation successful: $output_name"
else
echo "Compilation for $GOOS/$GOARCH failed."
exit 1
fi
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
OUTPUT_DIR=./bin
GO_FILE_PATH="./cmd/server/main.go"
PROGRAM_NAME=RenewCMS
platforms=("windows/amd64" "windows/arm64" "linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64")
for platform in "${platforms[@]}"
do
platform_split=(${platform//\// })
GOOS=${platform_split[0]}
GOARCH=${platform_split[1]}
output_name="$OUTPUT_DIR/${PROGRAM_NAME}_${GOOS}_${GOARCH}"
if [ "$GOOS" = "windows" ]; then
output_name+='.exe'
fi
echo "Building for $GOOS/$GOARCH..."
env GOOS=$GOOS GOARCH=$GOARCH go build -o "$output_name" "$GO_FILE_PATH"
if [ $? -ne 0 ]; then
echo "Compilation for $GOOS/$GOARCH failed."
exit $?
fi
done
echo "Compilation successful for all platforms."
+99
View File
@@ -0,0 +1,99 @@
package test
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/infrastructure/useCases"
"errors"
"testing"
)
type mockArticleRepo struct {
articles map[uint32]article.Article
}
func (m *mockArticleRepo) FindByFilters(f article.Filters) ([]article.Article, error) {
var results []article.Article
for _, a := range m.articles {
if f.IsOnline != nil && a.IsOnline == *f.IsOnline {
results = append(results, a)
}
}
return results, nil
}
func (m *mockArticleRepo) Get(id uint32) (article.Article, error) {
a, ok := m.articles[id]
if !ok {
return article.Article{}, article.ErrArticleNotFound
}
return a, nil
}
func (m *mockArticleRepo) Create(a article.Article) (article.Article, error) {
a.ID = uint32(len(m.articles) + 1)
m.articles[a.ID] = a
return a, nil
}
func (m *mockArticleRepo) Delete(id uint32) error {
delete(m.articles, id)
return nil
}
func (m *mockArticleRepo) GetByName(n string) (article.Article, error) { return article.Article{}, nil }
func (m *mockArticleRepo) GetAll() []article.Article { return nil }
func (m *mockArticleRepo) UpdateBody(id uint32, b string) (article.Article, error) {
return article.Article{}, nil
}
func (m *mockArticleRepo) UpdateIsOnline(id uint32, o bool) (article.Article, error) {
return article.Article{}, nil
}
func (m *mockArticleRepo) AddImage(p, i uint32) error { return nil }
var _ article.Repository = &mockArticleRepo{}
func TestCreateArticle(t *testing.T) {
repo := &mockArticleRepo{articles: make(map[uint32]article.Article)}
uc := useCases.NewCreateArticleUseCase(repo)
t.Run("Valid Title", func(t *testing.T) {
cmd := useCases.CreateArticleCommand{Title: "Valid Title", Body: "Content"}
res, err := uc.CreateArticle(cmd)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if res.Title != cmd.Title {
t.Errorf("Expected %s, got %s", cmd.Title, res.Title)
}
})
t.Run("Short Title", func(t *testing.T) {
cmd := useCases.CreateArticleCommand{Title: "Ab", Body: "Content"}
_, err := uc.CreateArticle(cmd)
if !errors.Is(err, article.ErrInvalidTitle) {
t.Errorf("Expected ErrInvalidTitle")
}
})
}
func TestGetOnlineArticle(t *testing.T) {
repo := &mockArticleRepo{articles: map[uint32]article.Article{
1: {ID: 1, IsOnline: true},
2: {ID: 2, IsOnline: false},
}}
uc := useCases.NewGetArticleUseCase(repo)
t.Run("Article Online", func(t *testing.T) {
_, err := uc.GetOnlineArticle(1)
if err != nil {
t.Errorf("Expected article to be found")
}
})
t.Run("Article Offline", func(t *testing.T) {
_, err := uc.GetOnlineArticle(2)
if !errors.Is(err, article.ErrArticleNotFound) {
t.Errorf("Offline article should return not found")
}
})
}
+67
View File
@@ -0,0 +1,67 @@
package test
import (
"RenewCMS/internal/domain/image"
"RenewCMS/internal/infrastructure/useCases"
"strings"
"testing"
)
type mockImageRepo struct {
images map[uint32]image.Image
}
func (m *mockImageRepo) Create(input image.ImageInput) (image.Image, error) {
newId := uint32(len(m.images) + 1)
img := image.Image{
ID: newId,
Path: "/static/" + input.Filename,
}
m.images[newId] = img
return img, nil
}
func (m *mockImageRepo) Delete(id uint32) error {
delete(m.images, id)
return nil
}
var _ image.Repository = &mockImageRepo{}
func TestImageUseCases(t *testing.T) {
repo := &mockImageRepo{images: make(map[uint32]image.Image)}
createUc := useCases.NewCreateImageUseCase(repo)
deleteUc := useCases.NewDeleteImageUseCase(repo)
t.Run("Create Image", func(t *testing.T) {
input := image.ImageInput{
Content: strings.NewReader("fake-image-data"),
Filename: "test.png",
ContentType: "image/png",
}
res, err := createUc.CreateImage(input)
if err != nil {
t.Fatalf("CreateImage failed: %v", err)
}
if res.ID == 0 {
t.Errorf("Expected valid ID, got 0")
}
if !strings.Contains(res.Path, "test.png") {
t.Errorf("Path mismatch: %s", res.Path)
}
})
t.Run("Delete Image", func(t *testing.T) {
repo.images[99] = image.Image{ID: 99, Path: "/static/delete-me.png"}
err := deleteUc.DeleteImage(99)
if err != nil {
t.Fatalf("DeleteImage failed: %v", err)
}
if _, exists := repo.images[99]; exists {
t.Errorf("Image 99 should have been deleted")
}
})
}
+55
View File
@@ -0,0 +1,55 @@
package test
import (
"RenewCMS/internal/domain/mail"
"RenewCMS/internal/infrastructure/useCases"
"errors"
"testing"
)
type mockMailRepo struct {
SentEmails []struct {
Address string
Template string
Data any
}
ShouldFail bool
}
func (m *mockMailRepo) Send(receiverAddress string, templateName string, data any) error {
if m.ShouldFail {
return errors.New("smtp error")
}
m.SentEmails = append(m.SentEmails, struct {
Address string
Template string
Data any
}{receiverAddress, templateName, data})
return nil
}
var _ mail.Repository = &mockMailRepo{}
func TestSendMail(t *testing.T) {
repo := &mockMailRepo{}
uc := useCases.NewSendMailUseCase(repo)
t.Run("Successful Email Send", func(t *testing.T) {
repo.ShouldFail = false
err := uc.SendMail("user@example.com", "welcome", "User123")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(repo.SentEmails) != 1 {
t.Errorf("Expected 1 email to be sent, got %d", len(repo.SentEmails))
}
})
t.Run("Failed Email Send", func(t *testing.T) {
repo.ShouldFail = true
err := uc.SendMail("fail@example.com", "reset", nil)
if err == nil {
t.Errorf("Expected an error but got nil")
}
})
}
+89
View File
@@ -0,0 +1,89 @@
package test
import (
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"testing"
)
type mockUserRepo struct {
users map[uint32]user.User
}
func (m *mockUserRepo) Create(u user.User) (user.User, error) {
u.ID = uint32(len(m.users) + 1)
m.users[u.ID] = u
return u, nil
}
func (m *mockUserRepo) Get(id uint32) (user.User, error) {
u, ok := m.users[id]
if !ok {
return user.User{}, nil
}
return u, nil
}
func (m *mockUserRepo) GetAll() []user.User {
var list []user.User
for _, u := range m.users {
list = append(list, u)
}
return list
}
func (m *mockUserRepo) UpdateVerificationStatus(id uint32, v bool) (user.User, error) {
u := m.users[id]
u.IsVerified = v
m.users[id] = u
return u, nil
}
func (m *mockUserRepo) GetByUsername(n string) (user.User, error) { return user.User{}, nil }
func (m *mockUserRepo) GetByEmail(e string) (user.User, error) { return user.User{}, nil }
func (m *mockUserRepo) Delete(id uint32) error { return nil }
func (m *mockUserRepo) UpdatePassword(id uint32, p string) (user.User, error) {
return user.User{}, nil
}
func (m *mockUserRepo) UpdatePasswordResetCode(id uint32, c string) (user.User, error) {
return user.User{}, nil
}
var _ user.Repository = &mockUserRepo{}
func TestCreateUser(t *testing.T) {
repo := &mockUserRepo{users: make(map[uint32]user.User)}
uc := useCases.NewCreateUserUseCase(repo)
cmd := useCases.CreateUserCommand{
Username: "testuser",
Password: "password123",
Email: "test@example.com",
}
res, err := uc.CreateUser(cmd)
if err != nil {
t.Fatalf("User creation failed: %v", err)
}
if res.Username != cmd.Username {
t.Errorf("Username mismatch")
}
if res.VerificationCode == "" {
t.Errorf("Verification code was not generated")
}
}
func TestUpdateUserVerification(t *testing.T) {
repo := &mockUserRepo{users: map[uint32]user.User{
1: {ID: 1, IsVerified: false},
}}
uc := useCases.NewUpdateUserUseCase(repo)
res, err := uc.UpdateVerificationStatus(1, true)
if err != nil {
t.Fatalf("Update failed: %v", err)
}
if !res.IsVerified {
t.Errorf("User should be verified")
}
}
+8
View File
@@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 4
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+36
View File
@@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"useTabs": true
}
+48
View File
@@ -0,0 +1,48 @@
# .
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
bun install
```
### Compile and Hot-Reload for Development
```sh
bun dev
```
### Type-Check, Compile and Minify for Production
```sh
bun run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
bun lint
```
-2
View File
@@ -1,2 +0,0 @@
/// <reference types="vite/client" />
declare const __APP_ENV__: Record<string, string>
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin panel</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
-30
View File
@@ -1,30 +0,0 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /app;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
-3264
View File
File diff suppressed because it is too large Load Diff
-28
View File
@@ -1,28 +0,0 @@
{
"name": "admin-gui",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@tinymce/tinymce-vue": "^4.0.7",
"pinia": "^2.0.28",
"tabulator-tables": "^5.4.3",
"vue": "^3.2.38",
"vue-router": "^4.1.5"
},
"devDependencies": {
"@types/node": "^16.11.56",
"@types/tabulator-tables": "^5.4.2",
"@vitejs/plugin-vue": "^3.0.3",
"@vue/tsconfig": "^0.1.3",
"npm-run-all": "^4.1.5",
"typescript": "~4.7.4",
"vite": "^3.0.9",
"vue-tsc": "^1.0.5"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
This is where language files should be placed.
Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/
-412
View File
@@ -1,412 +0,0 @@
/*!
* TinyMCE Language Pack
*
* Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
* Licensed under the Tiny commercial license. See https://www.tiny.cloud/legal/
*/
tinymce.addI18n('fr_FR', {
"Redo": "Rétablir",
"Undo": "Annuler",
"Cut": "Couper",
"Copy": "Copier",
"Paste": "Coller",
"Select all": "Sélectionner tout",
"New document": "Nouveau document",
"Ok": "OK",
"Cancel": "Annuler",
"Visual aids": "Aides visuelles",
"Bold": "Gras",
"Italic": "Italique",
"Underline": "Souligné",
"Strikethrough": "Barré",
"Superscript": "Exposant",
"Subscript": "Indice",
"Clear formatting": "Effacer la mise en forme",
"Remove": "Retiré",
"Align left": "Aligner à gauche",
"Align center": "Centrer",
"Align right": "Aligner à droite",
"No alignment": "Aucun alignement",
"Justify": "Justifier",
"Bullet list": "Liste à puces",
"Numbered list": "Liste numérotée",
"Decrease indent": "Réduire le retrait",
"Increase indent": "Augmenter le retrait",
"Close": "Fermer",
"Formats": "Formats",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": "Votre navigateur ne supporte pas laccès direct au presse-papiers. Merci d'utiliser les raccourcis clavier Ctrl+X/C/V.",
"Headings": "Titres",
"Heading 1": "Titre 1",
"Heading 2": "Titre 2",
"Heading 3": "Titre 3",
"Heading 4": "Titre 4",
"Heading 5": "Titre 5",
"Heading 6": "Titre 6",
"Preformatted": "Préformaté",
"Div": "Div",
"Pre": "Préformaté",
"Code": "Code",
"Paragraph": "Paragraphe",
"Blockquote": "Bloc de citation",
"Inline": "En ligne",
"Blocks": "Blocs",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Le collage est maintenant en mode texte brut. Les contenus seront collés sans retenir les formatages jusqu'à ce que vous désactivez cette option.",
"Fonts": "Polices",
"Font sizes": "Tailles de police",
"Class": "Classe",
"Browse for an image": "Rechercher une image",
"OR": "OU",
"Drop an image here": "Déposer une image ici",
"Upload": "Télécharger",
"Uploading image": "Téléversement d'une image",
"Block": "Bloc",
"Align": "Aligner",
"Default": "Par défaut",
"Circle": "Cercle",
"Disc": "Disque",
"Square": "Carré",
"Lower Alpha": "Alphabet en minuscules",
"Lower Greek": "Alphabet grec en minuscules",
"Lower Roman": "Chiffre romain inférieur",
"Upper Alpha": "Alphabet en majuscules",
"Upper Roman": "Chiffre romain supérieur",
"Anchor...": "Ancre...",
"Anchor": "Ancre",
"Name": "Nom",
"ID": "ID",
"ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "L'ID doit commencer par une lettre, suivie uniquement par des lettres, numéros, tirets, points, deux-points et underscores.",
"You have unsaved changes are you sure you want to navigate away?": "Vous avez des modifications non enregistrées, êtes-vous sûr de vouloir quitter la page ?",
"Restore last draft": "Restaurer le dernier brouillon",
"Special character...": "Caractère spécial...",
"Special Character": "Caractère spécial",
"Source code": "Code source",
"Insert/Edit code sample": "Insérer / modifier une exemple de code",
"Language": "Langue",
"Code sample...": "Exemple de code...",
"Left to right": "De gauche à droite",
"Right to left": "De droite à gauche",
"Title": "Titre",
"Fullscreen": "Plein écran",
"Action": "Action",
"Shortcut": "Raccourci",
"Help": "Aide",
"Address": "Adresse",
"Focus to menubar": "Mettre le focus sur la barre de menu",
"Focus to toolbar": "Mettre le focus sur la barre d'outils",
"Focus to element path": "Mettre le focus sur le chemin vers l'élément",
"Focus to contextual toolbar": "Mettre le focus sur la barre d'outils contextuelle",
"Insert link (if link plugin activated)": "Insérer un lien (si le plug-in link est activé)",
"Save (if save plugin activated)": "Enregistrer (si le plug-in save est activé)",
"Find (if searchreplace plugin activated)": "Rechercher (si le plug-in searchreplace est activé)",
"Plugins installed ({0}):": "Plug-ins installés ({0}) :",
"Premium plugins:": "Plug-ins premium :",
"Learn more...": "En savoir plus...",
"You are using {0}": "Vous utilisez {0}",
"Plugins": "Plug-ins",
"Handy Shortcuts": "Raccourcis utiles",
"Horizontal line": "Ligne horizontale",
"Insert/edit image": "Insérer/modifier image",
"Alternative description": "Description alternative",
"Accessibility": "Accessibilité",
"Image is decorative": "L'image est décorative",
"Source": "Source",
"Dimensions": "Dimensions",
"Constrain proportions": "Limiter les proportions",
"General": "Général",
"Advanced": "Options avancées",
"Style": "Style",
"Vertical space": "Espace vertical",
"Horizontal space": "Espace horizontal",
"Border": "Bordure",
"Insert image": "Insérer une image",
"Image...": "Image...",
"Image list": "Liste des images",
"Resize": "Redimensionner",
"Insert date/time": "Insérer date/heure",
"Date/time": "Date/heure",
"Insert/edit link": "Insérer/modifier lien",
"Text to display": "Texte à afficher",
"Url": "URL",
"Open link in...": "Ouvrir le lien dans...",
"Current window": "Fenêtre active",
"None": "Aucun",
"New window": "Nouvelle fenêtre",
"Open link": "Ouvrir le lien",
"Remove link": "Enlever le lien",
"Anchors": "Ancres",
"Link...": "Lien...",
"Paste or type a link": "Coller ou taper un lien",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "L'URL que vous avez saisi semble être une adresse e-mail. Souhaitez-vous y ajouter le préfixe requis mailto: ?",
"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": "L'URL que vous avez saisi semble être un lien externe. Souhaitez-vous y ajouter le préfixe requis mailto: ?",
"The URL you entered seems to be an external link. Do you want to add the required https:// prefix?": "L'URL que vous avez saisie semble être un lien externe. Voulez-vous ajouter le préfixe https:// requis ?",
"Link list": "Liste des liens",
"Insert video": "Insérer une vidéo",
"Insert/edit video": "Insérer/modifier une vidéo",
"Insert/edit media": "Insérer/modifier un média",
"Alternative source": "Source alternative",
"Alternative source URL": "URL de la source alternative",
"Media poster (Image URL)": "Affiche de média (URL de l'image)",
"Paste your embed code below:": "Collez votre code incorporé ci-dessous :",
"Embed": "Incorporer",
"Media...": "Média...",
"Nonbreaking space": "Espace insécable",
"Page break": "Saut de page",
"Paste as text": "Coller comme texte",
"Preview": "Aperçu",
"Print": "Imprimer",
"Print...": "Imprimer...",
"Save": "Enregistrer",
"Find": "Rechercher",
"Replace with": "Remplacer par",
"Replace": "Remplacer",
"Replace all": "Remplacer tout",
"Previous": "Précédente",
"Next": "Suivante",
"Find and Replace": "Trouver et remplacer",
"Find and replace...": "Trouver et remplacer...",
"Could not find the specified string.": "Impossible de trouver la chaîne spécifiée.",
"Match case": "Respecter la casse",
"Find whole words only": "Mot entier",
"Find in selection": "Trouver dans la sélection",
"Insert table": "Insérer un tableau",
"Table properties": "Propriétés du tableau",
"Delete table": "Supprimer le tableau",
"Cell": "Cellule",
"Row": "Ligne",
"Column": "Colonne",
"Cell properties": "Propriétés de la cellule",
"Merge cells": "Fusionner les cellules",
"Split cell": "Diviser la cellule",
"Insert row before": "Insérer une ligne avant",
"Insert row after": "Insérer une ligne après",
"Delete row": "Supprimer la ligne",
"Row properties": "Propriétés de la ligne",
"Cut row": "Couper la ligne",
"Cut column": "Couper la colonne",
"Copy row": "Copier la ligne",
"Copy column": "Copier la colonne",
"Paste row before": "Coller la ligne avant",
"Paste column before": "Coller la colonne avant",
"Paste row after": "Coller la ligne après",
"Paste column after": "Coller la colonne après",
"Insert column before": "Insérer une colonne avant",
"Insert column after": "Insérer une colonne après",
"Delete column": "Supprimer la colonne",
"Cols": "Colonnes",
"Rows": "Lignes",
"Width": "Largeur",
"Height": "Hauteur",
"Cell spacing": "Espacement entre les cellules",
"Cell padding": "Marge intérieure des cellules",
"Row clipboard actions": "Actions du presse-papiers des lignes",
"Column clipboard actions": "Actions du presse-papiers des colonnes",
"Table styles": "Style tableau",
"Cell styles": "Type de cellule",
"Column header": "En-tête de colonne",
"Row header": "En-tête de ligne",
"Table caption": "Légende de tableau",
"Caption": "Légende",
"Show caption": "Afficher une légende",
"Left": "Gauche",
"Center": "Centre",
"Right": "Droite",
"Cell type": "Type de cellule",
"Scope": "Étendue",
"Alignment": "Alignement",
"Horizontal align": "Alignement horizontal",
"Vertical align": "Alignement vertical",
"Top": "En haut",
"Middle": "Au milieu",
"Bottom": "En bas",
"Header cell": "Cellule d'en-tête",
"Row group": "Groupe de lignes",
"Column group": "Groupe de colonnes",
"Row type": "Type de ligne",
"Header": "En-tête",
"Body": "Corps",
"Footer": "Pied de page",
"Border color": "Couleur de bordure",
"Solid": "Trait continu",
"Dotted": "Pointillé",
"Dashed": "Tirets",
"Double": "Deux traits continus",
"Groove": "Sculpté",
"Ridge": "Extrudé",
"Inset": "Incrusté",
"Outset": "Relief",
"Hidden": "Masqué",
"Insert template...": "Insérer un modèle...",
"Templates": "Modèles",
"Template": "Modèle",
"Insert Template": "Insérer le modèle",
"Text color": "Couleur du texte",
"Background color": "Couleur d'arrière-plan",
"Custom...": "Personnalisée...",
"Custom color": "Couleur personnalisée",
"No color": "Aucune couleur",
"Remove color": "Supprimer la couleur",
"Show blocks": "Afficher les blocs",
"Show invisible characters": "Afficher les caractères invisibles",
"Word count": "Nombre de mots",
"Count": "Total",
"Document": "Document",
"Selection": "Sélection",
"Words": "Mots",
"Words: {0}": "Mots : {0}",
"{0} words": "{0} mots",
"File": "Fichier",
"Edit": "Modifier",
"Insert": "Insérer",
"View": "Afficher",
"Format": "Format",
"Table": "Tableau",
"Tools": "Outils",
"Powered by {0}": "Avec {0}",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Zone de Texte Riche. Appuyez sur ALT-F9 pour le menu. Appuyez sur ALT-F10 pour la barre d'outils. Appuyez sur ALT-0 pour l'aide",
"Image title": "Titre d'image",
"Border width": "Épaisseur de la bordure",
"Border style": "Style de la bordure",
"Error": "Erreur",
"Warn": "Avertir",
"Valid": "Valide",
"To open the popup, press Shift+Enter": "Pour ouvrir la popup, appuyez sur Maj+Entrée",
"Rich Text Area": "Zone de Texte Riche",
"Rich Text Area. Press ALT-0 for help.": "Zone de Texte Riche. Appuyez sur ALT-0 pour l'aide.",
"System Font": "Police système",
"Failed to upload image: {0}": "Échec d'envoi de l'image : {0}",
"Failed to load plugin: {0} from url {1}": "Échec de chargement du plug-in : {0} à partir de lURL {1}",
"Failed to load plugin url: {0}": "Échec de chargement de l'URL du plug-in : {0}",
"Failed to initialize plugin: {0}": "Échec d'initialisation du plug-in : {0}",
"example": "exemple",
"Search": "Rechercher",
"All": "Tout",
"Currency": "Devise",
"Text": "Texte",
"Quotations": "Citations",
"Mathematical": "Opérateurs mathématiques",
"Extended Latin": "Latin étendu",
"Symbols": "Symboles",
"Arrows": "Flèches",
"User Defined": "Défini par l'utilisateur",
"dollar sign": "Symbole dollar",
"currency sign": "Symbole devise",
"euro-currency sign": "Symbole euro",
"colon sign": "Symbole colón",
"cruzeiro sign": "Symbole cruzeiro",
"french franc sign": "Symbole franc français",
"lira sign": "Symbole lire",
"mill sign": "Symbole millième",
"naira sign": "Symbole naira",
"peseta sign": "Symbole peseta",
"rupee sign": "Symbole roupie",
"won sign": "Symbole won",
"new sheqel sign": "Symbole nouveau chékel",
"dong sign": "Symbole dong",
"kip sign": "Symbole kip",
"tugrik sign": "Symbole tougrik",
"drachma sign": "Symbole drachme",
"german penny symbol": "Symbole pfennig",
"peso sign": "Symbole peso",
"guarani sign": "Symbole guarani",
"austral sign": "Symbole austral",
"hryvnia sign": "Symbole hryvnia",
"cedi sign": "Symbole cedi",
"livre tournois sign": "Symbole livre tournois",
"spesmilo sign": "Symbole spesmilo",
"tenge sign": "Symbole tenge",
"indian rupee sign": "Symbole roupie indienne",
"turkish lira sign": "Symbole lire turque",
"nordic mark sign": "Symbole du mark nordique",
"manat sign": "Symbole manat",
"ruble sign": "Symbole rouble",
"yen character": "Sinogramme Yen",
"yuan character": "Sinogramme Yuan",
"yuan character, in hong kong and taiwan": "Sinogramme Yuan, Hong Kong et Taiwan",
"yen/yuan character variant one": "Sinogramme Yen/Yuan, première variante",
"Emojis": "Émojis",
"Emojis...": "Émojis...",
"Loading emojis...": "Chargement des emojis...",
"Could not load emojis": "Impossible de charger les emojis",
"People": "Personnes",
"Animals and Nature": "Animaux & nature",
"Food and Drink": "Nourriture & boissons",
"Activity": "Activité",
"Travel and Places": "Voyages & lieux",
"Objects": "Objets",
"Flags": "Drapeaux",
"Characters": "Caractères",
"Characters (no spaces)": "Caractères (espaces non compris)",
"{0} characters": "{0} caractères",
"Error: Form submit field collision.": "Erreur : conflit de champs lors de la soumission du formulaire.",
"Error: No form element found.": "Erreur : aucun élément de formulaire trouvé.",
"Color swatch": "Échantillon de couleurs",
"Color Picker": "Sélecteur de couleurs",
"Invalid hex color code: {0}": "Code couleur hexadécimal invalide : {0}",
"Invalid input": "Entrée invalide",
"R": "R",
"Red component": "Composant rouge",
"G": "V",
"Green component": "Composant vert",
"B": "B",
"Blue component": "Composant bleu",
"#": "#",
"Hex color code": "Code couleur hexadécimal",
"Range 0 to 255": "Plage de 0 à 255",
"Turquoise": "Turquoise",
"Green": "Vert",
"Blue": "Bleu",
"Purple": "Violet",
"Navy Blue": "Bleu marine",
"Dark Turquoise": "Turquoise foncé",
"Dark Green": "Vert foncé",
"Medium Blue": "Bleu moyen",
"Medium Purple": "Violet moyen",
"Midnight Blue": "Bleu de minuit",
"Yellow": "Jaune",
"Orange": "Orange",
"Red": "Rouge",
"Light Gray": "Gris clair",
"Gray": "Gris",
"Dark Yellow": "Jaune foncé",
"Dark Orange": "Orange foncé",
"Dark Red": "Rouge foncé",
"Medium Gray": "Gris moyen",
"Dark Gray": "Gris foncé",
"Light Green": "Vert clair",
"Light Yellow": "Jaune clair",
"Light Red": "Rouge clair",
"Light Purple": "Violet clair",
"Light Blue": "Bleu clair",
"Dark Purple": "Violet foncé",
"Dark Blue": "Bleu foncé",
"Black": "Noir",
"White": "Blanc",
"Switch to or from fullscreen mode": "Passer en ou quitter le mode plein écran",
"Open help dialog": "Ouvrir la boîte de dialogue d'aide",
"history": "historique",
"styles": "styles",
"formatting": "mise en forme",
"alignment": "alignement",
"indentation": "retrait",
"Font": "Police",
"Size": "Taille",
"More...": "Plus...",
"Select...": "Sélectionner...",
"Preferences": "Préférences",
"Yes": "Oui",
"No": "Non",
"Keyboard Navigation": "Navigation au clavier",
"Version": "Version",
"Code view": "Affichage du code",
"Open popup menu for split buttons": "Ouvrir le menu contextuel pour les boutons partagés",
"List Properties": "Propriétés de la liste",
"List properties...": "Lister les propriétés...",
"Start list at number": "Liste de départ au numéro",
"Line height": "Hauteur de la ligne",
"Dropped file type is not supported": "Le type de fichier déposé n'est pas pris en charge",
"Loading...": "Chargement...",
"ImageProxy HTTP error: Rejected request": "Erreur HTTP d'ImageProxy : Requête rejetée",
"ImageProxy HTTP error: Could not find Image Proxy": "Erreur HTTP d'ImageProxy : Impossible de trouver ImageProxy",
"ImageProxy HTTP error: Incorrect Image Proxy URL": "Erreur HTTP d'ImageProxy : URL de ImageProxy incorrecte",
"ImageProxy HTTP error: Unknown ImageProxy error": "Erreur HTTP d'ImageProxy : Erreur ImageProxy inconnue"
});
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because one or more lines are too long
@@ -1,4 +0,0 @@
/**
* TinyMCE version 6.3.1 (2022-12-06)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=(t,e,r)=>{const s="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(s,!1,!1===r?null:{"list-style-type":r})},r=t=>e=>e.options.get(t),s=r("advlist_number_styles"),n=r("advlist_bullet_styles"),l=t=>null==t,i=t=>!l(t);var o=tinymce.util.Tools.resolve("tinymce.util.Tools");class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return i(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=t=>i(t)&&/^(TH|TD)$/.test(t.nodeName),d=t=>l(t)||"default"===t?"":t,g=(t,e)=>r=>{const s=s=>{r.setActive(((t,e,r)=>{const s=((t,e)=>{for(let r=0;r<t.length;r++)if(e(t[r]))return r;return-1})(e.parents,u),n=-1!==s?e.parents.slice(0,s):e.parents,l=o.grep(n,(t=>e=>i(e)&&/^(OL|UL|DL)$/.test(e.nodeName)&&((t,e)=>t.dom.isChildOf(e,t.getBody()))(t,e))(t));return l.length>0&&l[0].nodeName===r})(t,s,e)),r.setEnabled(!((t,e)=>{const r=t.dom.getParent(e,"ol,ul,dl");return((t,e)=>null!==e&&"false"===t.dom.getContentEditableParent(e))(t,r)})(t,s.element))};return t.on("NodeChange",s),()=>t.off("NodeChange",s)},h=(t,r,s,n,l,i)=>{i.length>1?((t,r,s,n,l,i)=>{t.ui.registry.addSplitButton(r,{tooltip:s,icon:"OL"===l?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:t=>{t(o.map(i,(t=>{const e="OL"===l?"num":"bull",r="disc"===t||"decimal"===t?"default":t,s=d(t),n=(t=>t.replace(/\-/g," ").replace(/\b\w/g,(t=>t.toUpperCase())))(t);return{type:"choiceitem",value:s,icon:"list-"+e+"-"+r,text:n}})))},onAction:()=>t.execCommand(n),onItemAction:(r,s)=>{e(t,l,s)},select:e=>{const r=(t=>{const e=t.dom.getParent(t.selection.getNode(),"ol,ul"),r=t.dom.getStyle(e,"listStyleType");return a.from(r)})(t);return r.map((t=>e===t)).getOr(!1)},onSetup:g(t,l)})})(t,r,s,n,l,i):((t,r,s,n,l,i)=>{t.ui.registry.addToggleButton(r,{active:!1,tooltip:s,icon:"OL"===l?"ordered-list":"unordered-list",onSetup:g(t,l),onAction:()=>t.queryCommandState(n)||""===i?t.execCommand(n):e(t,l,i)})})(t,r,s,n,l,d(i[0]))};t.add("advlist",(t=>{t.hasPlugin("lists")?((t=>{const e=t.options.register;e("advlist_number_styles",{processor:"string[]",default:"default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman".split(",")}),e("advlist_bullet_styles",{processor:"string[]",default:"default,circle,square".split(",")})})(t),(t=>{h(t,"numlist","Numbered list","InsertOrderedList","OL",s(t)),h(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))})(t),(t=>{t.addCommand("ApplyUnorderedListStyle",((r,s)=>{e(t,"UL",s["list-style-type"])})),t.addCommand("ApplyOrderedListStyle",((r,s)=>{e(t,"OL",s["list-style-type"])}))})(t)):console.error("Please use the Lists plugin together with the Advanced List plugin.")}))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 6.3.1 (2022-12-06)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=("allow_html_in_named_anchor",e=>e.options.get("allow_html_in_named_anchor"));const a="a:not([href])",r=e=>!e,i=e=>e.getAttribute("id")||e.getAttribute("name")||"",l=e=>(e=>"a"===e.nodeName.toLowerCase())(e)&&!e.getAttribute("href")&&""!==i(e),s=e=>e.dom.getParent(e.selection.getStart(),a),d=(e,a)=>{const r=s(e);r?((e,t,o)=>{o.removeAttribute("name"),o.id=t,e.addVisual(),e.undoManager.add()})(e,a,r):((e,a)=>{e.undoManager.transact((()=>{n(e)||e.selection.collapse(!0),e.selection.isCollapsed()?e.insertContent(e.dom.createHTML("a",{id:a})):((e=>{const n=e.dom;t(n).walk(e.selection.getRng(),(e=>{o.each(e,(e=>{var t;l(t=e)&&!t.firstChild&&n.remove(e,!1)}))}))})(e),e.formatter.remove("namedAnchor",void 0,void 0,!0),e.formatter.apply("namedAnchor",{value:a}),e.addVisual())}))})(e,a),e.focus()},c=e=>(e=>r(e.attr("href"))&&!r(e.attr("id")||e.attr("name")))(e)&&!e.firstChild,m=e=>t=>{for(let o=0;o<t.length;o++){const n=t[o];c(n)&&n.attr("contenteditable",e)}};e.add("anchor",(e=>{(e=>{(0,e.options.register)("allow_html_in_named_anchor",{processor:"boolean",default:!1})})(e),(e=>{e.on("PreInit",(()=>{e.parser.addNodeFilter("a",m("false")),e.serializer.addNodeFilter("a",m(null))}))})(e),(e=>{e.addCommand("mceAnchor",(()=>{(e=>{const t=(e=>{const t=s(e);return t?i(t):""})(e);e.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:t=>{((e,t)=>/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(t)?(d(e,t),!0):(e.windowManager.alert("ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),!1))(e,t.getData().id)&&t.close()}})})(e)}))})(e),(e=>{const t=()=>e.execCommand("mceAnchor");e.ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:t,onSetup:t=>e.selection.selectorChangedWithUnbind("a:not([href])",t.setActive).unbind}),e.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:t})})(e),e.on("PreInit",(()=>{(e=>{e.formatter.register("namedAnchor",{inline:"a",selector:a,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:(e,t,o)=>l(e)})})(e)}))}))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 6.3.1 (2022-12-06)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),n=t("autolink_pattern"),o=t("link_default_target"),r=t("link_default_protocol"),a=t("allow_unsafe_link_target"),s=("string",e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(a=o.constructor)||void 0===a?void 0:a.name)===r.name)?"string":t;var n,o,r,a})(e));const l=(void 0,e=>undefined===e);const i=e=>!(e=>null==e)(e),c=Object.hasOwnProperty,d=e=>"\ufeff"===e;var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>/^[(\[{ \u00a0]$/.test(e),g=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!d(t)&&n(t))return o}return-1},m=(e,t)=>{var o;const a=e.schema.getVoidElements(),s=n(e),{dom:i,selection:d}=e;if(null!==i.getParent(d.getNode(),"a[href]"))return null;const m=d.getRng(),k=u(i,(e=>{return i.isBlock(e)||(t=a,n=e.nodeName.toLowerCase(),c.call(t,n))||"false"===i.getContentEditable(e);var t,n})),{container:p,offset:y}=((e,t)=>{let n=e,o=t;for(;1===n.nodeType&&n.childNodes[o];)n=n.childNodes[o],o=3===n.nodeType?n.data.length:n.childNodes.length;return{container:n,offset:o}})(m.endContainer,m.endOffset),h=null!==(o=i.getParent(p,i.isBlock))&&void 0!==o?o:i.getRoot(),w=k.backwards(p,y+t,((e,t)=>{const n=e.data,o=g(n,t,(r=f,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1}),h);if(!w)return null;let v=w.container;const _=k.backwards(w.container,w.offset,((e,t)=>{v=e;const n=g(e.data,t,f);return-1===n?n:n+1}),h),A=i.createRng();_?A.setStart(_.container,_.offset):A.setStart(v,0),A.setEnd(w.container,w.offset);const C=A.toString().replace(/\uFEFF/g,"").match(s);if(C){let t=C[0];return $="www.",(b=t).length>=$.length&&b.substr(0,0+$.length)===$?t=r(e)+"://"+t:((e,t,n=0,o)=>{const r=e.indexOf(t,n);return-1!==r&&(!!l(o)||r+t.length<=o)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:A,url:t}}var b,$;return null},k=(e,t)=>{const{dom:n,selection:r}=e,{rng:l,url:i}=t,c=r.getBookmark();r.setRng(l);const d="createlink",u={command:d,ui:!1,value:i};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(d,!1,i),e.dispatch("ExecCommand",u);const t=o(e);if(s(t)){const o=r.getNode();n.setAttrib(o,"target",t),"_blank"!==t||a(e)||n.setAttrib(o,"rel","noopener")}}r.moveToBookmark(c),e.nodeChanged()},p=e=>{const t=m(e,-1);i(t)&&k(e,t)},y=p;e.add("autolink",(e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",(t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=m(e,0);i(t)&&k(e,t)})(e)})),e.on("keyup",(t=>{32===t.keyCode?p(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&y(e)}))})(e)}))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 6.3.1 (2022-12-06)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env");const o=e=>t=>t.options.get(e),n=o("min_height"),s=o("max_height"),i=o("autoresize_overflow_padding"),r=o("autoresize_bottom_margin"),l=(e,t)=>{const o=e.getBody();o&&(o.style.overflowY=t?"":"hidden",t||(o.scrollTop=0))},a=(e,t,o,n)=>{var s;const i=parseInt(null!==(s=e.getStyle(t,o,n))&&void 0!==s?s:"",10);return isNaN(i)?0:i},g=(e,o,i)=>{var c;const u=e.dom,d=e.getDoc();if(!d)return;if((e=>e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen())(e))return void l(e,!0);const f=d.documentElement,m=r(e),p=null!==(c=n(e))&&void 0!==c?c:e.getElement().offsetHeight;let h=p;const v=a(u,f,"margin-top",!0),y=a(u,f,"margin-bottom",!0);let C=f.offsetHeight+v+y+m;C<0&&(C=0);const S=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight;C+S>p&&(h=C+S);const z=s(e);if(z&&h>z?(h=z,l(e,!0)):l(e,!1),h!==o.get()){const n=h-o.get();if(u.setStyle(e.getContainer(),"height",h+"px"),o.set(h),(e=>{e.dispatch("ResizeEditor")})(e),t.browser.isSafari()&&(t.os.isMacOS()||t.os.isiOS())){const t=e.getWin();t.scrollTo(t.pageXOffset,t.pageYOffset)}e.hasFocus()&&(e=>{if("setcontent"===(null==e?void 0:e.type.toLowerCase())){const t=e;return!0===t.selection||!0===t.paste}return!1})(i)&&e.selection.scrollIntoView(),(t.browser.isSafari()||t.browser.isChromium())&&n<0&&g(e,o,i)}};e.add("autoresize",(e=>{if((e=>{const t=e.options.register;t("autoresize_overflow_padding",{processor:"number",default:1}),t("autoresize_bottom_margin",{processor:"number",default:50})})(e),e.options.isSet("resize")||e.options.set("resize",!1),!e.inline){const t=(e=>{let t=0;return{get:()=>t,set:e=>{t=e}}})();((e,t)=>{e.addCommand("mceAutoResize",(()=>{g(e,t)}))})(e,t),((e,t)=>{e.on("init",(()=>{const t=i(e),o=e.dom;o.setStyles(e.getDoc().documentElement,{height:"auto"}),o.setStyles(e.getBody(),{paddingLeft:t,paddingRight:t,"min-height":0})})),e.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",(o=>{g(e,t,o)}))})(e,t)}}))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 6.3.1 (2022-12-06)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=("string",t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(r=o=t,(a=String).prototype.isPrototypeOf(r)||(null===(s=o.constructor)||void 0===s?void 0:s.name)===a.name)?"string":e;var r,o,a,s})(t));const r=(void 0,t=>undefined===t);var o=tinymce.util.Tools.resolve("tinymce.util.Delay"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),s=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=t=>{const e=/^(\d+)([ms]?)$/.exec(t);return(e&&e[2]?{s:1e3,m:6e4}[e[2]]:1)*parseInt(t,10)},i=t=>e=>e.options.get(t),u=i("autosave_ask_before_unload"),l=i("autosave_restore_when_empty"),c=i("autosave_interval"),d=i("autosave_retention"),m=t=>{const e=document.location;return t.options.get("autosave_prefix").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)},v=(t,e)=>{if(r(e))return t.dom.isEmpty(t.getBody());{const r=s.trim(e);if(""===r)return!0;{const e=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(e)}}},f=t=>{var e;const r=parseInt(null!==(e=a.getItem(m(t)+"time"))&&void 0!==e?e:"0",10)||0;return!((new Date).getTime()-r>d(t)&&(p(t,!1),1))},p=(t,e)=>{const r=m(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&(t=>{t.dispatch("RemoveDraft")})(t)},g=t=>{const e=m(t);!v(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),(t=>{t.dispatch("StoreDraft")})(t))},y=t=>{var e;const r=m(t);f(t)&&(t.setContent(null!==(e=a.getItem(r+"draft"))&&void 0!==e?e:"",{format:"raw"}),(t=>{t.dispatch("RestoreDraft")})(t))};var D=tinymce.util.Tools.resolve("tinymce.EditorManager");const h=t=>e=>{e.setEnabled(f(t));const r=()=>e.setEnabled(f(t));return t.on("StoreDraft RestoreDraft RemoveDraft",r),()=>t.off("StoreDraft RestoreDraft RemoveDraft",r)};t.add("autosave",(t=>((t=>{const r=t.options.register,o=t=>{const r=e(t);return r?{value:n(t),valid:r}:{valid:!1,message:"Must be a string."}};r("autosave_ask_before_unload",{processor:"boolean",default:!0}),r("autosave_prefix",{processor:"string",default:"tinymce-autosave-{path}{query}{hash}-{id}-"}),r("autosave_restore_when_empty",{processor:"boolean",default:!1}),r("autosave_interval",{processor:o,default:"30s"}),r("autosave_retention",{processor:o,default:"20m"})})(t),(t=>{t.editorManager.on("BeforeUnload",(t=>{let e;s.each(D.get(),(t=>{t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))})),e&&(t.preventDefault(),t.returnValue=e)}))})(t),(t=>{(t=>{const e=c(t);o.setEditorInterval(t,(()=>{g(t)}),e)})(t);const e=()=>{(t=>{t.undoManager.transact((()=>{y(t),p(t)})),t.focus()})(t)};t.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)}),t.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)})})(t),t.on("init",(()=>{l(t)&&t.dom.isEmpty(t.getBody())&&y(t)})),(t=>({hasDraft:()=>f(t),storeDraft:()=>g(t),restoreDraft:()=>y(t),removeDraft:e=>p(t,e),isEmpty:e=>v(t,e)}))(t))))}();
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More