Skip to content

Commit 2296d54

Browse files
Merge pull request #1 from Zenika/wip
First version of backend, just missing tests
2 parents 160456b + ec55f3d commit 2296d54

40 files changed

+3779
-6
lines changed

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.sqlite
2+
*.db
3+
.idea/
4+
*.md
5+
Dockerfile

.gitignore

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudiocode,go
2+
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,visualstudiocode,go
3+
4+
### Go ###
5+
# If you prefer the allow list template instead of the deny list, see community template:
6+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
7+
#
8+
# Binaries for programs and plugins
9+
*.exe
10+
*.exe~
11+
*.dll
12+
*.so
13+
*.dylib
14+
15+
# Test binary, built with `go test -c`
16+
*.test
17+
18+
# Output of the go coverage tool, specifically when used with LiteIDE
19+
*.out
20+
21+
# Dependency directories (remove the comment below to include it)
22+
# vendor/
23+
24+
# Go workspace file
25+
go.work
26+
27+
### Intellij+all ###
28+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
29+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
30+
31+
# User-specific stuff
32+
.idea/**/workspace.xml
33+
.idea/**/tasks.xml
34+
.idea/**/usage.statistics.xml
35+
.idea/**/dictionaries
36+
.idea/**/shelf
37+
38+
# AWS User-specific
39+
.idea/**/aws.xml
40+
41+
# Generated files
42+
.idea/**/contentModel.xml
43+
44+
# Sensitive or high-churn files
45+
.idea/**/dataSources/
46+
.idea/**/dataSources.ids
47+
.idea/**/dataSources.local.xml
48+
.idea/**/sqlDataSources.xml
49+
.idea/**/dynamic.xml
50+
.idea/**/uiDesigner.xml
51+
.idea/**/dbnavigator.xml
52+
53+
# Gradle
54+
.idea/**/gradle.xml
55+
.idea/**/libraries
56+
57+
# Gradle and Maven with auto-import
58+
# When using Gradle or Maven with auto-import, you should exclude module files,
59+
# since they will be recreated, and may cause churn. Uncomment if using
60+
# auto-import.
61+
# .idea/artifacts
62+
# .idea/compiler.xml
63+
# .idea/jarRepositories.xml
64+
# .idea/modules.xml
65+
# .idea/*.iml
66+
# .idea/modules
67+
# *.iml
68+
# *.ipr
69+
70+
# CMake
71+
cmake-build-*/
72+
73+
# Mongo Explorer plugin
74+
.idea/**/mongoSettings.xml
75+
76+
# File-based project format
77+
*.iws
78+
79+
# IntelliJ
80+
out/
81+
82+
# mpeltonen/sbt-idea plugin
83+
.idea_modules/
84+
85+
# JIRA plugin
86+
atlassian-ide-plugin.xml
87+
88+
# Cursive Clojure plugin
89+
.idea/replstate.xml
90+
91+
# SonarLint plugin
92+
.idea/sonarlint/
93+
94+
# Crashlytics plugin (for Android Studio and IntelliJ)
95+
com_crashlytics_export_strings.xml
96+
crashlytics.properties
97+
crashlytics-build.properties
98+
fabric.properties
99+
100+
# Editor-based Rest Client
101+
.idea/httpRequests
102+
103+
# Android studio 3.1+ serialized cache file
104+
.idea/caches/build_file_checksums.ser
105+
106+
### Intellij+all Patch ###
107+
# Ignore everything but code style settings and run configurations
108+
# that are supposed to be shared within teams.
109+
110+
.idea/*
111+
112+
!.idea/codeStyles
113+
!.idea/runConfigurations
114+
115+
### VisualStudioCode ###
116+
.vscode/*
117+
!.vscode/settings.json
118+
!.vscode/tasks.json
119+
!.vscode/launch.json
120+
!.vscode/extensions.json
121+
!.vscode/*.code-snippets
122+
123+
# Local History for Visual Studio Code
124+
.history/
125+
126+
# Built Visual Studio Code Extensions
127+
*.vsix
128+
129+
### VisualStudioCode Patch ###
130+
# Ignore all local history of files
131+
.history
132+
.ionide
133+
134+
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudiocode,go
135+
*.sqlite3

Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM golang:1.24-bookworm AS build
2+
3+
WORKDIR /build
4+
COPY . .
5+
RUN go mod tidy
6+
RUN CGO_ENABLED=1 go build -tags=viper_bind_struct -o /build/til ./cmd
7+
8+
FROM debian:bookworm AS run
9+
10+
RUN apt update ; apt install -y ca-certificates
11+
WORKDIR /
12+
COPY --from=build /build/til /til
13+
RUN chmod +x /til
14+
ENTRYPOINT ["/til"]

README.md

Lines changed: 151 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,156 @@
1-
# TIL v2
1+
# Today I Learned (TiL) - v2
22

3-
Today I Learned is back 😎
3+
TIL is a simple software that aim to enable article and reflexion-sharing across all Zenika agencies in the world. It
4+
acts as an aggregator (as
5+
daily.dev can do), but on-premises and with filters on content to avoid being spammed with news that don't interest us.
46

5-
Documentation will come shortly
67

7-
**THIS IS A WIP!**
8+
## Getting started
89

9-
Run it: `TIL_SERVER_PORT=9023 TIL_DATABASE_FILE_NAME=toto.db TIL_DEBUG=false TIL_GOOGLE_CLIENT_ID=my_id TIL_GOOGLE_CLIENT_SECRET=my_secret TIL_JWT_SECRET=aaaaaa go run -tags=viper_bind_struct ./cmd`
10+
### Developer tools
1011

11-
Proudly powered by the Dobby Team.
12+
If you would like to develop TiL, the best way is to simply install Go on your computer ; then run
13+
`go run -tags=viper_bind_struct ./cmd`. The server
14+
will start on port 8000.
15+
16+
### Docker
17+
18+
If you simply want to run the application, Docker is the best way to do. Simply run `docker build -t til:latest .`, and
19+
run it! Full configuration
20+
reference below. Please think to mount database file in your container if you want persistent data!
21+
22+
## Configuration
23+
24+
All the configuration is done through environment variables. Viper is in charge of automatically parse them and load
25+
them in our Go application. The
26+
available environment variables are the following:
27+
28+
* **(Mandatory)** `TIL_JWT_SECRET`: The secret key used to sign JWT tokens issued by the app.
29+
* **(Mandatory)** `TIL_GOOGLE_CLIENT_ID`: The Client ID for Google Authentication
30+
* **(Mandatory)** `TIL_GOOGLE_CLIENT_SECRET`: The Client Secret for Google Authentication
31+
* *(Optional)* `TIL_DEBUG` (default: `false`): Set this variable to `true` if you want to enable debug logs on
32+
application. Please be careful: this mode will log WAY MORE information, even secret tokens! The debug mode will also
33+
disable token expiry time check, which may lead to a lot of JWT renewal. Do not use this in production.
34+
* *(Optional)* `TIL_DATABASE_FILE_NAME` (default: `til.sqlite3`): Specify database file name (and location).
35+
* *(Optional)* `TIL_SERVER_PORT` (default: `8000`): Server port to listen on
36+
* *(Optional)* `TIL_GOOGLE_TOKEN_ENDPOINT` (default: `https://oauth2.googleapis.com/token`): The Google Authentication
37+
endpoint to validate user token
38+
* *(Optional)* `TIL_DEFAULT_ADMIN` (default: none): The user to immediately promote as admin. Use his Google ID in this
39+
variable to make it a superuser of the application. This variable will only take effect if ALL the following conditions are met :
40+
* The variable is set to a non-void value;
41+
* There is no other user (admin or not) in the database.
42+
43+
44+
## Tests
45+
46+
Tests are made using Gherkin (Cucumber). This allows non-technical people to understand and collaborate to tests,
47+
improving security and coverage.
48+
49+
All the tests can be find in `test/` folder. We are using a mock for the Google Authentication Server so you'll never
50+
have to provide a secret to run tests. The `test/` folder includes some secrets (notably an RSA key and a JWT secret
51+
key): these secrets are dedicated to our tests, and have never been used in production. Please do not report them as a
52+
security issue; we're perfectly aware of it. Obviously, do not use these secrets for your own production (otherwise,
53+
magic will probably happen!).
54+
55+
All our tests are containerized to avoid configuration issues. To run them, simply type `go test -v ./test/`, and Golang
56+
will do the rest.
57+
58+
The following sentences are available :
59+
60+
#### Authentication
61+
62+
* `I have a JWT token for my regular|admin user`
63+
* This call will connect a user with the defined privilege. Every subsequent API calls will be authenticated using
64+
this JWT token.
65+
* `I clear the current JWT token`
66+
* Remove the current JWT token (if any). Every subsequent API calls will be unauthenticated.
67+
68+
#### Database
69+
70+
* `I reset the database`
71+
* Reset the database by restarting the container
72+
73+
#### HTTP
74+
75+
* `I send a "GET|POST|PUT|DELETE" request to "ENDPOINT"`
76+
* Send a request to the defined endpoint (an endpoint is an API path, for example `/posts`).
77+
* `I send a "GET|POST|PUT|DELETE" request to "ENDPOINT" with payload`
78+
* Same as before, but add a payload to the request. The payload must be placed on the next line, between `"""`. See
79+
example below or in `test/features/test.feature` for more details.
80+
* `the response code should be XXX`
81+
* Check that the previous request sent have the defined status code.
82+
* `the response should have XXX items in path "PATH"`
83+
* **WORKS ONLY ON ARRAYS** Count the number of items returned by the server in the JSON file.
84+
* The path is the place on the JSON to look, dot-separated. `@` is the main document. For example, if you want to
85+
access the `items` part of your JSON, the path will be `@.items`.
86+
* `I save the "XXXXX" header as "XXXXXX" for suite`
87+
* Save the content of a header in a variable for later re-use. For example,
88+
`I save the "X-Id" header as "postId" for suite` will allow you to write
89+
`I send a "GET" request to "/posts/{{postId}}"` later on.
90+
* `I save the value "XXX" in path "YYY" as "ZZZ" for suite`
91+
* Save the value XXX located in path YYY in a variable for later reuse. For example, `I save the value "id" in path "@" as "userId" for suite`
92+
* `the response should have the following content in path "XXX"`
93+
* **WORKS ONLY ON OBJECTS** Check that the specified values for each key are correct.
94+
* `the response should have the following items in path "XXX"`
95+
* **WORKS ONLY ON ARRAYS** Same as the previous one, but for arrays.
96+
* `the response should have the key "XXX" in path "YYY"`
97+
* Check if the response contains a specified key in the desired path. For example `the response should have the key "id" in path "@.items.0"`
98+
* `the response should not have the key "XXX" in path "YYY"`
99+
* The exact opposite as before ;-)
100+
101+
102+
### Example
103+
104+
In this example, we log in as a regular user, then create a post before accessing it.
105+
106+
```gherkin
107+
Feature: Demo
108+
109+
Scenario: Demo
110+
When I have a JWT token for my regular user
111+
And I send a "POST" request to "/posts" with payload
112+
"""
113+
{
114+
"title": "Test",
115+
"link": "https://lamontagne.fr",
116+
"tags": ["lang:fr"]
117+
}
118+
"""
119+
And the response code should be 201
120+
And I save the "X-Post-Id" header as "postId" for suite
121+
And I send a "GET" request to "/posts/"
122+
And the response code should be 200
123+
And the response should have the following content in path "@"
124+
| total_items | total_pages | current_page | items_per_page |
125+
| 1 | 1 | 0 | 20 |
126+
And the response should have the following items in path "@.items"
127+
| id | title | link |
128+
| {{postId}} | Test | https://lamontagne.fr |
129+
And the response should have 1 items in path "@.items.0.tags"
130+
And the response should have the following items in path "@.items.0.tags"
131+
| lang:fr |
132+
```
133+
134+
## Will it scale?
135+
136+
TiL is conceived following the KISS way of life; but it doesn't mean it won't scale. During our tests, we ran the
137+
following Locust file, with 50 concurrent users running 131 RPS:
138+
139+
```py
140+
from locust import HttpUser, between, task
141+
142+
143+
class WebsiteUser(HttpUser):
144+
@task
145+
def index(self):
146+
self.client.get("/posts", headers={"Authorization": "token"})
147+
148+
149+
@task
150+
def publish(self):
151+
self.client.post("/posts", headers={"Authorization": ""}, json={"title":"essai", "link":"https://www.lamontagne.fr/paris-75000/actualites/coupure-de-courant-geante-habitants-dans-la-rue-metros-et-avions-a-l-arret-les-images-du-chaos-en-espagne-et-au-portugal_14679007/", "tags": ["failure", "electricity", "lang:fr"], "content": "severe electricty shortage in Spain and Portugal"})
152+
```
153+
154+
During 10 minutes, the locust file generated roughly 30k entries in database, and the same amount of article queried.
155+
The Locust file ended with 0 failure and a MTRR around 600ms, which is a good result stating that our test is far above
156+
the real frequentation.

cmd/main.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"github.com/go-chi/chi/v5"
6+
"github.com/go-chi/chi/v5/middleware"
7+
"github.com/zenika/tilv2back/internal/configuration"
8+
"github.com/zenika/tilv2back/internal/controllers"
9+
"github.com/zenika/tilv2back/internal/repository"
10+
"github.com/zenika/tilv2back/internal/structures"
11+
"net/http"
12+
)
13+
14+
func main() {
15+
r := chi.NewRouter()
16+
r.Use(controllers.HandleCORS)
17+
r.Use(middleware.Logger)
18+
r.Use(controllers.PaginationMiddleware)
19+
20+
// Unauthenticated endpoints
21+
r.Get("/auth", controllers.RunAuth)
22+
r.Get("/configuration", controllers.Configuration)
23+
24+
// Authenticated endpoints
25+
r.Group(func(r chi.Router) {
26+
r.Use(controllers.AuthenticationMiddleware)
27+
28+
r.Route("/posts", func(r chi.Router) {
29+
r.Get("/stream", controllers.RealTimePostsUpdates)
30+
r.Get("/", controllers.GetPosts)
31+
r.Post("/", controllers.CreatePost)
32+
r.Delete("/{postId}", controllers.DeletePost)
33+
r.Get("/{postId}", controllers.GetPost)
34+
})
35+
36+
r.Route("/users", func(r chi.Router) {
37+
r.Get("/", controllers.GetUsers)
38+
r.Delete("/{userId}", controllers.DeleteUser)
39+
r.Get("/{userId}", controllers.GetUser)
40+
r.Put("/{userId}", controllers.UpdateUser)
41+
r.Put("/{userId}/renew", controllers.RenewFeedKey)
42+
})
43+
44+
r.Route("/bookmarks", func(r chi.Router) {
45+
r.Get("/", controllers.GetBookmarks)
46+
r.Put("/{postId}", controllers.AddBookmark)
47+
r.Delete("/{postId}", controllers.DeleteBookmark)
48+
})
49+
50+
r.Get("/rss", controllers.GetRSSFeed)
51+
r.Get("/tags", controllers.GetTags)
52+
r.Get("/renew", controllers.RenewToken)
53+
})
54+
55+
if users, err := repository.GetUsers(structures.Pagination{}, false); err == nil && users.TotalItems == 0 && configuration.Configuration.DefaultAdmin != "" {
56+
_, err := repository.CreateUser(structures.User{
57+
DisplayName: "Admin",
58+
GoogleId: configuration.Configuration.DefaultAdmin,
59+
IsAdmin: true,
60+
})
61+
if err != nil {
62+
configuration.Logger.Error("Unable to register default admin.", err.Error())
63+
}
64+
}
65+
66+
configuration.Logger.Info(fmt.Sprintf("Server is now listening on port %d", configuration.Configuration.ServerPort))
67+
_ = http.ListenAndServe(fmt.Sprintf(":%d", configuration.Configuration.ServerPort), r)
68+
}

0 commit comments

Comments
 (0)