├── .dockerignore ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── Dockerfile.tls.yml ├── LICENSE ├── README.md ├── Taskfile.yml ├── auth ├── authenticator.go ├── authenticator_test.go ├── authorizer.go ├── authorizer_test.go ├── middleware.go ├── middleware_test.go ├── rbac_model.conf ├── rbac_policy.csv ├── users.go └── users_test.go ├── database ├── collection.go ├── collection_test.go ├── db.go └── db_test.go ├── examples ├── go │ ├── .gitignore │ ├── README.md │ ├── daredb_basic.go │ ├── daredb_jwt_basic.go │ ├── go.mod │ └── go.sum └── python │ ├── .gitignore │ └── daredb_jwt_collection.py ├── go.mod ├── go.sum ├── logger ├── formatter.go └── logger.go ├── main.go ├── openapi ├── .gitignore ├── Dockerfile ├── README.md ├── compose.yml ├── daredb-openapi_primary.yaml ├── go.mod ├── go.sum ├── images │ └── openapi-auth.png └── main.go ├── server ├── configuration.go ├── configuration_test.go ├── constants.go ├── dare-server.go ├── dare-server_test.go ├── factory.go ├── factory_test.go ├── server.go └── server_test.go ├── taskfiles └── Taskfile_docker.yml └── utils └── utils.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | .vscode 23 | 24 | # Go workspace file 25 | go.work 26 | go.work.sum 27 | 28 | # Temporary files created by the Go build process 29 | tmp/ 30 | pkg/ 31 | temp/ 32 | 33 | # Cache directories created by Go tools 34 | 35 | # CGO (foreign function interface) cache 36 | _cgo_defunct.c 37 | _cgo_export.h 38 | 39 | # Build cache 40 | build/ 41 | 42 | # DareDB config file 43 | config.toml 44 | 45 | # DareDB directories 46 | settings/* 47 | data/* 48 | 49 | # log files 50 | 51 | *.log -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @dmarro89 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dmarro89 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | CHANGE ME: provide a short description for the pull request 2 | 3 | Added: 4 | 5 | * CHANGE ME: if applicable, please make a short list with added features/code 6 | 7 | Changes and edits: 8 | 9 | * CHANGE ME: if applicable, please make a short list with edits and changes 10 | 11 | Fixed: 12 | 13 | * CHANGE ME: if applicable, please make a short list with fixes 14 | 15 | Dependencies: 16 | 17 | * CHANGE ME: if applicable, please make a short list added/removed dependencies 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup Go 1.23.4 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: 1.23.4 20 | cache-dependency-path: ../../go.sum 21 | - name: Install dependencies 22 | run: | 23 | go get ./... 24 | - name: Build 25 | run: go build -v ./... 26 | - name: Test with the Go CLI 27 | run: go test -v ./... > TestResults.json 28 | - name: Upload Go test results 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: Go-results 32 | path: TestResults.json 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | .vscode 23 | 24 | # Go workspace file 25 | go.work 26 | go.work.sum 27 | 28 | # Temporary files created by the Go build process 29 | tmp/ 30 | pkg/ 31 | temp/ 32 | resources/ 33 | # Cache directories created by Go tools 34 | 35 | # CGO (foreign function interface) cache 36 | _cgo_defunct.c 37 | _cgo_export.h 38 | 39 | # Build cache 40 | build/ 41 | 42 | # DareDB config file 43 | config.toml 44 | 45 | # DareDB directories 46 | settings/* 47 | data/* 48 | 49 | # log files 50 | *.log 51 | 52 | # special binaries 53 | dare-db-app -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-bullseye AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod . 6 | COPY go.sum . 7 | 8 | RUN go mod download 9 | COPY . . 10 | ENV GOCACHE=/root/.cache/go-build 11 | RUN --mount=type=cache,target="/root/.cache/go-build" go build -o app 12 | 13 | FROM ubuntu:22.04 14 | RUN apt-get update && apt-get install -y curl netcat 15 | 16 | ENV DARE_TLS_ENABLED="false" 17 | RUN mkdir /app 18 | WORKDIR /app 19 | 20 | COPY auth/rbac_model.conf /app/auth/rbac_model.conf 21 | COPY auth/rbac_policy.csv /app/auth/rbac_policy.csv 22 | 23 | COPY --from=builder /app/app . 24 | 25 | EXPOSE 2605 26 | 27 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /Dockerfile.tls.yml: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-bullseye AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod . 6 | COPY go.sum . 7 | 8 | RUN go mod download 9 | COPY . . 10 | ENV GOCACHE=/root/.cache/go-build 11 | RUN --mount=type=cache,target="/root/.cache/go-build" go build -o app 12 | 13 | # handle self-signed certificates 14 | FROM ubuntu:latest AS tls-cert-gen 15 | RUN set -x \ 16 | && apt-get update \ 17 | && apt-get install -y \ 18 | openssl 19 | 20 | RUN mkdir -p /ssl/settings 21 | 22 | RUN openssl req \ 23 | -x509 -nodes -newkey rsa:2048 \ 24 | -keyout /ssl/settings/cert_private.pem \ 25 | -out /ssl/settings/cert_public.pem \ 26 | -sha256 -days 365 \ 27 | -subj "/C=EU/ST=Earth/L=Earth/O=DareDB/OU=DareDB/CN=localhost" 28 | 29 | # handle database 30 | FROM ubuntu:22.04 AS dare-db-tls 31 | 32 | RUN mkdir /app 33 | RUN mkdir -p /app/settings 34 | 35 | # set envs for database 36 | ENV DARE_TLS_ENABLED=True 37 | ENV DARE_CERT_PRIVATE=/app/settings/cert_private.pem 38 | ENV DARE_CERT_PUBLIC=/app/settings/cert_public.pem 39 | 40 | WORKDIR /app 41 | 42 | COPY auth/rbac_model.conf /app/auth/rbac_model.conf 43 | COPY auth/rbac_policy.csv /app/auth/rbac_policy.csv 44 | 45 | COPY --from=builder /app/app . 46 | 47 | COPY --from=tls-cert-gen /ssl/settings/cert_private.pem /app/settings/cert_private.pem 48 | COPY --from=tls-cert-gen /ssl/settings/cert_public.pem /app/settings/cert_public.pem 49 | 50 | EXPOSE 2605 51 | 52 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Davide Marro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dare-DB 2 | 3 | Here's the [official documentation](https://dmarro89.github.io/daredb-docs/). 4 | 5 | **Dare-DB** is a project that provides an in-memory database utilizing Redis-inspired hashtables implemented in Go package [go-redis-hashtable](https://github.com/dmarro89/go-redis-hashtable). It offers a lightweight and efficient solution for storing data in memory and accessing it through simple HTTP operations. 6 | 7 | ## Project Purpose 8 | 9 | The primary goal of this project is to offer an in-memory database that leverages hashtables for efficient data storage and retrieval. The Go implementation allows using this database as a component in other Go services or integrating it into applications that require rapid access to in-memory data. 10 | 11 | ## Running the Database 12 | 13 | ### Using `Docker` 14 | 15 | To run the database as a Docker image, ensure you have Docker installed on your system. First, navigate to the root directory of your project and execute the following command to build the Docker image: 16 | 17 | ```bash 18 | docker build -t dare-db:latest . 19 | ``` 20 | Once the image is built, you can run the database as a Docker container with the following command (*note: a configuration option ```-e DARE_HOST="0.0.0.0"``` is explicitly set to enable connections from the host machine to the database running within the Docker container*): 21 | 22 | ```bash 23 | docker run -d -p "127.0.0.1:2605:2605" -e DARE_HOST="0.0.0.0" dare-db 24 | ``` 25 | 26 | This command will start the database as a Docker container in detached mode, exposing port 2605 of the container to port ```2605``` on your localhost `127.0.0.1`. 27 | 28 | ### Using TLS Version in `Docker` 29 | 30 | Build special Docker image, which will generate certificates 31 | 32 | ```bash 33 | docker build -t dare-db-tls:latest -f Dockerfile.tls.yml . 34 | ``` 35 | 36 | Once the image is built, you can run the database as a Docker container with the following command: 37 | 38 | ```bash 39 | docker run -d -p "127.0.0.1:2605:2605" -e DARE_HOST="0.0.0.0" -e DARE_PORT=2605 -e DARE_TLS_ENABLED="True" -e DARE_CERT_PRIVATE="/app/settings/cert_private.pem" -e DARE_CERT_PUBLIC="/app/settings/cert_public.pem" dare-db-tls 40 | ``` 41 | 42 | Access API over HTTPS on https://127.0.0.1:2605 43 | 44 | 45 | ## How to Use: Core API Overview 46 | 47 | The in-memory database provides three simple HTTP endpoints to interact with stored data. By default endpoints are protected by JWT: 48 | 49 | ### POST /login 50 | 51 | Get the JWT token. For credentials check file: `config.toml`: 52 | ``` 53 | curl --insecure -X POST -u ADMIN:PASSWORD https://127.0.0.1:2605/login 54 | ``` 55 | 56 | * `--insecure` is a workaround to overcome issues for `TLS` version working with self-signed certificates 57 | * `-H "Authorization: ` is how thw JWT must be passed by, note there is no `Bearer` in the header 58 | 59 | ### GET /get/{key} 60 | 61 | This endpoint retrieves an item from the hashtable using a specific key. 62 | 63 | Example usage with cURL: 64 | 65 | ```bash 66 | curl -X GET -H "Authorization: " http://127.0.0.1:2605/get/myKey 67 | ``` 68 | 69 | ### SET /set 70 | 71 | This endpoint inserts a new item into the hashtable. The request body should contain the key and value of the new item. 72 | 73 | Example usage with cURL: 74 | 75 | ```bash 76 | curl -X POST -H "Authorization: " -d '{"myKey":"myValue"}' http://127.0.0.1:2605/set 77 | ``` 78 | 79 | ### DELETE /delete/{key} 80 | 81 | This endpoint deletes an item from the hashtable using a specific key. 82 | 83 | Example usage with cURL: 84 | 85 | ```bash 86 | curl -X DELETE -H "Authorization: " http://127.0.0.1:2605/delete/myKey 87 | ``` 88 | 89 | ## How to Use: Examples 90 | 91 | A number of examples to demonstrate, how to use the database in a Go application: 92 | 93 | * [daredb_basic.go](examples/go/daredb_basic.go) 94 | * [daredb_jwt_basic.go](examples/go/daredb_jwt_basic.go) 95 | 96 | A number of examples to demonstrate, how to use the database in a Python application: 97 | 98 | * [daredb_jwt_collection.py](examples/python/daredb_jwt_collection.py) 99 | 100 | ## OpenAPI 3.0 Specification 101 | 102 | For latest OpenAPI spec see: [openapi](openapi) 103 | 104 | ## How to Contribute 105 | 106 | All sorts of contributions to this project! Here's how you can get involved: 107 | 108 |
109 | 110 | How to Contribute: Overview 111 | 112 | #### How to Contribute: Steps 113 | 114 | * *Found a bug?* Let us know! Open an [issue](https://github.com/dmarro89/dare-db/issues) and briefly describe the problem. 115 | * *Have a great idea for a new feature?* Open an [issue](https://github.com/dmarro89/dare-db/issues) to discuss it. If you'd like to implementing it yourself, you can assign this issue to yourself and create a pull request once the code/improvement/fix is ready. 116 | * *Want to talk about something related to the project?* [Discussion threads](https://github.com/dmarro89/dare-db/discussions) are the perfect place to brainstorm ideas 117 | 118 | 119 | Here is how you could add your new ```code/improvement/fix``` with a *pull request*: 120 | 121 | 1. Fork the repository (e.g., latest changes must be in ```develop``` branch) 122 | ``` 123 | git clone -b develop https://github.com/dmarro89/dare-db 124 | ``` 125 | 2. Create a new branch for your feature. Use number of a newly created issue and keywords (e.g., ```10-implement-feature-ABC```) 126 | ``` 127 | git checkout -b 10-implement-feature-ABC 128 | ``` 129 | 3. Add changes to the branch 130 | ``` 131 | git add . 132 | ``` 133 | 4. Commit your changes 134 | ``` 135 | git commit -am 'add new feature ABC' 136 | ``` 137 | 5. Push to the branch 138 | ``` 139 | git push origin 10-implement-feature-ABC 140 | ``` 141 | 6. Open a pull request based on a new branch 142 | 7. Provide a short notice in the pull request according to the following template: 143 | + Added: ... 144 | + Changed: ... 145 | + Fixed: ... 146 | + Dependencies: ... 147 |
148 | 149 |
150 | 151 | How to Contribute: Dependencies 152 | 153 | #### How to Contribute: Dependencies 154 | 155 | * [task (a.k.a.: `taskfile`)](https://github.com/go-task/task) 156 | + Install as Go module (globally) 157 | ```bash 158 | go install github.com/go-task/task/v3/cmd/task@latest 159 | ``` 160 | * [wgo - watcher-go](https://github.com/bokwoon95/wgo) 161 | + Install as Go module (globally) 162 | ```bash 163 | go install github.com/bokwoon95/wgo@latest 164 | ``` 165 | * [Lefhook (polyglot Git hooks manager)](https://github.com/evilmartians/lefthook/tree/master) 166 | + Install as Go module (globally) 167 | ``` 168 | go install github.com/evilmartians/lefthook@latest 169 | ``` 170 |
-------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | docker: ./taskfiles/Taskfile_docker.yml 5 | 6 | tasks: 7 | 8 | default: 9 | silent: true 10 | cmds: 11 | - task --list-all 12 | 13 | build: 14 | cmds: 15 | - go build -v main.go 16 | - go build -o dare-db-app main.go 17 | 18 | mod-tidy: 19 | aliases: [gmt, tidy] 20 | silent: true 21 | cmds: 22 | - go mod tidy 23 | 24 | run: 25 | aliases: [r] 26 | env: 27 | DARE_HOST: 127.0.0.1 28 | cmds: 29 | - go run main.go 30 | 31 | run-watch: 32 | aliases: [rw] 33 | env: 34 | DARE_HOST: 127.0.0.1 35 | cmds: 36 | - wgo -xdir data go run main.go 37 | 38 | test: 39 | desc: runs all tests 40 | aliases: [t] 41 | cmds: 42 | - go test ./... -v 43 | 44 | pre-commit-run: 45 | aliases: [pcr] 46 | silent: true 47 | run: once 48 | cmds: 49 | - lefthook run pre-commit --all-files 50 | -------------------------------------------------------------------------------- /auth/authenticator.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "github.com/golang-jwt/jwt/v5" 12 | ) 13 | 14 | // constants 15 | const JWT_TIME_TO_LIVE_MINUTES int = 60 16 | 17 | type Authenticator interface { 18 | GenerateToken(string) (string, error) 19 | VerifyToken(token string) (string, error) 20 | } 21 | 22 | type JWTAutenticator struct { 23 | usersStore *UserStore 24 | jwtKey []byte 25 | } 26 | 27 | func NewJWTAutenticator() *JWTAutenticator { 28 | return &JWTAutenticator{ 29 | jwtKey: getJWTKey(), 30 | usersStore: NewUserStore(), 31 | } 32 | } 33 | 34 | func NewJWTAutenticatorWithUsers(usersStore *UserStore) *JWTAutenticator { 35 | return &JWTAutenticator{ 36 | jwtKey: getJWTKey(), 37 | usersStore: usersStore, 38 | } 39 | } 40 | 41 | type Claims struct { 42 | Username string `json:"username"` 43 | jwt.RegisteredClaims 44 | } 45 | 46 | func (jwtAuthenticator *JWTAutenticator) GenerateToken(username string) (string, error) { 47 | expirationTime := time.Now().Add(time.Duration(JWT_TIME_TO_LIVE_MINUTES) * time.Minute) 48 | claims := &Claims{ 49 | Username: username, 50 | RegisteredClaims: jwt.RegisteredClaims{ 51 | ExpiresAt: jwt.NewNumericDate(expirationTime), 52 | }, 53 | } 54 | 55 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 56 | return token.SignedString(jwtAuthenticator.jwtKey) 57 | } 58 | 59 | func (jwtAuthenticator *JWTAutenticator) VerifyToken(tokenString string) (string, error) { 60 | claims := &Claims{} 61 | 62 | token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { 63 | return jwtAuthenticator.jwtKey, nil 64 | }) 65 | 66 | if err != nil { 67 | return "", fmt.Errorf("failed to parse token: %w", err) 68 | } 69 | 70 | if !token.Valid { 71 | return "", fmt.Errorf("invalid token") 72 | } 73 | 74 | userStore := jwtAuthenticator.usersStore 75 | 76 | if userStore.tokens[claims.Username] != tokenString { 77 | return "", fmt.Errorf("invalid token") 78 | } 79 | 80 | return claims.Username, nil 81 | } 82 | 83 | var ( 84 | jwtKey []byte 85 | once sync.Once 86 | ) 87 | 88 | func getJWTKey() []byte { 89 | once.Do(func() { 90 | key := os.Getenv("JWT_SECRET_KEY") 91 | 92 | if key == "" { 93 | randomKey := make([]byte, 32) 94 | if _, err := rand.Read(randomKey); err != nil { 95 | panic("Failed to generate random key: " + err.Error()) 96 | } 97 | key = hex.EncodeToString(randomKey) 98 | } 99 | 100 | jwtKey = []byte(key) 101 | }) 102 | 103 | return jwtKey 104 | } 105 | -------------------------------------------------------------------------------- /auth/authenticator_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt/v5" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGenerateToken(t *testing.T) { 12 | jwtKey := getJWTKey() 13 | authenticator := &JWTAutenticator{ 14 | usersStore: NewUserStore(), 15 | jwtKey: jwtKey, 16 | } 17 | 18 | username := "testuser" 19 | 20 | token, err := authenticator.GenerateToken(username) 21 | assert.NoError(t, err, "Expected no error when generating token") 22 | assert.NotEmpty(t, token, "Expected a token, got an empty string") 23 | 24 | claims := &Claims{} 25 | parsedToken, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) { 26 | return jwtKey, nil 27 | }) 28 | 29 | assert.NoError(t, err, "Error parsing token") 30 | assert.True(t, parsedToken.Valid, "Generated token is not valid") 31 | assert.Equal(t, username, claims.Username, "Expected username %v, got %v", username, claims.Username) 32 | } 33 | 34 | func TestJWTAuthenticator_VerifyToken(t *testing.T) { 35 | userStore := NewUserStore() 36 | jwtKey := getJWTKey() 37 | authenticator := &JWTAutenticator{ 38 | usersStore: userStore, 39 | jwtKey: jwtKey, 40 | } 41 | 42 | username := "testuser" 43 | password := "testpassword" 44 | userStore.AddUser(username, password) 45 | tokenString, err := authenticator.GenerateToken(username) 46 | assert.NoError(t, err, "Expected no error when generating token") 47 | assert.NotEmpty(t, tokenString, "Expected a token, got an empty string") 48 | 49 | userStore.SaveToken(username, tokenString) 50 | 51 | // Test case: valid token 52 | returnedUsername, err := authenticator.VerifyToken(tokenString) 53 | assert.NoError(t, err, "Expected no error for a valid token") 54 | assert.Equal(t, username, returnedUsername, "Expected username to match") 55 | 56 | // Test case: invalid token 57 | invalidTokenString := tokenString + "invalid" 58 | _, err = authenticator.VerifyToken(invalidTokenString) 59 | assert.Error(t, err, "Expected error for an invalid token") 60 | assert.Equal(t, "failed to parse token: token signature is invalid: signature is invalid", err.Error(), "Expected 'token signature is invalid: signature is invalid' error") 61 | 62 | // Test case: expired token 63 | expiredTokenString, _ := generateExpiredToken(username) 64 | userStore.SaveToken(username, expiredTokenString) 65 | _, err = authenticator.VerifyToken(expiredTokenString) 66 | assert.Error(t, err, "Expected error for an expired token") 67 | 68 | // Test case: token not present in user store 69 | userStore.DeleteUser(username) 70 | _, err = authenticator.VerifyToken(tokenString) 71 | assert.Error(t, err, "Expected error when the token is not present in the user store") 72 | assert.Equal(t, "invalid token", err.Error(), "Expected 'invalid token' error") 73 | } 74 | 75 | // Helper function to generate an expired token 76 | func generateExpiredToken(username string) (string, error) { 77 | expirationTime := time.Now().Add(-5 * time.Minute) // Set expiration time in the past 78 | claims := &Claims{ 79 | Username: username, 80 | RegisteredClaims: jwt.RegisteredClaims{ 81 | ExpiresAt: jwt.NewNumericDate(expirationTime), 82 | }, 83 | } 84 | 85 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 86 | 87 | jwtKey := getJWTKey() 88 | return token.SignedString(jwtKey) 89 | } 90 | -------------------------------------------------------------------------------- /auth/authorizer.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/casbin/casbin" 9 | "github.com/dmarro89/dare-db/logger" 10 | ) 11 | 12 | const GUEST_USER = "guest" 13 | const GUEST_ROLE = "guest" 14 | const DEFAULT_USER = "admin" 15 | const DEFAULT_ROLE = "admin" 16 | 17 | const RBAC_CONFIG_FILE = "auth/rbac_model.conf" 18 | const RBAC_POLICY_FILE = "auth/rbac_policy.csv" 19 | 20 | type Authorizer interface { 21 | HasPermission(userID, action, asset string) bool 22 | } 23 | 24 | type User struct { 25 | Roles []string 26 | } 27 | 28 | type Users map[string]*User 29 | 30 | type CasbinAuth struct { 31 | users Users 32 | enforcer *casbin.Enforcer 33 | logger logger.Logger 34 | } 35 | 36 | func NewCasbinAuth(modelPath, policyPath string, users Users) *CasbinAuth { 37 | if users == nil { 38 | users = Users{GUEST_USER: {Roles: []string{GUEST_ROLE}}} 39 | } 40 | enforcer, err := casbin.NewEnforcerSafe(modelPath, policyPath) 41 | if err != nil { 42 | panic(fmt.Sprintf("Failed to create Casbin enforcer: %v", err)) 43 | } 44 | return &CasbinAuth{ 45 | users: users, 46 | enforcer: enforcer, 47 | logger: logger.NewDareLogger(), 48 | } 49 | } 50 | 51 | func (a *CasbinAuth) HasPermission(userID, action, asset string) bool { 52 | user, ok := a.users[userID] 53 | if !ok { 54 | a.logger.Error("Unknown user:", userID) 55 | return false 56 | } 57 | 58 | for _, role := range user.Roles { 59 | if a.enforcer.Enforce(role, asset, action) { 60 | a.logger.Info(fmt.Sprintf("User '%s' is allowed to '%s' resource '%s'", userID, action, asset)) 61 | return true 62 | } 63 | } 64 | 65 | a.logger.Info(fmt.Sprintf("User '%s' is not allowed to '%s' resource '%s'", userID, action, asset)) 66 | return false 67 | } 68 | 69 | func GetDefaultAuth() *CasbinAuth { 70 | dir, err := os.Getwd() 71 | if err != nil { 72 | panic("Failed to get current working directory: " + err.Error()) 73 | } 74 | 75 | modelPath := filepath.Join(dir, RBAC_CONFIG_FILE) 76 | policyPath := filepath.Join(dir, RBAC_POLICY_FILE) 77 | 78 | return NewCasbinAuth(modelPath, policyPath, Users{ 79 | DEFAULT_USER: {Roles: []string{DEFAULT_ROLE}}, 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /auth/authorizer_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | const RBAC_MODEL_CONTENT = `[request_definition] 11 | r = sub, obj, act 12 | 13 | [policy_definition] 14 | p = sub, obj, act 15 | 16 | [role_definition] 17 | g = _, _ 18 | 19 | [policy_effect] 20 | e = some(where (p.eft == allow)) 21 | 22 | [matchers] 23 | m = g(r.sub, p.sub) && (p.obj == "*" || keyMatch(r.obj, p.obj)) && regexMatch(r.act, p.act) 24 | ` 25 | 26 | const RBAC_POLICY = `p, role1, *, GET 27 | p, role2, *, POST 28 | 29 | g, user1, role1 30 | g, user2, role2` 31 | 32 | func TestNewCasbinAuth(t *testing.T) { 33 | modelFile, err := os.CreateTemp("", "rbac_model.conf") 34 | if err != nil { 35 | t.Fatalf("Error creating rbac model file: %v", err) 36 | } 37 | defer os.Remove(modelFile.Name()) 38 | 39 | policyFile, err := os.CreateTemp("", "rbac_policy.csv") 40 | if err != nil { 41 | t.Fatalf("Error creating rbac policy file: %v", err) 42 | } 43 | defer os.Remove(policyFile.Name()) 44 | 45 | if _, err := modelFile.Write([]byte(RBAC_MODEL_CONTENT)); err != nil { 46 | t.Fatalf("Errorwriting rbac model file: %v", err) 47 | } 48 | if err := modelFile.Close(); err != nil { 49 | t.Fatalf("Error closing rbac model file: %v", err) 50 | } 51 | 52 | if _, err := policyFile.Write([]byte(RBAC_POLICY)); err != nil { 53 | t.Fatalf("Error creating policy file: %v", err) 54 | } 55 | if err := policyFile.Close(); err != nil { 56 | t.Fatalf("Error closing policy file: %v", err) 57 | } 58 | 59 | casbinAuth := NewCasbinAuth(modelFile.Name(), policyFile.Name(), Users{ 60 | "user1": {Roles: []string{"role1"}}, 61 | "user2": {Roles: []string{"role2"}}, 62 | }) 63 | 64 | require.NotNil(t, casbinAuth) 65 | require.NotNil(t, casbinAuth.enforcer) 66 | require.NotNil(t, casbinAuth.logger) 67 | } 68 | 69 | func TestHasPermission(t *testing.T) { 70 | modelFile, err := os.CreateTemp("", "rbac_model.conf") 71 | if err != nil { 72 | t.Fatalf("Error creating rbac model file: %v", err) 73 | } 74 | defer os.Remove(modelFile.Name()) 75 | 76 | policyFile, err := os.CreateTemp("", "rbac_policy.csv") 77 | if err != nil { 78 | t.Fatalf("Error creating rbac policy file: %v", err) 79 | } 80 | defer os.Remove(policyFile.Name()) 81 | 82 | if _, err := modelFile.Write([]byte(RBAC_MODEL_CONTENT)); err != nil { 83 | t.Fatalf("Errorwriting rbac model file: %v", err) 84 | } 85 | if err := modelFile.Close(); err != nil { 86 | t.Fatalf("Error closing rbac model file: %v", err) 87 | } 88 | 89 | if _, err := policyFile.Write([]byte(RBAC_POLICY)); err != nil { 90 | t.Fatalf("Error creating policy file: %v", err) 91 | } 92 | if err := policyFile.Close(); err != nil { 93 | t.Fatalf("Error closing policy file: %v", err) 94 | } 95 | 96 | casbinAuth := NewCasbinAuth(modelFile.Name(), policyFile.Name(), Users{ 97 | "user1": {Roles: []string{"role1"}}, 98 | "user2": {Roles: []string{"role2"}}, 99 | }) 100 | 101 | // Test with user1 and GET on dare-db 102 | ok := casbinAuth.HasPermission("user1", "GET", "dare-db") 103 | require.True(t, ok) 104 | 105 | // Test with user2 and POST on dare-db 106 | ok = casbinAuth.HasPermission("user2", "POST", "dare-db") 107 | require.True(t, ok) 108 | 109 | // Test with user1 and POST on dare-db (should not have permission) 110 | ok = casbinAuth.HasPermission("user1", "POST", "dare-db") 111 | require.False(t, ok) 112 | 113 | // Test with user2 and GET on dare-db (should not have permission) 114 | ok = casbinAuth.HasPermission("user2", "GET", "dare-db") 115 | require.False(t, ok) 116 | } 117 | 118 | func TestUnknownUser(t *testing.T) { 119 | modelFile, err := os.CreateTemp("", "rbac_model.conf") 120 | if err != nil { 121 | t.Fatalf("Error creating rbac model file: %v", err) 122 | } 123 | defer os.Remove(modelFile.Name()) 124 | 125 | policyFile, err := os.CreateTemp("", "rbac_policy.csv") 126 | if err != nil { 127 | t.Fatalf("Error creating rbac policy file: %v", err) 128 | } 129 | defer os.Remove(policyFile.Name()) 130 | 131 | if _, err := modelFile.Write([]byte(RBAC_MODEL_CONTENT)); err != nil { 132 | t.Fatalf("Errorwriting rbac model file: %v", err) 133 | } 134 | if err := modelFile.Close(); err != nil { 135 | t.Fatalf("Error closing rbac model file: %v", err) 136 | } 137 | 138 | if _, err := policyFile.Write([]byte(RBAC_POLICY)); err != nil { 139 | t.Fatalf("Error creating policy file: %v", err) 140 | } 141 | if err := policyFile.Close(); err != nil { 142 | t.Fatalf("Error closing policy file: %v", err) 143 | } 144 | 145 | casbinAuth := NewCasbinAuth(modelFile.Name(), policyFile.Name(), Users{ 146 | "user1": {Roles: []string{"role1"}}, 147 | "user2": {Roles: []string{"role2"}}, 148 | }) 149 | 150 | // Test with unknown user 151 | ok := casbinAuth.HasPermission("unknown", "GET", "dare-db") 152 | require.False(t, ok) 153 | } 154 | -------------------------------------------------------------------------------- /auth/middleware.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/dmarro89/dare-db/logger" 9 | ) 10 | 11 | type Middleware interface { 12 | HandleFunc(next http.HandlerFunc) http.HandlerFunc 13 | } 14 | type DareMiddleware struct { 15 | authorizer Authorizer 16 | authenticator Authenticator 17 | logger logger.Logger 18 | } 19 | 20 | func NewCasbinMiddleware(casbinAuth Authorizer, authenticator Authenticator) Middleware { 21 | return &DareMiddleware{ 22 | authorizer: casbinAuth, 23 | authenticator: authenticator, 24 | logger: logger.NewDareLogger(), 25 | } 26 | } 27 | 28 | func (middleware *DareMiddleware) HandleFunc(next http.HandlerFunc) http.HandlerFunc { 29 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | tokenStr := r.Header.Get("Authorization") 31 | if tokenStr == "" { 32 | middleware.logger.Info("Missing authorization token") 33 | http.Error(w, "Unauthorized: missing authorization token", http.StatusUnauthorized) 34 | return 35 | } 36 | 37 | username, err := middleware.authenticator.VerifyToken(tokenStr) 38 | if err != nil { 39 | middleware.logger.Error(fmt.Sprintf("Invalid authorization token: %v", err)) 40 | http.Error(w, "Unauthorized: invalid authorization token", http.StatusUnauthorized) 41 | return 42 | } 43 | 44 | asset := middleware.extractAssetFromPath(r.URL.Path) 45 | 46 | middleware.logger.Info(fmt.Sprintf("User '%s' is requesting '%s' resource '%s'", username, r.Method, asset)) 47 | if !middleware.authorizer.HasPermission(username, r.Method, asset) { 48 | middleware.logger.Info(fmt.Sprintf("User '%s' is not allowed to '%s' resource '%s'", username, r.Method, asset)) 49 | http.Error(w, "Forbidden: you do not have permission to access this resource", http.StatusForbidden) 50 | return 51 | } 52 | 53 | next(w, r) 54 | }) 55 | } 56 | 57 | func (middleware *DareMiddleware) extractAssetFromPath(path string) string { 58 | if strings.HasPrefix(path, "/get/") { 59 | return strings.TrimPrefix(path, "/get/") 60 | } 61 | if strings.HasPrefix(path, "/delete/") { 62 | return strings.TrimPrefix(path, "/delete/") 63 | } 64 | if path == "/set" { 65 | return "set" 66 | } 67 | return "dare-db" 68 | } 69 | -------------------------------------------------------------------------------- /auth/middleware_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "testing" 8 | 9 | "github.com/dmarro89/dare-db/logger" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestMiddleware(t *testing.T) { 15 | modelFile, err := os.CreateTemp("", "rbac_model.conf") 16 | if err != nil { 17 | t.Fatalf("Error creating rbac model file: %v", err) 18 | } 19 | defer os.Remove(modelFile.Name()) 20 | 21 | policyFile, err := os.CreateTemp("", "rbac_policy.csv") 22 | if err != nil { 23 | t.Fatalf("Error creating rbac policy file: %v", err) 24 | } 25 | defer os.Remove(policyFile.Name()) 26 | 27 | if _, err := modelFile.Write([]byte(RBAC_MODEL_CONTENT)); err != nil { 28 | t.Fatalf("Errorwriting rbac model file: %v", err) 29 | } 30 | if err := modelFile.Close(); err != nil { 31 | t.Fatalf("Error closing rbac model file: %v", err) 32 | } 33 | 34 | if _, err := policyFile.Write([]byte(RBAC_POLICY)); err != nil { 35 | t.Fatalf("Error creating policy file: %v", err) 36 | } 37 | if err := policyFile.Close(); err != nil { 38 | t.Fatalf("Error closing policy file: %v", err) 39 | } 40 | 41 | casbinAuth := NewCasbinAuth(modelFile.Name(), policyFile.Name(), Users{ 42 | "user1": {Roles: []string{"role1"}}, 43 | "user2": {Roles: []string{"role2"}}, 44 | }) 45 | 46 | userStore := NewUserStore() 47 | authenticator := &JWTAutenticator{ 48 | usersStore: userStore, 49 | } 50 | 51 | middleware := &DareMiddleware{ 52 | authorizer: casbinAuth, 53 | authenticator: authenticator, 54 | logger: logger.NewDareLogger(), 55 | } 56 | 57 | handler := middleware.HandleFunc(func(w http.ResponseWriter, r *http.Request) { 58 | w.WriteHeader(http.StatusOK) 59 | }) 60 | 61 | // Test case where user1 is authorized for GET 62 | req, err := http.NewRequest("GET", "/some-path", nil) 63 | require.NoError(t, err) 64 | 65 | token, err := middleware.authenticator.GenerateToken("user1") 66 | require.NoError(t, err) 67 | assert.NotNil(t, token) 68 | userStore.SaveToken("user1", token) 69 | req.Header.Set("Authorization", token) 70 | 71 | rr := httptest.NewRecorder() 72 | handler.ServeHTTP(rr, req) 73 | 74 | require.Equal(t, http.StatusOK, rr.Code) 75 | 76 | // Test case where user1 is not authorized for POST 77 | req, err = http.NewRequest("POST", "/some-path", nil) 78 | require.NoError(t, err) 79 | req.Header.Set("Authorization", token) 80 | 81 | rr = httptest.NewRecorder() 82 | handler.ServeHTTP(rr, req) 83 | 84 | require.Equal(t, http.StatusForbidden, rr.Code) 85 | 86 | // Test case where user2 is authorized for POST 87 | req, err = http.NewRequest("POST", "/some-path", nil) 88 | require.NoError(t, err) 89 | token, err = middleware.authenticator.GenerateToken("user2") 90 | assert.Nil(t, err) 91 | userStore.SaveToken("user2", token) 92 | req.Header.Set("Authorization", token) 93 | 94 | rr = httptest.NewRecorder() 95 | handler.ServeHTTP(rr, req) 96 | 97 | require.Equal(t, http.StatusOK, rr.Code) 98 | 99 | // Test case where user2 is not authorized for GET 100 | req, err = http.NewRequest("GET", "/some-path", nil) 101 | require.NoError(t, err) 102 | req.Header.Set("Authorization", token) 103 | 104 | rr = httptest.NewRecorder() 105 | handler.ServeHTTP(rr, req) 106 | 107 | require.Equal(t, http.StatusForbidden, rr.Code) 108 | 109 | // Test case where credentials are missing 110 | req, err = http.NewRequest("GET", "/some-path", nil) 111 | require.NoError(t, err) 112 | 113 | rr = httptest.NewRecorder() 114 | handler.ServeHTTP(rr, req) 115 | 116 | require.Equal(t, http.StatusUnauthorized, rr.Code) 117 | } 118 | -------------------------------------------------------------------------------- /auth/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && (p.obj == "*" || keyMatch(r.obj, p.obj)) && regexMatch(r.act, p.act) -------------------------------------------------------------------------------- /auth/rbac_policy.csv: -------------------------------------------------------------------------------- 1 | p, admin, *, GET 2 | p, admin, *, POST 3 | p, admin, *, PUT 4 | p, admin, *, DELETE -------------------------------------------------------------------------------- /auth/users.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | type UserStore struct { 9 | usersMu sync.RWMutex 10 | tokenMu sync.RWMutex 11 | users map[string]string 12 | tokens map[string]string 13 | } 14 | 15 | func NewUserStore() *UserStore { 16 | return &UserStore{ 17 | users: make(map[string]string), 18 | tokens: make(map[string]string), 19 | usersMu: sync.RWMutex{}, 20 | tokenMu: sync.RWMutex{}, 21 | } 22 | } 23 | 24 | func (store *UserStore) AddUser(username, password string) error { 25 | store.usersMu.Lock() 26 | defer store.usersMu.Unlock() 27 | 28 | if _, exists := store.users[username]; exists { 29 | return errors.New("user already exists") 30 | } 31 | 32 | store.users[username] = password 33 | return nil 34 | } 35 | 36 | func (store *UserStore) DeleteUser(username string) error { 37 | store.usersMu.Lock() 38 | defer store.usersMu.Unlock() 39 | 40 | if _, exists := store.users[username]; !exists { 41 | return errors.New("user does not exist") 42 | } 43 | 44 | delete(store.users, username) 45 | store.DeleteToken(username) 46 | return nil 47 | } 48 | 49 | func (store *UserStore) UpdatePassword(username, newPassword string) error { 50 | store.usersMu.Lock() 51 | defer store.usersMu.Unlock() 52 | 53 | if _, exists := store.users[username]; !exists { 54 | return errors.New("user does not exist") 55 | } 56 | 57 | store.users[username] = newPassword 58 | return nil 59 | } 60 | 61 | func (store *UserStore) ValidateCredentials(username, password string) bool { 62 | store.usersMu.RLock() 63 | defer store.usersMu.RUnlock() 64 | 65 | storedPassword, exists := store.users[username] 66 | return exists && storedPassword == password 67 | } 68 | 69 | func (store *UserStore) SaveToken(username, token string) { 70 | store.tokenMu.Lock() 71 | defer store.tokenMu.Unlock() 72 | store.tokens[username] = token 73 | } 74 | 75 | func (store *UserStore) DeleteToken(username string) error { 76 | store.tokenMu.Lock() 77 | defer store.tokenMu.Unlock() 78 | 79 | if _, exists := store.tokens[username]; !exists { 80 | return errors.New("user does not exist") 81 | } 82 | 83 | delete(store.tokens, username) 84 | 85 | return nil 86 | } 87 | 88 | func (store *UserStore) ValidateToken(username, token string) bool { 89 | store.usersMu.RLock() 90 | defer store.usersMu.RUnlock() 91 | storedToken, exists := store.tokens[username] 92 | return exists && storedToken == token 93 | } 94 | -------------------------------------------------------------------------------- /auth/users_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUserStore_AddUser(t *testing.T) { 10 | store := NewUserStore() 11 | 12 | // Test adding a new user 13 | err := store.AddUser("user1", "password1") 14 | assert.NoError(t, err, "Expected no error when adding a new user") 15 | 16 | // Test adding a duplicate user 17 | err = store.AddUser("user1", "password2") 18 | assert.Error(t, err, "Expected error when adding a duplicate user") 19 | assert.Equal(t, "user already exists", err.Error()) 20 | } 21 | 22 | func TestUserStore_DeleteUser(t *testing.T) { 23 | store := NewUserStore() 24 | 25 | // Add a user to delete 26 | store.AddUser("user1", "password1") 27 | 28 | // Test deleting an existing user 29 | err := store.DeleteUser("user1") 30 | assert.NoError(t, err, "Expected no error when deleting an existing user") 31 | 32 | // Test deleting a non-existing user 33 | err = store.DeleteUser("user2") 34 | assert.Error(t, err, "Expected error when deleting a non-existing user") 35 | assert.Equal(t, "user does not exist", err.Error()) 36 | } 37 | 38 | func TestUserStore_UpdatePassword(t *testing.T) { 39 | store := NewUserStore() 40 | 41 | // Add a user to update 42 | store.AddUser("user1", "password1") 43 | 44 | // Test updating the password for an existing user 45 | err := store.UpdatePassword("user1", "newpassword") 46 | assert.NoError(t, err, "Expected no error when updating password for an existing user") 47 | 48 | // Verify the password was updated 49 | assert.True(t, store.ValidateCredentials("user1", "newpassword"), "Expected the updated password to be valid") 50 | 51 | // Test updating the password for a non-existing user 52 | err = store.UpdatePassword("user2", "newpassword") 53 | assert.Error(t, err, "Expected error when updating password for a non-existing user") 54 | assert.Equal(t, "user does not exist", err.Error()) 55 | } 56 | 57 | func TestUserStore_ValidateCredentials(t *testing.T) { 58 | store := NewUserStore() 59 | 60 | // Add a user to validate 61 | store.AddUser("user1", "password1") 62 | 63 | // Test validating correct credentials 64 | valid := store.ValidateCredentials("user1", "password1") 65 | assert.True(t, valid, "Expected credentials to be valid") 66 | 67 | // Test validating incorrect password 68 | valid = store.ValidateCredentials("user1", "wrongpassword") 69 | assert.False(t, valid, "Expected credentials to be invalid") 70 | 71 | // Test validating non-existing user 72 | valid = store.ValidateCredentials("user2", "password1") 73 | assert.False(t, valid, "Expected credentials to be invalid for non-existing user") 74 | } 75 | 76 | func TestUserStore_SaveToken(t *testing.T) { 77 | store := NewUserStore() 78 | 79 | // Save a token for a user 80 | store.SaveToken("user1", "token123") 81 | 82 | // Test that the token was saved correctly 83 | assert.True(t, store.ValidateToken("user1", "token123"), "Expected the token to be valid") 84 | } 85 | 86 | func TestUserStore_ValidateToken(t *testing.T) { 87 | store := NewUserStore() 88 | 89 | // Save a token for a user 90 | store.SaveToken("user1", "token123") 91 | 92 | // Test validating correct token 93 | valid := store.ValidateToken("user1", "token123") 94 | assert.True(t, valid, "Expected the token to be valid") 95 | 96 | // Test validating incorrect token 97 | valid = store.ValidateToken("user1", "wrongtoken") 98 | assert.False(t, valid, "Expected the token to be invalid") 99 | 100 | // Test validating token for non-existing user 101 | valid = store.ValidateToken("user2", "token123") 102 | assert.False(t, valid, "Expected the token to be invalid for non-existing user") 103 | } 104 | -------------------------------------------------------------------------------- /database/collection.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | const DEFAULT_COLLECTION = "default" 8 | 9 | type CollectionManager struct { 10 | collections map[string]*Database 11 | mu sync.RWMutex 12 | } 13 | 14 | func NewCollectionManager() *CollectionManager { 15 | return &CollectionManager{ 16 | collections: make(map[string]*Database), 17 | } 18 | } 19 | 20 | func (cm *CollectionManager) AddCollection(name string) { 21 | cm.mu.Lock() 22 | defer cm.mu.Unlock() 23 | cm.collections[name] = NewDatabase() 24 | } 25 | 26 | func (cm *CollectionManager) GetCollection(name string) (*Database, bool) { 27 | cm.mu.RLock() 28 | defer cm.mu.RUnlock() 29 | db, exists := cm.collections[name] 30 | return db, exists 31 | } 32 | 33 | func (cm *CollectionManager) GetDefaultCollection() *Database { 34 | cm.mu.RLock() 35 | defer cm.mu.RUnlock() 36 | db := cm.collections[DEFAULT_COLLECTION] 37 | return db 38 | } 39 | 40 | func (cm *CollectionManager) GetCollectionNames() []string { 41 | cm.mu.RLock() 42 | defer cm.mu.RUnlock() 43 | 44 | var collectionNames []string 45 | for key := range cm.collections { 46 | collectionNames = append(collectionNames, key) 47 | } 48 | return collectionNames 49 | } 50 | 51 | func (cm *CollectionManager) RemoveCollection(name string) { 52 | cm.mu.Lock() 53 | defer cm.mu.Unlock() 54 | delete(cm.collections, name) 55 | } 56 | -------------------------------------------------------------------------------- /database/collection_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewCollectionManager(t *testing.T) { 10 | cm := NewCollectionManager() 11 | 12 | // Assert that cm is not nil 13 | assert.NotNil(t, cm, "Expected NewCollectionManager to return a non-nil value") 14 | 15 | // Assert that the collections map is initially empty 16 | assert.Equal(t, 0, len(cm.collections), "Expected initial collections map to be empty") 17 | } 18 | 19 | func TestAddCollection(t *testing.T) { 20 | cm := NewCollectionManager() 21 | cm.AddCollection("test-collection") 22 | 23 | // Assert that the collections map contains exactly one collection 24 | assert.Equal(t, 1, len(cm.collections), "Expected collections map to have 1 collection") 25 | 26 | // Assert that 'test-collection' was added to the collections map 27 | _, exists := cm.collections["test-collection"] 28 | assert.True(t, exists, "Expected collection 'test-collection' to be added") 29 | } 30 | 31 | func TestGetCollection(t *testing.T) { 32 | cm := NewCollectionManager() 33 | cm.AddCollection("test-collection") 34 | 35 | // Assert that the collection exists and is not nil 36 | db, exists := cm.GetCollection("test-collection") 37 | assert.True(t, exists, "Expected collection 'test-collection' to exist") 38 | assert.NotNil(t, db, "Expected non-nil database for collection 'test-collection'") 39 | } 40 | 41 | func TestGetCollectionNotExist(t *testing.T) { 42 | cm := NewCollectionManager() 43 | 44 | // Assert that a nonexistent collection returns false 45 | _, exists := cm.GetCollection("nonexistent-collection") 46 | assert.False(t, exists, "Expected nonexistent collection to return false") 47 | } 48 | 49 | func TestGetDefaultCollection(t *testing.T) { 50 | cm := NewCollectionManager() 51 | cm.AddCollection(DEFAULT_COLLECTION) 52 | 53 | // Assert that the default collection is not nil 54 | db := cm.GetDefaultCollection() 55 | assert.NotNil(t, db, "Expected non-nil database for default collection") 56 | } 57 | 58 | func TestGetCollectionNames(t *testing.T) { 59 | cm := NewCollectionManager() 60 | cm.AddCollection("collection1") 61 | cm.AddCollection("collection2") 62 | 63 | // Retrieve collection names and assert they match the expected names 64 | names := cm.GetCollectionNames() 65 | expectedNames := []string{"collection1", "collection2"} 66 | 67 | assert.ElementsMatch(t, expectedNames, names, "Expected all collection names to be returned") 68 | } 69 | 70 | func TestRemoveCollection(t *testing.T) { 71 | cm := NewCollectionManager() 72 | cm.AddCollection("collection-to-remove") 73 | 74 | // Remove the collection and assert the collections map is empty 75 | cm.RemoveCollection("collection-to-remove") 76 | assert.Equal(t, 0, len(cm.collections), "Expected collections map to be empty") 77 | 78 | // Assert that the collection no longer exists 79 | _, exists := cm.collections["collection-to-remove"] 80 | assert.False(t, exists, "Expected collection 'collection-to-remove' to be removed") 81 | } 82 | -------------------------------------------------------------------------------- /database/db.go: -------------------------------------------------------------------------------- 1 | // database.go 2 | 3 | package database 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/dmarro89/go-redis-hashtable/structure" 9 | ) 10 | 11 | type Database struct { 12 | dict structure.IDict 13 | mu sync.RWMutex 14 | } 15 | 16 | func NewDatabase() *Database { 17 | return &Database{ 18 | dict: structure.NewSipHashDict(), 19 | } 20 | } 21 | 22 | func (db *Database) Get(key string) string { 23 | db.mu.RLock() 24 | defer db.mu.RUnlock() 25 | 26 | return db.dict.Get(key) 27 | } 28 | 29 | func (db *Database) GetAllItems() map[string]string { 30 | db.mu.RLock() 31 | defer db.mu.RUnlock() 32 | 33 | return db.dict.GetAllItems() 34 | } 35 | 36 | func (db *Database) Set(key string, value string) error { 37 | db.mu.Lock() 38 | defer db.mu.Unlock() 39 | 40 | return db.dict.Set(key, value) 41 | } 42 | 43 | func (db *Database) Delete(key string) error { 44 | db.mu.Lock() 45 | defer db.mu.Unlock() 46 | 47 | return db.dict.Delete(key) 48 | } 49 | -------------------------------------------------------------------------------- /database/db_test.go: -------------------------------------------------------------------------------- 1 | // database_test.go 2 | 3 | package database 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestDatabase_SetAndGet(t *testing.T) { 13 | db := NewDatabase() 14 | 15 | key := "testKey" 16 | value := "testValue" 17 | 18 | err := db.Set(key, value) 19 | if err != nil { 20 | t.Errorf("Error setting value: %v", err) 21 | } 22 | 23 | result := db.Get(key) 24 | if result != value { 25 | t.Errorf("Expected %v, got %v", value, result) 26 | } 27 | } 28 | 29 | func TestDatabase_GetAllItems(t *testing.T) { 30 | db := NewDatabase() 31 | 32 | key := "testKey" 33 | value := "testValue" 34 | 35 | for i := 0; i < 10; i++ { 36 | db.Set(fmt.Sprintf("%s%d", key, i), fmt.Sprintf("%s%d", value, i)) 37 | } 38 | 39 | result := db.GetAllItems() 40 | assert.Equal(t, 10, len(result)) 41 | 42 | for i := 0; i < 10; i++ { 43 | assert.Equal(t, fmt.Sprintf("%s%d", value, i), result[fmt.Sprintf("%s%d", key, i)]) 44 | } 45 | } 46 | 47 | func TestDatabase_SetAndGetConcurrently(t *testing.T) { 48 | db := NewDatabase() 49 | 50 | key := "testKey" 51 | value := "testValue" 52 | 53 | go func() { 54 | err := db.Set(key, value) 55 | if err != nil { 56 | t.Errorf("Error setting value: %v", err) 57 | } 58 | }() 59 | 60 | go func() { 61 | result := db.Get(key) 62 | if result != value { 63 | t.Errorf("Expected %v, got %v", value, result) 64 | } 65 | }() 66 | 67 | t.Logf("Waiting for goroutines to finish...") 68 | } 69 | 70 | func TestDatabase_Delete(t *testing.T) { 71 | db := NewDatabase() 72 | 73 | key := "testKey" 74 | value := "testValue" 75 | 76 | err := db.Set(key, value) 77 | if err != nil { 78 | t.Errorf("Error setting value: %v", err) 79 | } 80 | 81 | err = db.Delete(key) 82 | if err != nil { 83 | t.Errorf("Error deleting key: %v", err) 84 | } 85 | 86 | result := db.Get(key) 87 | if result != "" { 88 | t.Errorf("Expected nil after deletion, got %v", result) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/go/.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | scripts/* 3 | # test files 4 | test_file_* 5 | 6 | # extentions 7 | *.csv 8 | *.txt 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | cover/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | 68 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 69 | __pypackages__/ 70 | 71 | 72 | # Environments 73 | .env 74 | .venv 75 | env/ 76 | venv/ 77 | ENV/ 78 | env.bak/ 79 | venv.bak/ 80 | 81 | # If you prefer the allow list template instead of the deny list, see community template: 82 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 83 | # 84 | # Binaries for programs and plugins 85 | *.exe 86 | *.exe~ 87 | *.dll 88 | *.so 89 | *.dylib 90 | 91 | # Test binary, built with `go test -c` 92 | *.test 93 | 94 | # Output of the go coverage tool, specifically when used with LiteIDE 95 | *.out 96 | 97 | # Dependency directories (remove the comment below to include it) 98 | # vendor/ 99 | 100 | # Go workspace file 101 | go.work 102 | .vscode 103 | 104 | # Go workspace file 105 | go.work 106 | go.work.sum 107 | 108 | # Temporary files created by the Go build process 109 | tmp/ 110 | pkg/ 111 | temp/ 112 | resources/ 113 | # Cache directories created by Go tools 114 | 115 | # CGO (foreign function interface) cache 116 | _cgo_defunct.c 117 | _cgo_export.h 118 | 119 | # Build cache 120 | build/ 121 | 122 | # DareDB config file 123 | config.toml 124 | 125 | # DareDB directories 126 | settings/* 127 | data/* 128 | 129 | # log files 130 | *.log 131 | 132 | # special binaries 133 | dare-db-app -------------------------------------------------------------------------------- /examples/go/README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | Example in Go to work with DareDB 4 | 5 | ## Setup 6 | 7 | Create `.env` file with the following content (modify credentials) 8 | ``` 9 | BASE_URL=http://127.0.0.1:2605 10 | #BASE_URL=https://127.0.0.1:2605 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | 14 | ``` 15 | 16 | 17 | ## Example 18 | 19 | * Making simple request to the database using JWT: `daredb_basic_jwt.go` 20 | ``` 21 | go run daredb_basic_jwt.go 22 | ``` -------------------------------------------------------------------------------- /examples/go/daredb_basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | // const DAREDB_BASE_URL = "https://127.0.0.1:2605" 11 | const DAREDB_BASE_URL = "http://127.0.0.1:2605" 12 | 13 | // Example JWT token (replace with a real token) 14 | const JWT_TOKEN = "YOUR-JWT-TOKEN-HERE" 15 | 16 | func addKeyWithPost() { 17 | log.Println("Making POST request to a database") 18 | url := fmt.Sprintf("%s/set", DAREDB_BASE_URL) 19 | log.Printf("URL: %s\n", url) 20 | 21 | reqSet, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(`{"myKey":"myValue"}`))) 22 | if err != nil { 23 | log.Println("Error creating set request:", err) 24 | return 25 | } 26 | 27 | reqSet.Header.Set("Content-Type", "application/json") 28 | reqSet.Header.Set("Authorization", JWT_TOKEN) 29 | resp, err := http.DefaultClient.Do(reqSet) 30 | defer resp.Body.Close() 31 | if err != nil { 32 | log.Println("Error while inserting item:", err) 33 | return 34 | } 35 | 36 | body := new(bytes.Buffer) 37 | body.ReadFrom(resp.Body) 38 | log.Printf("HTTP Code: %d\n", resp.StatusCode) 39 | log.Printf("content: %s\n\n", body.String()) 40 | } 41 | 42 | func retrieveKeyWithGet() { 43 | log.Println("Making GET request to a database") 44 | keyToRetrieve := "myKey" 45 | url := fmt.Sprintf("%s/get/%s", DAREDB_BASE_URL, keyToRetrieve) 46 | log.Printf("URL: %s\n", url) 47 | 48 | reqGet, err := http.NewRequest("GET", url, nil) 49 | if err != nil { 50 | log.Println("Error creating get request:", err) 51 | return 52 | } 53 | reqGet.Header.Set("Authorization", JWT_TOKEN) 54 | resp, err := http.DefaultClient.Do(reqGet) 55 | if err != nil { 56 | log.Println("Error while retrieving item:", err) 57 | return 58 | } 59 | defer resp.Body.Close() 60 | 61 | body := new(bytes.Buffer) 62 | body.ReadFrom(resp.Body) 63 | log.Printf("HTTP Code: %d\n", resp.StatusCode) 64 | log.Printf("content: %s\n\n", body.String()) 65 | } 66 | 67 | func deleteKeyWithDelete() { 68 | log.Println("Making DELETE request to a database") 69 | keyToRetrieve := "myKey" 70 | url := fmt.Sprintf("%s/delete/%s", DAREDB_BASE_URL, keyToRetrieve) 71 | log.Printf("URL: %s\n", url) 72 | 73 | reqGet, err := http.NewRequest("DELETE", url, nil) 74 | if err != nil { 75 | log.Println("Error creating get request:", err) 76 | return 77 | } 78 | reqGet.Header.Set("Authorization", JWT_TOKEN) 79 | resp, err := http.DefaultClient.Do(reqGet) 80 | if err != nil { 81 | log.Println("Error while retrieving item:", err) 82 | return 83 | } 84 | defer resp.Body.Close() 85 | 86 | body := new(bytes.Buffer) 87 | body.ReadFrom(resp.Body) 88 | log.Printf("HTTP Code: %d\n", resp.StatusCode) 89 | log.Printf("content: %s\n\n", body.String()) 90 | } 91 | 92 | func main() { 93 | addKeyWithPost() 94 | retrieveKeyWithGet() 95 | deleteKeyWithDelete() 96 | } 97 | -------------------------------------------------------------------------------- /examples/go/daredb_jwt_basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | 12 | "github.com/ilyakaznacheev/cleanenv" 13 | ) 14 | 15 | type Config struct { 16 | BaseURL string `env:"BASE_URL" env-default:"https://127.0.0.1:2605"` 17 | Username string `env:"DB_USERNAME" env-default:"admin"` 18 | Password string `env:"DB_PASSWORD" env-default:"password"` 19 | } 20 | 21 | type DareDBPyClientBase struct { 22 | Username string 23 | Password string 24 | BaseURL string 25 | JWTToken string 26 | AuthURL string 27 | HTTPClient *http.Client 28 | } 29 | 30 | func loadConfig() (*Config, error) { 31 | var cfg Config 32 | err := cleanenv.ReadConfig(".env", &cfg) 33 | if err != nil { 34 | return nil, fmt.Errorf("error loading config: %w", err) 35 | } 36 | return &cfg, nil 37 | } 38 | 39 | func NewDareDBPyClientBase(username, password, baseURL string) *DareDBPyClientBase { 40 | client := &DareDBPyClientBase{ 41 | Username: username, 42 | Password: password, 43 | BaseURL: baseURL, 44 | HTTPClient: &http.Client{Transport: &http.Transport{ 45 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Be cautious using this in production 46 | }}, 47 | } 48 | 49 | client.AuthURL = client.BaseURL + "/login" 50 | client.JWTToken = client.getJWTToken() 51 | log.Printf("JWT token: %s", client.JWTToken) 52 | 53 | return client 54 | } 55 | 56 | func (c *DareDBPyClientBase) getJWTToken() string { 57 | 58 | if c.JWTToken != "" { 59 | return c.JWTToken 60 | } 61 | 62 | log.Printf("URL to get JWT token: %s", c.AuthURL) 63 | 64 | req, err := http.NewRequest("POST", c.AuthURL, nil) 65 | if err != nil { 66 | log.Fatalf("Error creating request: %v", err) 67 | return "" 68 | } 69 | 70 | req.SetBasicAuth(c.Username, c.Password) 71 | 72 | resp, err := c.HTTPClient.Do(req) 73 | if err != nil { 74 | log.Fatalf("Error sending request: %v", err) 75 | return "" 76 | } 77 | defer resp.Body.Close() 78 | 79 | if resp.StatusCode != http.StatusOK { 80 | log.Fatalf("Error getting JWT token: status code %d", resp.StatusCode) 81 | return "" 82 | 83 | } 84 | 85 | var tokenData map[string]string 86 | err = json.NewDecoder(resp.Body).Decode(&tokenData) 87 | if err != nil { 88 | log.Fatalf("Error decoding response: %v", err) 89 | return "" 90 | } 91 | 92 | c.JWTToken = tokenData["token"] 93 | 94 | return c.JWTToken 95 | } 96 | 97 | func (c *DareDBPyClientBase) buildHeadersWithJWT(jwtToken string) map[string]string { 98 | token := jwtToken 99 | if token == "" { 100 | token = c.JWTToken 101 | } 102 | return map[string]string{ 103 | "Authorization": token, 104 | "Content-Type": "application/json", 105 | } 106 | } 107 | 108 | func (c *DareDBPyClientBase) sendPostWithJWT(urlStr string, data map[string]interface{}) (*http.Response, error) { 109 | headers := c.buildHeadersWithJWT("") 110 | jsonData, err := json.Marshal(data) 111 | if err != nil { 112 | return nil, fmt.Errorf("error marshaling JSON: %v", err) 113 | } 114 | 115 | req, err := http.NewRequest("POST", urlStr, bytes.NewBuffer(jsonData)) 116 | if err != nil { 117 | return nil, fmt.Errorf("error creating request: %v", err) 118 | } 119 | 120 | for key, value := range headers { 121 | req.Header.Set(key, value) 122 | } 123 | 124 | resp, err := c.HTTPClient.Do(req) 125 | if err != nil { 126 | return nil, fmt.Errorf("error sending request: %v", err) 127 | } 128 | 129 | return resp, nil 130 | } 131 | 132 | func (c *DareDBPyClientBase) sendGetWithJWT(urlStr string) (*http.Response, error) { 133 | headers := c.buildHeadersWithJWT("") 134 | 135 | req, err := http.NewRequest("GET", urlStr, nil) 136 | if err != nil { 137 | return nil, fmt.Errorf("error creating request: %v", err) 138 | } 139 | 140 | for key, value := range headers { 141 | req.Header.Set(key, value) 142 | } 143 | 144 | resp, err := c.HTTPClient.Do(req) 145 | if err != nil { 146 | return nil, fmt.Errorf("error sending request: %v", err) 147 | } 148 | 149 | return resp, nil 150 | } 151 | 152 | func (c *DareDBPyClientBase) sendDeleteWithJWT(urlStr string) (*http.Response, error) { 153 | headers := c.buildHeadersWithJWT("") 154 | 155 | req, err := http.NewRequest("DELETE", urlStr, nil) 156 | if err != nil { 157 | return nil, fmt.Errorf("error creating request: %v", err) 158 | } 159 | 160 | for key, value := range headers { 161 | req.Header.Set(key, value) 162 | } 163 | 164 | resp, err := c.HTTPClient.Do(req) 165 | if err != nil { 166 | return nil, fmt.Errorf("error sending request: %v", err) 167 | } 168 | 169 | return resp, nil 170 | } 171 | 172 | func (c *DareDBPyClientBase) logResponse(resp *http.Response) { 173 | body := new(bytes.Buffer) 174 | body.ReadFrom(resp.Body) 175 | log.Printf("HTTP Code: %d; content: %s", resp.StatusCode, body.String()) 176 | } 177 | 178 | func main() { 179 | 180 | cfg, err := loadConfig() 181 | if err != nil { 182 | log.Fatalf("Error loading config: %v", err) 183 | } 184 | 185 | dareDBClient := NewDareDBPyClientBase(cfg.Username, cfg.Password, cfg.BaseURL) 186 | 187 | exampleURL, _ := url.Parse(dareDBClient.BaseURL + "/set") 188 | exampleData := map[string]interface{}{"keyToSave": "valueToSave"} 189 | log.Printf("URL to set value: %s\n", exampleURL) 190 | 191 | postResp, postErr := dareDBClient.sendPostWithJWT(exampleURL.String(), exampleData) 192 | defer postResp.Body.Close() 193 | 194 | if postErr != nil { 195 | log.Printf("POST error: %v", postErr) 196 | } else { 197 | log.Println("POST request results ->") 198 | dareDBClient.logResponse(postResp) 199 | } 200 | 201 | exampleURLGet, _ := url.Parse(dareDBClient.BaseURL + "/get/" + "keyToSave") 202 | getResp, getErr := dareDBClient.sendGetWithJWT(exampleURLGet.String()) 203 | defer getResp.Body.Close() 204 | if getErr != nil { 205 | log.Printf("GET error: %v", getErr) 206 | } else { 207 | log.Println("GET request results ->") 208 | dareDBClient.logResponse(getResp) 209 | 210 | } 211 | exampleURLDelete, _ := url.Parse(dareDBClient.BaseURL + "/delete/" + "keyToSave") 212 | deleteResp, deleteErr := dareDBClient.sendDeleteWithJWT(exampleURLDelete.String()) 213 | defer deleteResp.Body.Close() 214 | 215 | if deleteErr != nil { 216 | log.Printf("DELETE error: %v", deleteErr) 217 | } else { 218 | log.Println("DELETE request results ->") 219 | dareDBClient.logResponse(deleteResp) 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /examples/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dmarro89/dare-db/examples/go 2 | 3 | go 1.23.4 4 | 5 | require github.com/ilyakaznacheev/cleanenv v1.5.0 6 | 7 | require ( 8 | github.com/BurntSushi/toml v1.2.1 // indirect 9 | github.com/joho/godotenv v1.5.1 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /examples/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= 4 | github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= 5 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 6 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= 12 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= 13 | -------------------------------------------------------------------------------- /examples/python/.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | scripts/* 3 | # test files 4 | test_file_* 5 | 6 | # extentions 7 | *.csv 8 | *.txt 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | cover/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | 68 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 69 | __pypackages__/ 70 | 71 | 72 | # Environments 73 | .env 74 | .venv 75 | env/ 76 | venv/ 77 | ENV/ 78 | env.bak/ 79 | venv.bak/ 80 | -------------------------------------------------------------------------------- /examples/python/daredb_jwt_collection.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import uuid 5 | 6 | import urllib3 7 | from dotenv import load_dotenv 8 | from requests import delete, get, post 9 | from requests.auth import HTTPBasicAuth 10 | from requests.exceptions import RequestException 11 | 12 | urllib3.disable_warnings() 13 | logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR) 14 | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 15 | # logging.basicConfig(level=logging.DEBUG, format="%(name)s - %(levelname)s - %(message)s") 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | load_dotenv() 20 | 21 | 22 | def load_credentials(): 23 | """ 24 | Loads base URL, username, and password from environment variables. 25 | """ 26 | base_url = os.getenv("BASE_URL") or "https://127.0.0.1:2605" 27 | username = os.getenv("DB_USERNAME") or "admin" 28 | password = os.getenv("DB_PASSWORD") or "password" 29 | return base_url, username, password 30 | 31 | 32 | class DareDBPyClientBase: 33 | 34 | def __init__(self, username: str, password: str, base_url: str): 35 | self.username = username 36 | self.password = password 37 | self.base_url = base_url 38 | self.jwt_token = None 39 | self.get_jwt_token() 40 | 41 | def get_jwt_token(self): 42 | """Retrieves JWT token by performing a login with username and password.""" 43 | 44 | self.auth_url = f"{self.base_url}/login" 45 | logger.info(f"URL to get JWT token: {self.auth_url}") 46 | 47 | if not self.jwt_token: 48 | try: 49 | response = post(self.auth_url, auth=HTTPBasicAuth(self.username, self.password), verify=False) 50 | response.raise_for_status() 51 | except RequestException as e: 52 | logger.error(f"Error getting JWT token: {e}") 53 | 54 | raise 55 | logger.debug(f"response.content: {response.content}") 56 | token_data = response.json() 57 | self.jwt_token = token_data.get("token") 58 | 59 | return self.jwt_token 60 | 61 | def build_headers_with_jwt(self, jwt_token=None): 62 | """Constructs the header dictionary with authorization token.""" 63 | token = jwt_token or self.jwt_token 64 | headers = {"Authorization": f"{token}", "Content-Type": "application/json"} 65 | return headers 66 | 67 | def send_post_with_jwt(self, url: str, data: dict = None): 68 | """Sends a POST request with provided URL, data, and JWT headers.""" 69 | headers = self.build_headers_with_jwt() 70 | try: 71 | response = post(url, headers=headers, data=json.dumps(data), verify=False) 72 | return response 73 | except RequestException as e: 74 | logger.error(f"Error sending POST request: {e}") 75 | raise 76 | 77 | def send_get_with_jwt(self, url: str): 78 | """Sends a GET request with provided URL, and JWT headers.""" 79 | headers = self.build_headers_with_jwt() 80 | try: 81 | response = get(url, headers=headers, verify=False) 82 | return response 83 | except RequestException as e: 84 | logger.error(f"Error sending POST request: {e}") 85 | raise 86 | 87 | def send_delete_with_jwt(self, url: str): 88 | """Sends a DELETE request with provided URL, and JWT headers.""" 89 | headers = self.build_headers_with_jwt() 90 | try: 91 | response = delete(url, headers=headers, verify=False) 92 | return response 93 | except RequestException as e: 94 | logger.error(f"Error sending POST request: {e}") 95 | raise 96 | 97 | def log_response(self, response): 98 | if response.status_code not in [200, 201]: 99 | if response.status_code == 404: 100 | logger.error(f"HTTP Code: {response.status_code}; content: {response.content}, url: {response.url}") 101 | return 102 | logger.error(f"HTTP Code: {response.status_code}; content: {response.content}") 103 | 104 | 105 | class DareDBDataSamplerSimple(DareDBPyClientBase): 106 | 107 | def populate_db_with_sample_data(self): 108 | """Populates database with sample data entries.""" 109 | 110 | url = f"{base_url}/set" 111 | logger.debug(f"Populate DB with sample data via URL: {url}") 112 | MAX_REQUESTS = 5 113 | for i in range(MAX_REQUESTS): 114 | data = {f"key_{i}_{uuid.uuid4()}": f"value_{i}"} 115 | response = self.send_post_with_jwt(url, data) 116 | self.log_response(response) 117 | 118 | 119 | class DareDBManageCollections(DareDBPyClientBase): 120 | MAX_COLLECTIONS = 5 121 | 122 | def create(self, name: str = "sample"): 123 | url = f"{base_url}/collections/{name}" 124 | response = self.send_post_with_jwt(url) 125 | self.log_response(response) 126 | 127 | def create_multiple(self): 128 | 129 | collection_name = "sample" 130 | for i in range(self.MAX_COLLECTIONS): 131 | url = f"{base_url}/collections/{collection_name}_{i}" 132 | response = self.send_post_with_jwt(url) 133 | self.log_response(response) 134 | 135 | def delete_multiple(self): 136 | collection_name = "sample" 137 | for i in range(self.MAX_COLLECTIONS): 138 | url = f"{base_url}/collections/{collection_name}_{i}" 139 | response = self.send_delete_with_jwt(url) 140 | self.log_response(response) 141 | 142 | def list(self): 143 | url = f"{base_url}/collections" 144 | response = self.send_get_with_jwt(url) 145 | if response.status_code == 200: 146 | logger.info(f"\n{json.dumps(response.json(), indent=2)}") 147 | else: 148 | self.log_response(response) 149 | 150 | 151 | class DareDBSamplerForCollections(DareDBManageCollections): 152 | 153 | MAX_REQUESTS = 5 154 | 155 | def populate(self, collection_name: str = "sample"): 156 | """Populates database with sample data entries.""" 157 | 158 | self.create(collection_name) 159 | url = f"{base_url}/collections/{collection_name}/set" 160 | logger.debug(f"Populate DB with sample data via URL (collection: {collection_name}): {url}") 161 | 162 | for i in range(self.MAX_REQUESTS): 163 | data = {f"key_{i}_{uuid.uuid4()}": f"value_{i} in collection: {collection_name}"} 164 | response = self.send_post_with_jwt(url, data) 165 | self.log_response(response) 166 | 167 | def get_all_items(self, collection_name: str = "sample"): 168 | 169 | url = f"{base_url}/collections/{collection_name}/items" 170 | response = self.send_get_with_jwt(url) 171 | if response.status_code == 200: 172 | logger.info(f"\n{json.dumps(response.json(), indent=2)}") 173 | else: 174 | self.log_response(response) 175 | 176 | 177 | if __name__ == "__main__": 178 | 179 | base_url, username, password = load_credentials() 180 | 181 | sampler = DareDBDataSamplerSimple(username, password, base_url) 182 | sampler.populate_db_with_sample_data() 183 | 184 | collections = DareDBManageCollections(username, password, base_url) 185 | 186 | collections.create() 187 | collections.create_multiple() 188 | collections.list() 189 | collections.delete_multiple() 190 | 191 | sampler_collections = DareDBSamplerForCollections(username, password, base_url) 192 | sampler_collections.populate() 193 | sampler_collections.get_all_items() 194 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dmarro89/dare-db 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/dmarro89/go-redis-hashtable v0.0.7 7 | github.com/golang-jwt/jwt/v5 v5.2.2 8 | github.com/sirupsen/logrus v1.9.3 9 | github.com/spf13/viper v1.20.1 10 | github.com/stretchr/testify v1.10.0 11 | gotest.tools v2.2.0+incompatible 12 | ) 13 | 14 | require ( 15 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect 16 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 17 | ) 18 | 19 | require ( 20 | github.com/casbin/casbin v1.9.1 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/dchest/siphash v1.2.3 // indirect 23 | github.com/fsnotify/fsnotify v1.8.0 // indirect 24 | github.com/google/go-cmp v0.6.0 // indirect 25 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 26 | github.com/pkg/errors v0.9.1 // indirect 27 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 28 | github.com/sagikazarmark/locafero v0.7.0 // indirect 29 | github.com/sourcegraph/conc v0.3.0 // indirect 30 | github.com/spf13/afero v1.12.0 // indirect 31 | github.com/spf13/cast v1.7.1 // indirect 32 | github.com/spf13/pflag v1.0.6 // indirect 33 | github.com/stretchr/objx v0.5.2 // indirect 34 | github.com/subosito/gotenv v1.6.0 // indirect 35 | go.uber.org/atomic v1.9.0 // indirect 36 | go.uber.org/multierr v1.9.0 // indirect 37 | golang.org/x/sys v0.29.0 // indirect 38 | golang.org/x/text v0.21.0 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= 2 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 3 | github.com/casbin/casbin v1.9.1 h1:ucjbS5zTrmSLtH4XogqOG920Poe6QatdXtz1FEbApeM= 4 | github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= 10 | github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= 11 | github.com/dmarro89/go-redis-hashtable v0.0.7 h1:XkckTjUlR2yNPmLQBT826YK6swBbKufr4Teyy3gQvHI= 12 | github.com/dmarro89/go-redis-hashtable v0.0.7/go.mod h1:GJxzF4dkIF7HT+kXi5X1gfJOhNJ3aNannR0LZciOh0s= 13 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 14 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 15 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 16 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 17 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 18 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 19 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 20 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 21 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 22 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 23 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 24 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 25 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 26 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 27 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 28 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 29 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 30 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 33 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 35 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 36 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 37 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 38 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 39 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 40 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 41 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 42 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 43 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 44 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 45 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 46 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 47 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 48 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 49 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 52 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 56 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 57 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 58 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 59 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 60 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 61 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 62 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 63 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 65 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 66 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 67 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 70 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 72 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 73 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 75 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 76 | -------------------------------------------------------------------------------- /logger/formatter.go: -------------------------------------------------------------------------------- 1 | // Package easy allows to easily format output of Logrus logger 2 | // Based on the - https://github.com/t-tomalak/logrus-easy-formatter 3 | // See the license - https://github.com/t-tomalak/logrus-easy-formatter/blob/master/LICENSE 4 | 5 | package logger 6 | 7 | import ( 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | const ( 16 | // Default log format will output [INFO]: 2006-01-02T15:04:05Z07:00 - Log message 17 | defaultLogFormat = "%time% [%lvl%] - %msg%" 18 | defaultTimestampFormat = time.RFC3339 19 | ) 20 | 21 | // Formatter implements logrus.Formatter interface. 22 | type Formatter struct { 23 | // Timestamp format 24 | TimestampFormat string 25 | // Available standard keys: time, msg, lvl 26 | // Also can include custom fields but limited to strings. 27 | // All of fields need to be wrapped inside %% i.e %time% %msg% 28 | LogFormat string 29 | } 30 | 31 | // Format building log message. 32 | func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) { 33 | 34 | output := f.LogFormat 35 | if output == "" { 36 | output = defaultLogFormat 37 | } 38 | 39 | timestampFormat := f.TimestampFormat 40 | if timestampFormat == "" { 41 | timestampFormat = defaultTimestampFormat 42 | } 43 | 44 | output = strings.Replace(output, "%time%", entry.Time.Format(timestampFormat), 1) 45 | 46 | output = strings.Replace(output, "%msg%", entry.Message, 1) 47 | 48 | level := strings.ToUpper(entry.Level.String()) 49 | output = strings.Replace(output, "%lvl%", level, 1) 50 | 51 | for k, val := range entry.Data { 52 | switch v := val.(type) { 53 | case string: 54 | output = strings.Replace(output, "%"+k+"%", v, 1) 55 | case int: 56 | s := strconv.Itoa(v) 57 | output = strings.Replace(output, "%"+k+"%", s, 1) 58 | case bool: 59 | s := strconv.FormatBool(v) 60 | output = strings.Replace(output, "%"+k+"%", s, 1) 61 | } 62 | } 63 | 64 | return []byte(output), nil 65 | } 66 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type Logger interface { 12 | Start(filename string) 13 | Close() 14 | Info(args ...interface{}) 15 | Warn(args ...interface{}) 16 | Debug(args ...interface{}) 17 | Error(args ...interface{}) 18 | Fatal(args ...interface{}) 19 | } 20 | 21 | type DareLogger struct { 22 | logger *logrus.Logger 23 | file *os.File 24 | } 25 | 26 | func NewDareLogger() Logger { 27 | log := logrus.New() 28 | log.SetFormatter(&Formatter{ 29 | TimestampFormat: "2006-01-02 15:04:05", 30 | LogFormat: "%time% [%lvl%] - %msg%\n", 31 | }) 32 | return &DareLogger{logger: log} 33 | } 34 | 35 | // Start opens a log file for writing and config output 36 | func (dareLogger *DareLogger) Start(filename string) { 37 | logFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 38 | if err != nil { 39 | fmt.Println("Error opening log file:", err) 40 | } 41 | dareLogger.file = logFile 42 | dareLogger.logger.SetOutput(io.MultiWriter(os.Stdout, logFile)) 43 | } 44 | 45 | // Close close the log file. 46 | func (dareLogger *DareLogger) Close() { 47 | if dareLogger.file != nil { 48 | err := dareLogger.file.Close() 49 | if err != nil { 50 | fmt.Println("error closing log file: %w", err) 51 | } 52 | dareLogger.file = nil 53 | } 54 | } 55 | 56 | // Debug logs a message at the debug level. 57 | func (dareLogger *DareLogger) Debug(args ...interface{}) { 58 | dareLogger.logger.Debug(args...) 59 | } 60 | 61 | // Info logs a message at the info level. 62 | func (dareLogger *DareLogger) Info(args ...interface{}) { 63 | dareLogger.logger.Info(args...) 64 | } 65 | 66 | // Warn logs a message at the warn level. 67 | func (dareLogger *DareLogger) Warn(args ...interface{}) { 68 | dareLogger.logger.Warn(args...) 69 | } 70 | 71 | // Error logs a message at the error level. 72 | func (dareLogger *DareLogger) Error(args ...interface{}) { 73 | dareLogger.logger.Error(args...) 74 | } 75 | 76 | // Fa tal logs a message at the fatal level, then exits the program. 77 | func (dareLogger *DareLogger) Fatal(args ...interface{}) { 78 | dareLogger.logger.Fatal(args...) 79 | } 80 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dmarro89/dare-db/auth" 5 | "github.com/dmarro89/dare-db/database" 6 | "github.com/dmarro89/dare-db/logger" 7 | "github.com/dmarro89/dare-db/server" 8 | ) 9 | 10 | func main() { 11 | logger := logger.NewDareLogger() 12 | configuration := server.NewConfiguration("") 13 | database := database.NewDatabase() 14 | userStore := auth.NewUserStore() 15 | userStore.AddUser(configuration.GetString("server.admin_user"), configuration.GetString("server.admin_password")) 16 | dareServer := server.NewDareServer(database, userStore) 17 | server := server.NewFactory(configuration, logger).GetWebServer(dareServer) 18 | 19 | server.Start() 20 | defer server.Stop() 21 | } 22 | -------------------------------------------------------------------------------- /openapi/.gitignore: -------------------------------------------------------------------------------- 1 | *.env -------------------------------------------------------------------------------- /openapi/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-bullseye AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod . 6 | COPY go.sum . 7 | 8 | RUN go mod download 9 | 10 | COPY main.go main.go 11 | 12 | ENV GOCACHE=/root/.cache/go-build 13 | RUN --mount=type=cache,target="/root/.cache/go-build" go build -o app 14 | 15 | FROM ubuntu:22.04 16 | 17 | RUN mkdir /app 18 | WORKDIR /app 19 | COPY . . 20 | COPY --from=builder /app/app . 21 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /openapi/README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | Following file contains OpenAPI 3.0 spec for DareDB in `YAML` format -> [daredb-openapi_primary.yaml](daredb-openapi_primary.yaml) 4 | 5 | 6 | ## Render API: Using `redocly` (remote) 7 | 8 | You can view API OpenAPI 3.0 spec for DareDB using `redocly` -> [daredb-openapi_primary.yaml](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/dmarro89/dare-db/refs/heads/main/openapi/daredb-openapi_primary.yaml) 9 | 10 | 11 | ## Render API: Self-Hosted (local) 12 | 13 | You can render API directly locally using `docker compose`. Must be run in the directory `openapi`. 14 | 15 | * Run with docker compose 16 | ```bash 17 | docker compose up 18 | ``` 19 | * (optional) Rebuild `openapi` server on demand 20 | ```bash 21 | docker compose up -d --no-deps --build openapi 22 | ``` 23 | * (optional) create and modify file with settings: `.env` 24 | * Make sure the database is running 25 | * In browser open url: 26 | - http://127.0.0.1:5002/docs 27 | * Use credentials to call database from webui. Follow steps 1, 2, 3, and 4: 28 | ![alt text](images/openapi-auth.png) 29 | 30 | ## Development: Dev Dependencies 31 | 32 | * OpenAPI 3.0 (Swagger Editor) - https://editor.swagger.io/ -------------------------------------------------------------------------------- /openapi/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | openapi: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | image: daredb-openapi:latest 7 | env_file: 8 | - .env 9 | ports: 10 | - "127.0.0.1:5002:5002" 11 | 12 | networks: 13 | daredb-network: -------------------------------------------------------------------------------- /openapi/daredb-openapi_primary.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # this is a template, following placeholders must be replaced: 3 | # - API_SERVER_NAME 4 | # 5 | openapi: "3.0.0" 6 | info: 7 | version: v0.0.6 8 | title: DareDB API Spec 9 | description: | 10 | DareDB API documentation in OpenAPI 3.0 format 11 | #### Features: 12 | * Provides documentation on REST API of the database 13 | servers: 14 | - url: "API_SERVER_NAME" 15 | 16 | paths: 17 | /login: 18 | post: 19 | summary: Retrieves a JWT token by performing a login with username and password. 20 | tags: 21 | - auth 22 | description: | 23 | This endpoint performs a basic authentication using the provided credentials 24 | and returns a JSON Web Token (JWT) that can be used for subsequent requests. 25 | security: 26 | - basicAuth: [] 27 | responses: 28 | '200': 29 | description: Successful JWT token retrieval 30 | content: 31 | application/json: 32 | schema: 33 | type: object 34 | properties: 35 | token: 36 | type: string 37 | description: The retrieved JWT token 38 | '401': 39 | description: Invalid username or password 40 | '500': 41 | description: Internal server error 42 | 43 | /set: 44 | post: 45 | summary: Set a key-value pair in the data store. 46 | tags: 47 | - default 48 | operationId: setKeyValue 49 | security: 50 | - jwtBearerAuth: [] 51 | parameters: 52 | - name: key 53 | in: path 54 | required: true 55 | schema: 56 | type: string 57 | - name: value 58 | in: body 59 | required: true 60 | schema: 61 | type: string 62 | responses: 63 | 201: 64 | description: Key-value pair set successfully. 65 | default: 66 | description: Unexpected error. 67 | 68 | /get: 69 | get: 70 | summary: Get the value associated with a key from the data store. 71 | tags: 72 | - default 73 | operationId: getValueByKey 74 | security: 75 | - jwtBearerAuth: [] 76 | parameters: 77 | - name: key 78 | in: path 79 | required: true 80 | schema: 81 | type: string 82 | responses: 83 | 200: 84 | description: Value retrieved successfully. 85 | content: 86 | application/json: 87 | schema: 88 | type: object 89 | properties: 90 | value: 91 | type: string 92 | default: 93 | description: Unexpected error. 94 | 95 | /collections: 96 | get: 97 | summary: List all collections in the data store. 98 | tags: 99 | - collections 100 | operationId: listCollections 101 | security: 102 | - jwtBearerAuth: [] 103 | responses: 104 | 200: 105 | description: Collections listed successfully. 106 | content: 107 | application/json: 108 | schema: 109 | type: array 110 | items: 111 | type: string 112 | 113 | /collections/{collectionName}: 114 | post: 115 | summary: Create a new collection in the data store. 116 | tags: 117 | - collections 118 | security: 119 | - jwtBearerAuth: [] 120 | operationId: createCollection 121 | parameters: 122 | - name: collectionName 123 | in: path 124 | required: true 125 | schema: 126 | type: string 127 | responses: 128 | 201: 129 | description: Collection created successfully. 130 | default: 131 | description: Unexpected error. 132 | 133 | delete: 134 | summary: Delete a collection from the data store. 135 | tags: 136 | - collections 137 | operationId: deleteCollection 138 | security: 139 | - jwtBearerAuth: [] 140 | parameters: 141 | - name: collectionName 142 | in: path 143 | required: true 144 | schema: 145 | type: string 146 | responses: 147 | 200: 148 | description: Collection deleted successfully. 149 | default: 150 | description: Unexpected error. 151 | 152 | components: 153 | schemas: 154 | Error: 155 | required: 156 | - code 157 | - message 158 | properties: 159 | code: 160 | type: integer 161 | format: int32 162 | message: 163 | type: string 164 | 165 | securitySchemes: 166 | basicAuth: 167 | description: user login and password from `config.toml` 168 | type: http 169 | scheme: basic 170 | #jwtBearerAuth: 171 | # type: http 172 | # scheme: bearer 173 | # bearerFormat: JWT 174 | jwtBearerAuth: 175 | description: obtain token after sending `/login` request (requires login/password from `config.toml`) 176 | type: apiKey 177 | in: header 178 | name: Authorization 179 | -------------------------------------------------------------------------------- /openapi/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dmarro89/dare-db/openapi 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/ilyakaznacheev/cleanenv v1.5.0 7 | github.com/kohkimakimoto/echo-openapidocs v0.2.0 8 | github.com/labstack/echo/v4 v4.13.3 9 | ) 10 | 11 | require ( 12 | github.com/kr/pretty v0.3.0 // indirect 13 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | 17 | require ( 18 | github.com/BurntSushi/toml v1.4.0 // indirect 19 | github.com/joho/godotenv v1.5.1 // indirect 20 | github.com/labstack/gommon v0.4.2 // indirect 21 | github.com/mattn/go-colorable v0.1.13 // indirect 22 | github.com/mattn/go-isatty v0.0.20 // indirect 23 | github.com/valyala/bytebufferpool v1.0.0 // indirect 24 | github.com/valyala/fasttemplate v1.2.2 // indirect 25 | golang.org/x/crypto v0.36.0 // indirect 26 | golang.org/x/net v0.38.0 // indirect 27 | golang.org/x/sys v0.31.0 // indirect 28 | golang.org/x/text v0.23.0 // indirect 29 | golang.org/x/time v0.8.0 // indirect 30 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /openapi/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 2 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 3 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= 8 | github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= 9 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 10 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 11 | github.com/kohkimakimoto/echo-openapidocs v0.2.0 h1:s41HMHTHqQ/S+exwzl/DlgJkasDFL2LGuYUR0c/Y2x0= 12 | github.com/kohkimakimoto/echo-openapidocs v0.2.0/go.mod h1:x+EuD5jebiiK9Zb2FZAjpfvBO7ckGoikM/kD2MFE3w4= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 15 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 16 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 17 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 18 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 22 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 23 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 24 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 25 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 26 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 27 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 28 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 29 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 33 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 34 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 35 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 36 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 37 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 38 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 39 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 40 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 41 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 42 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 43 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 44 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 47 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 48 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 49 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 50 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 51 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 55 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 56 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 57 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 58 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 59 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= 60 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= 61 | -------------------------------------------------------------------------------- /openapi/images/openapi-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmarro89/dare-db/bb63e17a7f1fa00abbff9d6638531851bff25a2e/openapi/images/openapi-auth.png -------------------------------------------------------------------------------- /openapi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/ilyakaznacheev/cleanenv" 13 | openapidocs "github.com/kohkimakimoto/echo-openapidocs" 14 | "github.com/labstack/echo/v4" 15 | "github.com/labstack/echo/v4/middleware" 16 | ) 17 | 18 | var workingDir string 19 | 20 | const ( 21 | URL_OPENAPI_DOC = "/docs" 22 | URL_OPENAPI_FILE = "/openapi-file" 23 | FILE_OPENAPI_DOC = "daredb-openapi_primary.yaml" 24 | FILE_OPENAPI_DOC_READY = "daredb-openapi_generated_webui.yaml" 25 | TEMPLATE_API_SERVER_NAME = "API_SERVER_NAME" 26 | ) 27 | 28 | const ( 29 | ECHO_LOG_FORMATTING = `${time_rfc3339} remote_ip=${remote_ip}, method=${method}, status=${status}, uri=${uri}, host=${host}, error=${error}` 30 | ) 31 | 32 | type customLogWriter struct { 33 | } 34 | 35 | type ConfigDatabase struct { 36 | OpenAPIServerHost string `env:"OPENAPI_SERVER_HOST" env-default:"127.0.0.1"` 37 | OpenAPIServerPort string `env:"OPENAPI_SERVER_PORT" env-default:"5002"` 38 | AccessOpenAPIServerHost string `env:"ACCESS_OPENAPI_SERVER_HOST" env-default:"127.0.0.1"` 39 | DareDBServerHost string `env:"DAREDB_SERVER_HOST" env-default:"127.0.0.1"` 40 | DareDBServerPort string `env:"DAREDB_SERVER_PORT" env-default:"5001"` 41 | DareDBWithTLS bool `env:"DAREDB_SERVER_WITH_TLS" env-default:"true"` 42 | } 43 | 44 | var Config ConfigDatabase 45 | 46 | func serveOpenAPIDocYAML(c echo.Context) error { 47 | 48 | filePath := filepath.Join(workingDir, FILE_OPENAPI_DOC) 49 | filePathGen := filepath.Join(workingDir, FILE_OPENAPI_DOC_READY) 50 | dareDBServerName := fmt.Sprintf("http://%s:%s", Config.DareDBServerHost, Config.DareDBServerPort) 51 | 52 | if Config.DareDBWithTLS == true { 53 | dareDBServerName = fmt.Sprintf("https://%s:%s", Config.DareDBServerHost, Config.DareDBServerPort) 54 | } 55 | 56 | err := replaceInFile(filePath, filePathGen, TEMPLATE_API_SERVER_NAME, dareDBServerName) 57 | if err != nil { 58 | c.Echo().Logger.Error(err) 59 | } 60 | 61 | return c.File(filePathGen) 62 | } 63 | 64 | func GetEnvVariable(key, fallback string) string { 65 | if value, ok := os.LookupEnv(key); ok { 66 | return value 67 | } 68 | return fallback 69 | } 70 | 71 | // Makes replacement of string in a file and creates a new file 72 | func replaceInFile(infile, outfile, oldString, newString string) error { 73 | data, err := os.ReadFile(infile) 74 | if err != nil { 75 | return fmt.Errorf("error reading file: %w", err) 76 | } 77 | newContent := strings.ReplaceAll(string(data), oldString, newString) 78 | return os.WriteFile(outfile, []byte(newContent), 0644) 79 | } 80 | 81 | // Redirects to the home page of the service 82 | func serverNotFoundMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 83 | return func(c echo.Context) error { 84 | if c.Path() == "/" { 85 | return c.Redirect(http.StatusMovedPermanently, URL_OPENAPI_DOC) 86 | } 87 | return next(c) 88 | } 89 | } 90 | 91 | // Making CORS less restrictive 92 | func nonRestrictiveCORSMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 93 | return func(c echo.Context) error { 94 | //c.Response().Header().Set("Referrer-Policy", "origin-when-cross-origin") // Choose your desired policy 95 | c.Response().Header().Set("Referrer-Policy", "unsafe-url") // non secure workaround CORS 96 | return next(c) 97 | } 98 | } 99 | 100 | // Configures global value of the current working directory 101 | func configWorkingDir() { 102 | workingDir, err := os.Getwd() 103 | if err != nil { 104 | log.Printf("Error getting working directory: %v", err) 105 | return 106 | } 107 | log.Printf("Working directory: %v", workingDir) 108 | } 109 | 110 | func (writer customLogWriter) Write(bytes []byte) (int, error) { 111 | return fmt.Print(time.Now().UTC().Format("2006-01-02 15:04:05") + " " + string(bytes)) 112 | } 113 | 114 | func main() { 115 | 116 | log.SetFlags(0) 117 | log.SetOutput(new(customLogWriter)) 118 | 119 | configWorkingDir() 120 | 121 | if err := cleanenv.ReadEnv(&Config); err != nil { 122 | log.Panicln("Was not able to read env") 123 | } 124 | 125 | //openapiServer := fmt.Sprintf("http://%s:%s", Config.OpenAPIServerHost, Config.OpenAPIServerPort) 126 | openapiFileServer := fmt.Sprintf("http://%s:%s", Config.AccessOpenAPIServerHost, Config.OpenAPIServerPort) 127 | //dareDBServer := fmt.Sprintf("http://%s:%s", Config.DareDBServerHost, Config.DareDBServerPort) 128 | //dareDBServerWithTLS := fmt.Sprintf("https://%s:%s", Config.DareDBServerHost, Config.DareDBServerPort) 129 | 130 | e := echo.New() 131 | e.HideBanner = true 132 | e.HidePort = true 133 | 134 | e.Use(serverNotFoundMiddleware) 135 | e.Use(nonRestrictiveCORSMiddleware) 136 | 137 | // Add custom logging middleware 138 | e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 139 | Format: ECHO_LOG_FORMATTING + "\n", 140 | })) 141 | 142 | // Enable CORS with specific origins (using v4 functions) 143 | e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 144 | AllowOrigins: []string{"*"}, 145 | //AllowOrigins: []string{openapiServer, openapiFileServer, dareDBServer, dareDBServerWithTLS}, 146 | })) 147 | 148 | openapiServerFileUrl := openapiFileServer + URL_OPENAPI_FILE 149 | 150 | // Register the Spotlight/Elements documentation with OpenAPI Spec url 151 | openapidocs.SwaggerUIDocuments(e, URL_OPENAPI_DOC, openapidocs.SwaggerUIConfig{ 152 | SpecUrl: openapiServerFileUrl, 153 | Title: "REST API", 154 | }) 155 | 156 | // Serving OpenAPI 3.0 file 157 | e.GET(URL_OPENAPI_FILE, serveOpenAPIDocYAML) 158 | 159 | serverStartOn := fmt.Sprintf("%s:%s", Config.OpenAPIServerHost, Config.OpenAPIServerPort) 160 | log.Printf("Run server on: %s\n", serverStartOn) 161 | 162 | // Starting server 163 | e.Start(serverStartOn) 164 | } 165 | -------------------------------------------------------------------------------- /server/configuration.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/dmarro89/dare-db/logger" 11 | "github.com/dmarro89/dare-db/utils" 12 | 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | type Config interface { 17 | Get(key string) interface{} 18 | GetString(key string) string 19 | GetBool(key string) bool 20 | IsSet(key string) bool 21 | } 22 | 23 | type ViperConfig struct { 24 | viper *viper.Viper 25 | logger logger.Logger 26 | mapsEnvsToConfig map[string]string 27 | } 28 | 29 | func (c *ViperConfig) checkFileExists(filePath string) bool { 30 | _, error := os.Stat(filePath) 31 | return !errors.Is(error, os.ErrNotExist) 32 | } 33 | 34 | func (c *ViperConfig) createDirectory(dirPath string) { 35 | err := os.MkdirAll(dirPath, 0755) 36 | switch { 37 | case err == nil: 38 | c.logger.Info("Directory created successfully: ", dirPath) 39 | case os.IsExist(err): 40 | c.logger.Debug("Directory already exists: ", dirPath) 41 | default: 42 | c.logger.Error("Error creating directory:", err) 43 | } 44 | } 45 | 46 | func (c *ViperConfig) createDefaultConfigFile(cfgFile string) { 47 | var passwordNew string = utils.GenerateRandomString(12) 48 | 49 | c.logger.Info("Creating default configuration file") 50 | 51 | c.viper.SetDefault("server.host", "127.0.0.1") 52 | c.viper.SetDefault("server.port", "2605") 53 | c.viper.SetDefault("server.admin_user", "admin") 54 | c.viper.SetDefault("server.admin_password", passwordNew) 55 | 56 | c.viper.SetDefault("log.log_level", "INFO") 57 | c.viper.SetDefault("log.log_file", "daredb.log") 58 | 59 | c.viper.SetDefault("settings.data_dir", DATA_DIR) 60 | c.viper.SetDefault("settings.settings_dir", SETTINGS_DIR) 61 | 62 | c.viper.SetDefault("security.tls_enabled", false) 63 | c.viper.SetDefault("security.cert_private", filepath.Join(SETTINGS_DIR, "cert_private.pem")) 64 | c.viper.SetDefault("security.cert_public", filepath.Join(SETTINGS_DIR, "cert_public.pem")) 65 | 66 | c.viper.WriteConfigAs(cfgFile) 67 | 68 | c.logger.Info("\n\nIMPORTANT! Generate default password for admin on initial start. Store it securely. Password: ", passwordNew, "\n") 69 | } 70 | 71 | func (c *ViperConfig) mappingEnvsToConfig() { 72 | c.mapsEnvsToConfig["server.host"] = "DARE_HOST" 73 | c.mapsEnvsToConfig["server.port"] = "DARE_PORT" 74 | c.mapsEnvsToConfig["server.admin_user"] = "DARE_USER" 75 | c.mapsEnvsToConfig["server.admin_password"] = "DARE_PASSWORD" 76 | 77 | c.mapsEnvsToConfig["log.log_level"] = "DARE_LOG_LEVEL" 78 | c.mapsEnvsToConfig["log.log_file"] = "DARE_LOG_FILE" 79 | 80 | c.mapsEnvsToConfig["settings.data_dir"] = "DARE_DATA_DIR" 81 | c.mapsEnvsToConfig["settings.base_dir"] = "DARE_BASE_DIR" 82 | c.mapsEnvsToConfig["settings.settings_dir"] = "DARE_SETTINGS_DIR" 83 | 84 | c.mapsEnvsToConfig["security.tls_enabled"] = "DARE_TLS_ENABLED" 85 | c.mapsEnvsToConfig["security.cert_private"] = "DARE_CERT_PRIVATE" 86 | c.mapsEnvsToConfig["security.cert_public"] = "DARE_CERT_PUBLIC" 87 | } 88 | 89 | func (c *ViperConfig) reReadConfigsFromEnvs(viper *viper.Viper) { 90 | c.logger.Info("Re-reading configurations from environmental variables") 91 | for key, value := range c.mapsEnvsToConfig { 92 | if valueFromEnv, ok := os.LookupEnv(value); ok { 93 | c.logger.Info("Use new configuration value from environmental variable for: ", key) 94 | viper.Set(key, valueFromEnv) 95 | } 96 | } 97 | } 98 | 99 | func (c *ViperConfig) initDBDirectories() { 100 | dbBaseDir, err := os.Getwd() 101 | if err != nil { 102 | c.logger.Error("Error in getting current working directory:", err) 103 | } 104 | os.Setenv("DARE_BASE_DIR", dbBaseDir) 105 | 106 | c.createDirectory(filepath.Join(dbBaseDir, SETTINGS_DIR)) 107 | c.createDirectory(filepath.Join(dbBaseDir, c.GetString("settings.data_dir"))) 108 | } 109 | 110 | func NewConfiguration(cfgFile string) Config { 111 | logger := logger.NewDareLogger() 112 | if len(strings.TrimSpace(cfgFile)) == 0 { 113 | logger.Info("No configuration file was supplied. Using default value: ", DEFAULT_CONFIG_FILE) 114 | cfgFile = DEFAULT_CONFIG_FILE 115 | } 116 | 117 | v := viper.New() 118 | v.SetConfigType("toml") 119 | 120 | c := &ViperConfig{viper: v, logger: logger, mapsEnvsToConfig: make(map[string]string)} 121 | c.mappingEnvsToConfig() 122 | 123 | if !c.checkFileExists(cfgFile) { 124 | c.logger.Info("Configuration file does not exist: ", cfgFile) 125 | c.createDefaultConfigFile(cfgFile) 126 | } 127 | 128 | logger.Info("Using configuration file: ", cfgFile) 129 | 130 | v.SetConfigFile(cfgFile) 131 | if err := v.ReadInConfig(); err != nil { 132 | c.logger.Fatal("Error reading config file:", err) 133 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 134 | panic("Config file was not found") 135 | } else { 136 | panic("Config file was found, but another error was produced") 137 | } 138 | } 139 | 140 | c.reReadConfigsFromEnvs(v) 141 | c.initDBDirectories() 142 | return &ViperConfig{viper: v, logger: logger, mapsEnvsToConfig: make(map[string]string)} 143 | } 144 | 145 | func (c *ViperConfig) Get(key string) interface{} { 146 | return c.viper.Get(key) 147 | } 148 | 149 | func (c *ViperConfig) GetString(key string) string { 150 | return c.viper.GetString(key) 151 | } 152 | 153 | func (c *ViperConfig) GetBool(key string) bool { 154 | return c.viper.GetBool(key) 155 | } 156 | 157 | func (c *ViperConfig) IsSet(key string) bool { 158 | return c.viper.IsSet(key) 159 | } 160 | -------------------------------------------------------------------------------- /server/configuration_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const TEST_CONFIG_FILE string = "config-test.toml" 13 | const TEST_CONFIGURATION_DIR = "server" 14 | 15 | var TEST_FOLDERS = []string{"data", "settings"} 16 | 17 | func SetupTestConfiguration() Config { 18 | checkCorrectTestDirectory() 19 | return NewConfiguration(TEST_CONFIG_FILE) 20 | } 21 | 22 | func TeardownTestConfiguration() { 23 | checkCorrectTestDirectory() 24 | err := removeFileOrDirIfExists(TEST_CONFIG_FILE) 25 | if err != nil { 26 | fmt.Println("Error:", err) 27 | } else { 28 | fmt.Println("File was removed successfully (if it existed):", TEST_CONFIG_FILE) 29 | } 30 | 31 | for _, folder := range TEST_FOLDERS { 32 | err := removeFileOrDirIfExists(folder) 33 | if err != nil { 34 | fmt.Println("Error:", err) 35 | } else { 36 | fmt.Println("Folder was removed successfully (if it existed):", TEST_CONFIG_FILE) 37 | } 38 | } 39 | } 40 | 41 | // check, if tests run in the right directory 42 | func checkCorrectTestDirectory() { 43 | baseDir, _ := os.Getwd() 44 | if !strings.HasSuffix(baseDir, TEST_CONFIGURATION_DIR) { 45 | panic("Wrong directory for running this test. Possibility to delete data and settings folders.") 46 | } 47 | } 48 | 49 | func removeFileOrDirIfExists(filePath string) error { 50 | _, err := os.Stat(filePath) 51 | if err != nil { 52 | if os.IsNotExist(err) { 53 | return nil 54 | } 55 | return err 56 | } 57 | err = os.RemoveAll(filePath) 58 | if err != nil { 59 | return fmt.Errorf("failed to remove file/directory: %w", err) 60 | } 61 | return nil 62 | } 63 | 64 | func TestDefaultParameters(t *testing.T) { 65 | 66 | testConfig := SetupTestConfiguration() 67 | defer TeardownTestConfiguration() 68 | 69 | // Check if the values are correctly set 70 | assert.Equal(t, "127.0.0.1", testConfig.GetString("server.host"), "Host should be '127.0.0.1'") 71 | assert.Equal(t, "2605", testConfig.GetString("server.port"), "Port should be '2605'") 72 | assert.Equal(t, "admin", testConfig.GetString("server.admin_user"), "Admin name should be 'admin'") 73 | assert.Equal(t, "INFO", testConfig.GetString("log.log_level"), "Must be 'INFO'") 74 | assert.Equal(t, "daredb.log", testConfig.GetString("log.log_file"), "Must be 'daredb.log'") 75 | assert.Equal(t, false, testConfig.GetBool("security.tls_enabled"), "Must be 'false'") 76 | } 77 | 78 | func TestConfigurationConstants(t *testing.T) { 79 | 80 | testConfig := SetupTestConfiguration() 81 | defer TeardownTestConfiguration() 82 | 83 | // Check if the values are correctly set 84 | assert.Equal(t, "config.toml", DEFAULT_CONFIG_FILE, "Host should be 'config.toml'") 85 | assert.Equal(t, "data", DATA_DIR, "Host should be 'data'") 86 | assert.Equal(t, "settings", SETTINGS_DIR, "Host should be 'settings'") 87 | assert.Equal(t, SETTINGS_DIR, testConfig.GetString("settings.settings_dir"), "Host should be 'settings'") 88 | } 89 | 90 | func TestConfiguratioReReadFeature(t *testing.T) { 91 | t.Setenv("DARE_PORT", "2606") 92 | 93 | testConfig := SetupTestConfiguration() 94 | defer TeardownTestConfiguration() 95 | 96 | // Check if the values are correctly set 97 | assert.Equal(t, "2606", testConfig.GetString("server.port"), "Port should be '2606'") 98 | } 99 | -------------------------------------------------------------------------------- /server/constants.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | const DEFAULT_CONFIG_FILE string = "config.toml" 4 | const DATA_DIR string = "data" // use to settings relevant to database instance 5 | const SETTINGS_DIR string = "settings" // use to settings relevant to database instance 6 | -------------------------------------------------------------------------------- /server/dare-server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/dmarro89/dare-db/auth" 10 | "github.com/dmarro89/dare-db/database" 11 | ) 12 | 13 | const KEY_PARAM = "key" 14 | const COLLECTION_NAME_PARAM = "collectionName" 15 | 16 | type IDare interface { 17 | CreateMux(auth.Authorizer, auth.Authenticator) *http.ServeMux 18 | HandlerGetById(w http.ResponseWriter, r *http.Request) 19 | HandlerSet(w http.ResponseWriter, r *http.Request) 20 | HandlerDelete(w http.ResponseWriter, r *http.Request) 21 | HandlerLogin(w http.ResponseWriter, r *http.Request) 22 | } 23 | 24 | type DareServer struct { 25 | userStore *auth.UserStore 26 | collectionManager *database.CollectionManager 27 | } 28 | 29 | func NewDareServer(db *database.Database, userStore *auth.UserStore) *DareServer { 30 | collectionManager := database.NewCollectionManager() 31 | collectionManager.AddCollection(database.DEFAULT_COLLECTION) 32 | 33 | return &DareServer{ 34 | userStore: userStore, 35 | collectionManager: collectionManager, 36 | } 37 | } 38 | 39 | func (srv *DareServer) CreateMux(authorizer auth.Authorizer, authenticator auth.Authenticator) *http.ServeMux { 40 | mux := http.NewServeMux() 41 | 42 | if authorizer == nil { 43 | authorizer = auth.GetDefaultAuth() 44 | } 45 | 46 | if authenticator == nil { 47 | authenticator = auth.NewJWTAutenticatorWithUsers(srv.userStore) 48 | } 49 | 50 | middleware := auth.NewCasbinMiddleware(authorizer, authenticator) 51 | mux.HandleFunc( 52 | fmt.Sprintf(`GET /get/{%s}`, KEY_PARAM), middleware.HandleFunc(srv.HandlerGetById)) 53 | mux.HandleFunc("POST /set", middleware.HandleFunc(srv.HandlerSet)) 54 | mux.HandleFunc(fmt.Sprintf(`DELETE /delete/{%s}`, KEY_PARAM), middleware.HandleFunc(srv.HandlerDelete)) 55 | mux.HandleFunc("POST /login", srv.HandlerLogin) 56 | mux.HandleFunc( 57 | fmt.Sprintf(`GET /collections/{%s}`, KEY_PARAM), middleware.HandleFunc(srv.HandlerGetCollection)) 58 | mux.HandleFunc( 59 | `GET /collections`, middleware.HandleFunc(srv.HandlerGetCollections)) 60 | mux.HandleFunc(fmt.Sprintf("POST /collections/{%s}", COLLECTION_NAME_PARAM), middleware.HandleFunc(srv.HandlerCreateCollection)) 61 | mux.HandleFunc(fmt.Sprintf(`DELETE /collections/{%s}`, COLLECTION_NAME_PARAM), middleware.HandleFunc(srv.HandlerDeleteCollection)) 62 | mux.HandleFunc( 63 | fmt.Sprintf(`GET /collections/{%s}/get/{%s}`, COLLECTION_NAME_PARAM, KEY_PARAM), middleware.HandleFunc(srv.HandlerCollectionGetById)) 64 | mux.HandleFunc( 65 | fmt.Sprintf(`GET /collections/{%s}/items`, COLLECTION_NAME_PARAM), middleware.HandleFunc(srv.HandlerGetPaginatedCollectionItems)) 66 | mux.HandleFunc(fmt.Sprintf("POST /collections/{%s}/set", COLLECTION_NAME_PARAM), middleware.HandleFunc(srv.HandlerCollectionSet)) 67 | mux.HandleFunc(fmt.Sprintf(`DELETE /collections/{%s}/delete/{%s}`, COLLECTION_NAME_PARAM, KEY_PARAM), middleware.HandleFunc(srv.HandlerCollectionDelete)) 68 | 69 | // Wrap the mux with the CORS handler 70 | corsHandler := srv.setupCORS(mux) 71 | // Create a new ServeMux that uses the CORS handler. 72 | finalMux := http.NewServeMux() 73 | finalMux.Handle("/", corsHandler) 74 | 75 | return finalMux 76 | } 77 | 78 | func (srv *DareServer) HandlerGetById(w http.ResponseWriter, r *http.Request) { 79 | if r.Method != http.MethodGet { 80 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 81 | return 82 | } 83 | 84 | key := r.PathValue(KEY_PARAM) 85 | if key == "" { 86 | http.Error(w, `url path param "key" cannot be empty`, http.StatusBadRequest) 87 | return 88 | } 89 | 90 | val := srv.collectionManager.GetDefaultCollection().Get(key) 91 | if val == "" { 92 | http.Error(w, fmt.Sprintf(`Key "%v" not found`, key), http.StatusNotFound) 93 | return 94 | } 95 | 96 | response, err := json.Marshal(map[string]string{key: val}) 97 | if err != nil { 98 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 99 | return 100 | } 101 | 102 | w.Header().Set("Content-Type", "application/json") 103 | w.Write(response) 104 | } 105 | 106 | func (srv *DareServer) HandlerCollectionGetById(w http.ResponseWriter, r *http.Request) { 107 | if r.Method != http.MethodGet { 108 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 109 | return 110 | } 111 | 112 | collectionName := r.PathValue(COLLECTION_NAME_PARAM) 113 | collection, exists := srv.collectionManager.GetCollection(collectionName) 114 | if !exists { 115 | http.Error(w, fmt.Sprintf(`Collection "%s" not found`, collectionName), http.StatusNotFound) 116 | return 117 | } 118 | 119 | key := r.PathValue(KEY_PARAM) 120 | if key == "" { 121 | http.Error(w, `url path param "key" cannot be empty`, http.StatusBadRequest) 122 | return 123 | } 124 | 125 | val := collection.Get(key) 126 | if val == "" { 127 | http.Error(w, fmt.Sprintf(`Key "%v" not found`, key), http.StatusNotFound) 128 | return 129 | } 130 | 131 | response, err := json.Marshal(map[string]string{key: val}) 132 | if err != nil { 133 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 134 | return 135 | } 136 | 137 | w.Header().Set("Content-Type", "application/json") 138 | w.Write(response) 139 | 140 | } 141 | 142 | func (srv *DareServer) HandlerGetPaginatedCollectionItems(w http.ResponseWriter, r *http.Request) { 143 | if r.Method != http.MethodGet { 144 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 145 | return 146 | } 147 | 148 | // Parse the page and pageSize from query parameters 149 | page := parseQueryParam(r, "page", 1) 150 | pageSize := parseQueryParam(r, "pageSize", 10) 151 | 152 | collectionName := r.PathValue(COLLECTION_NAME_PARAM) 153 | collection, exists := srv.collectionManager.GetCollection(collectionName) 154 | if !exists { 155 | http.Error(w, fmt.Sprintf(`Collection "%s" not found`, collectionName), http.StatusNotFound) 156 | return 157 | } 158 | 159 | // Retrieve paginated items 160 | items := collection.GetAllItems() 161 | paginatedItems := paginateItems(items, page, pageSize) 162 | 163 | response, err := json.Marshal(map[string]interface{}{ 164 | "items": paginatedItems, 165 | "page": page, 166 | "pageSize": pageSize, 167 | }) 168 | if err != nil { 169 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 170 | return 171 | } 172 | 173 | w.Header().Set("Content-Type", "application/json") 174 | w.Write(response) 175 | } 176 | 177 | type Item struct { 178 | Key string `json:"key"` 179 | Value string `json:"value"` 180 | } 181 | 182 | func paginateItems(items map[string]string, page, pageSize int) []Item { 183 | keys := make([]string, 0, len(items)) 184 | for key := range items { 185 | keys = append(keys, key) 186 | } 187 | 188 | totalItems := len(keys) 189 | startIndex := (page - 1) * pageSize 190 | if startIndex >= totalItems { 191 | return []Item{} // Return empty array if page exceeds total items 192 | } 193 | 194 | endIndex := startIndex + pageSize 195 | if endIndex > totalItems { 196 | endIndex = totalItems 197 | } 198 | 199 | // Crea una slice di Item per contenere gli elementi paginati 200 | paginatedItems := make([]Item, 0, endIndex-startIndex) 201 | for _, key := range keys[startIndex:endIndex] { 202 | paginatedItems = append(paginatedItems, Item{ 203 | Key: key, 204 | Value: items[key], 205 | }) 206 | } 207 | 208 | return paginatedItems 209 | } 210 | 211 | func parseQueryParam(r *http.Request, key string, defaultValue int) int { 212 | queryValue := r.URL.Query().Get(key) 213 | if queryValue == "" { 214 | return defaultValue 215 | } 216 | parsedValue, err := strconv.Atoi(queryValue) 217 | if err != nil { 218 | return defaultValue 219 | } 220 | return parsedValue 221 | } 222 | 223 | func (srv *DareServer) HandlerSet(w http.ResponseWriter, r *http.Request) { 224 | if r.Method != http.MethodPost { 225 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 226 | return 227 | } 228 | 229 | var data map[string]string 230 | err := json.NewDecoder(r.Body).Decode(&data) 231 | if err != nil { 232 | http.Error(w, "Invalid JSON format, the body must be in the form of {\"key\": \"value\"}", http.StatusBadRequest) 233 | return 234 | } 235 | 236 | for key, value := range data { 237 | err = srv.collectionManager.GetDefaultCollection().Set(key, value) 238 | if err != nil { 239 | http.Error(w, "Error saving data", http.StatusInternalServerError) 240 | return 241 | } 242 | } 243 | 244 | w.WriteHeader(http.StatusCreated) 245 | } 246 | 247 | func (srv *DareServer) HandlerCollectionSet(w http.ResponseWriter, r *http.Request) { 248 | if r.Method != http.MethodPost { 249 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 250 | return 251 | } 252 | 253 | var data map[string]string 254 | err := json.NewDecoder(r.Body).Decode(&data) 255 | if err != nil { 256 | http.Error(w, "Invalid JSON format, the body must be in the form of {\"key\": \"value\"}", http.StatusBadRequest) 257 | return 258 | } 259 | 260 | collectionName := r.PathValue(COLLECTION_NAME_PARAM) 261 | collection, exists := srv.collectionManager.GetCollection(collectionName) 262 | if !exists { 263 | srv.collectionManager.AddCollection(collectionName) 264 | collection, _ = srv.collectionManager.GetCollection(collectionName) 265 | } 266 | 267 | for key, value := range data { 268 | err = collection.Set(key, value) 269 | if err != nil { 270 | http.Error(w, "Error saving data", http.StatusInternalServerError) 271 | return 272 | } 273 | } 274 | 275 | w.WriteHeader(http.StatusCreated) 276 | } 277 | 278 | func (srv *DareServer) HandlerDelete(w http.ResponseWriter, r *http.Request) { 279 | if r.Method != http.MethodDelete { 280 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 281 | return 282 | } 283 | 284 | key := r.PathValue(KEY_PARAM) 285 | if key == "" { 286 | http.Error(w, `url path param "key" cannot be empty`, http.StatusBadRequest) 287 | return 288 | } 289 | 290 | err := srv.collectionManager.GetDefaultCollection().Delete(key) 291 | if err != nil { 292 | http.Error(w, "Error deleting data", http.StatusInternalServerError) 293 | return 294 | } 295 | w.WriteHeader(http.StatusOK) 296 | } 297 | 298 | func (srv *DareServer) HandlerCollectionDelete(w http.ResponseWriter, r *http.Request) { 299 | if r.Method != http.MethodDelete { 300 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 301 | return 302 | } 303 | 304 | collectionName := r.PathValue(COLLECTION_NAME_PARAM) 305 | collection, exists := srv.collectionManager.GetCollection(collectionName) 306 | if !exists { 307 | http.Error(w, fmt.Sprintf(`Collection "%s" not found`, collectionName), http.StatusNotFound) 308 | return 309 | } 310 | 311 | key := r.PathValue(KEY_PARAM) 312 | if key == "" { 313 | http.Error(w, `url path param "key" cannot be empty`, http.StatusBadRequest) 314 | return 315 | } 316 | 317 | err := collection.Delete(key) 318 | if err != nil { 319 | http.Error(w, "Error deleting data", http.StatusInternalServerError) 320 | return 321 | } 322 | w.WriteHeader(http.StatusOK) 323 | } 324 | 325 | func (srv *DareServer) HandlerLogin(w http.ResponseWriter, r *http.Request) { 326 | if r.Method != http.MethodPost { 327 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 328 | return 329 | } 330 | 331 | username, password, ok := r.BasicAuth() 332 | if !ok || !srv.userStore.ValidateCredentials(username, password) { 333 | http.Error(w, "Unauthorized: missing or invalid credentials", http.StatusUnauthorized) 334 | return 335 | } 336 | 337 | authenticator := auth.NewJWTAutenticatorWithUsers(srv.userStore) 338 | token, err := authenticator.GenerateToken(username) 339 | if err != nil { 340 | http.Error(w, "Failed to generate token", http.StatusInternalServerError) 341 | return 342 | } 343 | srv.userStore.SaveToken(username, token) 344 | 345 | w.WriteHeader(http.StatusOK) 346 | w.Header().Set("Content-Type", "application/json") 347 | err = json.NewEncoder(w).Encode(map[string]string{ 348 | "token": token, 349 | }) 350 | 351 | if err != nil { 352 | http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) 353 | return 354 | } 355 | } 356 | 357 | func (srv *DareServer) HandlerGetCollection(w http.ResponseWriter, r *http.Request) { 358 | if r.Method != http.MethodGet { 359 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 360 | return 361 | } 362 | 363 | collectionName := r.PathValue(COLLECTION_NAME_PARAM) 364 | collection, exists := srv.collectionManager.GetCollection(collectionName) 365 | if !exists { 366 | http.Error(w, fmt.Sprintf(`Collection "%s" not found`, collectionName), http.StatusNotFound) 367 | return 368 | } 369 | 370 | key := r.PathValue(KEY_PARAM) 371 | if key == "" { 372 | http.Error(w, `url path param "key" cannot be empty`, http.StatusBadRequest) 373 | return 374 | } 375 | 376 | val := collection.Get(key) 377 | if val == "" { 378 | http.Error(w, fmt.Sprintf(`Key "%v" not found`, key), http.StatusNotFound) 379 | return 380 | } 381 | 382 | response, err := json.Marshal(map[string]string{key: val}) 383 | if err != nil { 384 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 385 | return 386 | } 387 | 388 | w.Header().Set("Content-Type", "application/json") 389 | w.Write(response) 390 | } 391 | 392 | func (srv *DareServer) HandlerGetCollections(w http.ResponseWriter, r *http.Request) { 393 | if r.Method != http.MethodGet { 394 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 395 | return 396 | } 397 | 398 | response, err := json.Marshal(srv.collectionManager.GetCollectionNames()) 399 | if err != nil { 400 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 401 | return 402 | } 403 | 404 | w.Header().Set("Content-Type", "application/json") 405 | w.Write(response) 406 | 407 | } 408 | 409 | func (srv *DareServer) HandlerCreateCollection(w http.ResponseWriter, r *http.Request) { 410 | if r.Method != http.MethodPost { 411 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 412 | return 413 | } 414 | 415 | collectionName := r.PathValue(COLLECTION_NAME_PARAM) 416 | _, exists := srv.collectionManager.GetCollection(collectionName) 417 | if exists { 418 | http.Error(w, fmt.Sprintf(`Collection "%s" already exists`, collectionName), http.StatusBadRequest) 419 | return 420 | } 421 | 422 | srv.collectionManager.AddCollection(collectionName) 423 | 424 | w.WriteHeader(http.StatusCreated) 425 | } 426 | 427 | func (srv *DareServer) HandlerDeleteCollection(w http.ResponseWriter, r *http.Request) { 428 | if r.Method != http.MethodDelete { 429 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 430 | return 431 | } 432 | 433 | collectionName := r.PathValue(COLLECTION_NAME_PARAM) 434 | _, exists := srv.collectionManager.GetCollection(collectionName) 435 | if !exists { 436 | http.Error(w, fmt.Sprintf(`Collection "%s" not exists`, collectionName), http.StatusBadRequest) 437 | return 438 | } 439 | 440 | srv.collectionManager.RemoveCollection(collectionName) 441 | w.WriteHeader(http.StatusOK) 442 | } 443 | 444 | func (srv *DareServer) setupCORS(next http.Handler) http.Handler { 445 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 446 | w.Header().Set("Access-Control-Allow-Origin", "http://127.0.0.1:5002") // Or "*" for all origins (less secure) 447 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 448 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 449 | w.Header().Set("Access-Control-Allow-Credentials", "true") 450 | 451 | if r.Method == "OPTIONS" { 452 | w.WriteHeader(http.StatusOK) 453 | return 454 | } 455 | 456 | next.ServeHTTP(w, r) 457 | }) 458 | } 459 | -------------------------------------------------------------------------------- /server/dare-server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/dmarro89/dare-db/auth" 12 | "github.com/dmarro89/dare-db/database" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestServer_SetAndGet(t *testing.T) { 18 | db := database.NewDatabase() 19 | srv := NewDareServer(db, auth.NewUserStore()) 20 | 21 | setWrongResponse := httptest.NewRecorder() 22 | setWrongRequest, _ := http.NewRequest("GET", "/set", bytes.NewBuffer([]byte{})) 23 | 24 | srv.HandlerSet(setWrongResponse, setWrongRequest) 25 | assert.Equal(t, http.StatusMethodNotAllowed, setWrongResponse.Code, "Method not allowed") 26 | 27 | setWrongFormatRequest, _ := http.NewRequest("POST", "/set", bytes.NewBuffer([]byte("plainText"))) 28 | setWrongFormatResponse := httptest.NewRecorder() 29 | srv.HandlerSet(setWrongFormatResponse, setWrongFormatRequest) 30 | assert.Equal(t, http.StatusBadRequest, setWrongFormatResponse.Code, "Invalid JSON format, the body must be in the form of {\"key\": \"value\"}") 31 | 32 | setData := map[string]string{"testKey": "testValue"} 33 | setDataJSON, _ := json.Marshal(setData) 34 | setRequest, _ := http.NewRequest("POST", "/set", bytes.NewBuffer(setDataJSON)) 35 | 36 | setResponse := httptest.NewRecorder() 37 | srv.HandlerSet(setResponse, setRequest) 38 | assert.Equal(t, http.StatusCreated, setResponse.Code, "Expected status %d, got %d", http.StatusCreated, setResponse.Code) 39 | 40 | getEmptyRequest, _ := http.NewRequest("GET", "/get", nil) 41 | getEmptyResponse := httptest.NewRecorder() 42 | srv.HandlerGetById(getEmptyResponse, getEmptyRequest) 43 | assert.Equal(t, http.StatusBadRequest, getEmptyResponse.Code, `url param query "key" cannot be empty`) 44 | 45 | getMissingKeyRequest, _ := http.NewRequest("GET", "/get/missingKey", nil) 46 | getMissingKeyRequest.SetPathValue("key", "missingKey") 47 | getMissingKeyResponse := httptest.NewRecorder() 48 | srv.HandlerGetById(getMissingKeyResponse, getMissingKeyRequest) 49 | assert.Equal(t, http.StatusNotFound, getMissingKeyResponse.Code, `Key "%s" not found`, "missingKey") 50 | 51 | getRequest, _ := http.NewRequest("GET", "/get/testKey", nil) 52 | getRequest.SetPathValue("key", "testKey") 53 | getResponse := httptest.NewRecorder() 54 | 55 | srv.HandlerGetById(getResponse, getRequest) 56 | 57 | assert.Equal(t, http.StatusOK, getResponse.Code, "Expected status %d, got %d", http.StatusOK, getResponse.Code) 58 | 59 | var getResult map[string]string 60 | err := json.Unmarshal(getResponse.Body.Bytes(), &getResult) 61 | assert.Nil(t, err, "Error decoding JSON response") 62 | 63 | for key, value := range getResult { 64 | assert.Equal(t, key, "testKey", "Unexpected response body: %v", getResult) 65 | assert.Equal(t, value, "testValue", "Unexpected response body: %v", getResult) 66 | } 67 | } 68 | 69 | func TestServer_SetAndDelete(t *testing.T) { 70 | db := database.NewDatabase() 71 | srv := NewDareServer(db, auth.NewUserStore()) 72 | 73 | setData := map[string]string{"testKey": "testValue"} 74 | setDataJSON, _ := json.Marshal(setData) 75 | setRequest, _ := http.NewRequest("POST", "/set", bytes.NewBuffer(setDataJSON)) 76 | setResponse := httptest.NewRecorder() 77 | 78 | srv.HandlerSet(setResponse, setRequest) 79 | 80 | assert.Equal(t, http.StatusCreated, setResponse.Code, "Expected status %d, got %d", http.StatusCreated, setResponse.Code) 81 | 82 | deleteWrongResponse := httptest.NewRecorder() 83 | deleteWrongRequest, _ := http.NewRequest("GET", "/delete", nil) 84 | srv.HandlerDelete(deleteWrongResponse, deleteWrongRequest) 85 | assert.Equal(t, http.StatusMethodNotAllowed, deleteWrongResponse.Code, "Method not allowed") 86 | 87 | deleteEmptyRequest, _ := http.NewRequest("DELETE", "/delete", nil) 88 | deleteEmptyResponse := httptest.NewRecorder() 89 | srv.HandlerDelete(deleteEmptyResponse, deleteEmptyRequest) 90 | assert.Equal(t, http.StatusBadRequest, deleteEmptyResponse.Code, `url param query "key" cannot be empty`) 91 | 92 | deleteRequest, _ := http.NewRequest("DELETE", "/delete/testKey", nil) 93 | deleteRequest.SetPathValue("key", "testKey") 94 | deleteResponse := httptest.NewRecorder() 95 | 96 | srv.HandlerDelete(deleteResponse, deleteRequest) 97 | 98 | if deleteResponse.Code != http.StatusOK { 99 | t.Errorf("Expected status %d, got %d", http.StatusNoContent, deleteResponse.Code) 100 | } 101 | 102 | getRequest, _ := http.NewRequest("GET", "/get/testKey", nil) 103 | getRequest.SetPathValue("key", "testKey") 104 | getResponse := httptest.NewRecorder() 105 | 106 | srv.HandlerGetById(getResponse, getRequest) 107 | 108 | if getResponse.Code != http.StatusNotFound { 109 | t.Errorf("Expected status %d, got %d", http.StatusNotFound, getResponse.Code) 110 | } 111 | } 112 | 113 | const RBAC_MODEL_CONTENT = `[request_definition] 114 | r = sub, obj, act 115 | 116 | [policy_definition] 117 | p = sub, obj, act 118 | 119 | [role_definition] 120 | g = _, _ 121 | 122 | [policy_effect] 123 | e = some(where (p.eft == allow)) 124 | 125 | [matchers] 126 | m = g(r.sub, p.sub) && (p.obj == "*" || keyMatch(r.obj, p.obj)) && regexMatch(r.act, p.act) 127 | ` 128 | 129 | const RBAC_POLICY = `p, role1, *, GET 130 | p, role2, *, POST 131 | 132 | g, user1, role1 133 | g, user2, role2 134 | ` 135 | 136 | func TestCreateMux(t *testing.T) { 137 | modelFile, err := os.CreateTemp("", "rbac_model.conf") 138 | if err != nil { 139 | t.Fatalf("Error creating rbac model file: %v", err) 140 | } 141 | defer os.Remove(modelFile.Name()) 142 | 143 | policyFile, err := os.CreateTemp("", "rbac_policy.csv") 144 | if err != nil { 145 | t.Fatalf("Error creating rbac policy file: %v", err) 146 | } 147 | defer os.Remove(policyFile.Name()) 148 | 149 | if _, err := modelFile.Write([]byte(RBAC_MODEL_CONTENT)); err != nil { 150 | t.Fatalf("Errorwriting rbac model file: %v", err) 151 | } 152 | if err := modelFile.Close(); err != nil { 153 | t.Fatalf("Error closing rbac model file: %v", err) 154 | } 155 | 156 | if _, err := policyFile.Write([]byte(RBAC_POLICY)); err != nil { 157 | t.Fatalf("Error creating policy file: %v", err) 158 | } 159 | if err := policyFile.Close(); err != nil { 160 | t.Fatalf("Error closing policy file: %v", err) 161 | } 162 | 163 | // Create a new instance of DareServer 164 | db := database.NewDatabase() 165 | srv := NewDareServer(db, auth.NewUserStore()) 166 | 167 | // Create a new ServeMux using the CreateMux method 168 | mux := srv.CreateMux(auth.NewCasbinAuth(modelFile.Name(), policyFile.Name(), auth.Users{ 169 | "user1": {Roles: []string{"role1"}}, "user2": {Roles: []string{"role2"}}, 170 | }), auth.NewJWTAutenticator()) 171 | assert.Equal(t, mux != nil, true) 172 | } 173 | 174 | func TestMiddleware_ProtectedEndpoints(t *testing.T) { 175 | modelFile, err := os.CreateTemp("", "rbac_model.conf") 176 | if err != nil { 177 | t.Fatalf("Error creating rbac model file: %v", err) 178 | } 179 | defer os.Remove(modelFile.Name()) 180 | 181 | policyFile, err := os.CreateTemp("", "rbac_policy.csv") 182 | if err != nil { 183 | t.Fatalf("Error creating rbac policy file: %v", err) 184 | } 185 | defer os.Remove(policyFile.Name()) 186 | 187 | if _, err := modelFile.Write([]byte(RBAC_MODEL_CONTENT)); err != nil { 188 | t.Fatalf("Errorwriting rbac model file: %v", err) 189 | } 190 | if err := modelFile.Close(); err != nil { 191 | t.Fatalf("Error closing rbac model file: %v", err) 192 | } 193 | 194 | if _, err := policyFile.Write([]byte(RBAC_POLICY)); err != nil { 195 | t.Fatalf("Error creating policy file: %v", err) 196 | } 197 | if err := policyFile.Close(); err != nil { 198 | t.Fatalf("Error closing policy file: %v", err) 199 | } 200 | 201 | db := database.NewDatabase() 202 | 203 | usersStore := auth.NewUserStore() 204 | usersStore.AddUser("user1", "password") 205 | usersStore.AddUser("user2", "password") 206 | 207 | srv := NewDareServer(db, usersStore) 208 | 209 | authenticator := auth.NewJWTAutenticatorWithUsers(usersStore) 210 | mux := srv.CreateMux(auth.NewCasbinAuth(modelFile.Name(), policyFile.Name(), auth.Users{ 211 | "user1": {Roles: []string{"role1"}}, "user2": {Roles: []string{"role2"}}, 212 | }), authenticator) 213 | 214 | req, err := http.NewRequest("POST", "/login", nil) 215 | require.NoError(t, err) 216 | req.SetBasicAuth("user2", "password") 217 | 218 | rr := httptest.NewRecorder() 219 | mux.ServeHTTP(rr, req) 220 | 221 | require.Equal(t, http.StatusOK, rr.Code) 222 | 223 | var tokenResponse map[string]string 224 | json.NewDecoder(rr.Body).Decode(&tokenResponse) 225 | 226 | // Test POST request for a protected resource 227 | postData := map[string]string{"newKey": "newValue"} 228 | body, err := json.Marshal(postData) 229 | require.NoError(t, err) 230 | 231 | req, err = http.NewRequest("POST", "/set", bytes.NewReader(body)) 232 | require.NoError(t, err) 233 | req.Header.Set("Authorization", tokenResponse["token"]) 234 | 235 | rr = httptest.NewRecorder() 236 | mux.ServeHTTP(rr, req) 237 | 238 | // Check if the response status code is Created (201) 239 | require.Equal(t, http.StatusCreated, rr.Code) 240 | 241 | //Login user1 242 | req, err = http.NewRequest("POST", "/login", nil) 243 | require.NoError(t, err) 244 | req.SetBasicAuth("user1", "password") 245 | 246 | rr = httptest.NewRecorder() 247 | mux.ServeHTTP(rr, req) 248 | json.NewDecoder(rr.Body).Decode(&tokenResponse) 249 | 250 | // Test GET request for a protected resource 251 | req, err = http.NewRequest("GET", "/get/newKey", nil) 252 | require.NoError(t, err) 253 | req.Header.Set("Authorization", tokenResponse["token"]) 254 | 255 | rr = httptest.NewRecorder() 256 | mux.ServeHTTP(rr, req) 257 | 258 | // Check if the response status code is OK (200) 259 | require.Equal(t, http.StatusOK, rr.Code) 260 | require.Contains(t, rr.Body.String(), string(body)) 261 | 262 | // Test DELETE request for a protected resource 263 | req, err = http.NewRequest("DELETE", "/delete/existingKey", nil) 264 | require.NoError(t, err) 265 | req.Header.Set("Authorization", tokenResponse["token"]) 266 | 267 | rr = httptest.NewRecorder() 268 | mux.ServeHTTP(rr, req) 269 | 270 | // Check if the response status code is OK (200) 271 | require.Equal(t, http.StatusForbidden, rr.Code) 272 | 273 | // Test accessing without proper credentials 274 | req, err = http.NewRequest("GET", "/get/existingKey", nil) 275 | require.NoError(t, err) 276 | // Not setting basic auth here to simulate missing credentials 277 | 278 | rr = httptest.NewRecorder() 279 | mux.ServeHTTP(rr, req) 280 | 281 | // Check if the response status code is Unauthorized (401) 282 | require.Equal(t, http.StatusUnauthorized, rr.Code) 283 | } 284 | 285 | func TestDareServer_HandlerLogin(t *testing.T) { 286 | usersStore := auth.NewUserStore() 287 | server := &DareServer{ 288 | userStore: usersStore, 289 | } 290 | 291 | // Adding a test user 292 | usersStore.AddUser("testuser", "testpassword") 293 | 294 | // Test case: valid login request 295 | req, err := http.NewRequest(http.MethodPost, "/login", nil) 296 | assert.NoError(t, err) 297 | 298 | req.SetBasicAuth("testuser", "testpassword") 299 | rr := httptest.NewRecorder() 300 | 301 | handler := http.HandlerFunc(server.HandlerLogin) 302 | handler.ServeHTTP(rr, req) 303 | 304 | assert.Equal(t, http.StatusOK, rr.Code) 305 | 306 | var responseBody map[string]string 307 | err = json.NewDecoder(rr.Body).Decode(&responseBody) 308 | assert.NoError(t, err) 309 | assert.Contains(t, responseBody, "token") 310 | assert.NotEmpty(t, responseBody["token"]) 311 | 312 | // Test case: invalid method 313 | req, err = http.NewRequest(http.MethodGet, "/login", nil) 314 | assert.NoError(t, err) 315 | 316 | rr = httptest.NewRecorder() 317 | handler.ServeHTTP(rr, req) 318 | 319 | assert.Equal(t, http.StatusMethodNotAllowed, rr.Code) 320 | 321 | // Test case: missing credentials 322 | req, err = http.NewRequest(http.MethodPost, "/login", nil) 323 | assert.NoError(t, err) 324 | 325 | rr = httptest.NewRecorder() 326 | handler.ServeHTTP(rr, req) 327 | 328 | assert.Equal(t, http.StatusUnauthorized, rr.Code) 329 | 330 | // Test case: invalid credentials 331 | req, err = http.NewRequest(http.MethodPost, "/login", nil) 332 | assert.NoError(t, err) 333 | 334 | req.SetBasicAuth("testuser", "wrongpassword") 335 | rr = httptest.NewRecorder() 336 | handler.ServeHTTP(rr, req) 337 | 338 | assert.Equal(t, http.StatusUnauthorized, rr.Code) 339 | } 340 | 341 | func TestHandlerCollectionGetById(t *testing.T) { 342 | // Setup 343 | srv := &DareServer{ 344 | collectionManager: database.NewCollectionManager(), 345 | } 346 | srv.collectionManager.AddCollection("test-collection") 347 | collection, _ := srv.collectionManager.GetCollection("test-collection") 348 | collection.Set("test-key", "test-value") 349 | 350 | req := httptest.NewRequest(http.MethodGet, "/test-collection/test-key", nil) 351 | w := httptest.NewRecorder() 352 | 353 | // Set PathValue as if from a router 354 | req.SetPathValue(COLLECTION_NAME_PARAM, "test-collection") 355 | req.SetPathValue(KEY_PARAM, "test-key") 356 | 357 | // Execute 358 | srv.HandlerCollectionGetById(w, req) 359 | 360 | // Assert 361 | resp := w.Result() 362 | assert.Equal(t, http.StatusOK, resp.StatusCode) 363 | 364 | var body map[string]string 365 | err := json.NewDecoder(resp.Body).Decode(&body) 366 | assert.NoError(t, err) 367 | assert.Equal(t, "test-value", body["test-key"]) 368 | } 369 | 370 | func TestHandlerCollectionSet(t *testing.T) { 371 | // Setup 372 | srv := &DareServer{ 373 | collectionManager: database.NewCollectionManager(), 374 | } 375 | srv.collectionManager.AddCollection("test-collection") 376 | 377 | data := map[string]string{"test-key": "test-value"} 378 | jsonData, _ := json.Marshal(data) 379 | 380 | req := httptest.NewRequest(http.MethodPost, "/test-collection", bytes.NewBuffer(jsonData)) 381 | w := httptest.NewRecorder() 382 | 383 | // Set PathValue as if from a router 384 | req.SetPathValue(COLLECTION_NAME_PARAM, "test-collection") 385 | 386 | // Execute 387 | srv.HandlerCollectionSet(w, req) 388 | 389 | // Assert 390 | resp := w.Result() 391 | assert.Equal(t, http.StatusCreated, resp.StatusCode) 392 | 393 | // Verify that the key-value pair was stored 394 | collection, _ := srv.collectionManager.GetCollection("test-collection") 395 | val := collection.Get("test-key") 396 | assert.Equal(t, "test-value", val) 397 | } 398 | 399 | func TestHandlerCollectionDelete(t *testing.T) { 400 | // Setup 401 | srv := &DareServer{ 402 | collectionManager: database.NewCollectionManager(), 403 | } 404 | srv.collectionManager.AddCollection("test-collection") 405 | collection, _ := srv.collectionManager.GetCollection("test-collection") 406 | collection.Set("test-key", "test-value") 407 | 408 | req := httptest.NewRequest(http.MethodDelete, "/test-collection/test-key", nil) 409 | w := httptest.NewRecorder() 410 | 411 | // Set PathValue as if from a router 412 | req.SetPathValue(COLLECTION_NAME_PARAM, "test-collection") 413 | req.SetPathValue(KEY_PARAM, "test-key") 414 | 415 | // Execute 416 | srv.HandlerCollectionDelete(w, req) 417 | 418 | // Assert 419 | resp := w.Result() 420 | assert.Equal(t, http.StatusOK, resp.StatusCode) 421 | 422 | // Verify the key was deleted 423 | val := collection.Get("test-key") 424 | assert.Equal(t, "", val) 425 | } 426 | 427 | func TestHandlerGetCollections(t *testing.T) { 428 | // Setup 429 | srv := &DareServer{ 430 | collectionManager: database.NewCollectionManager(), 431 | } 432 | srv.collectionManager.AddCollection("collection1") 433 | srv.collectionManager.AddCollection("collection2") 434 | 435 | req := httptest.NewRequest(http.MethodGet, "/collections", nil) 436 | w := httptest.NewRecorder() 437 | 438 | // Execute 439 | srv.HandlerGetCollections(w, req) 440 | 441 | // Assert 442 | resp := w.Result() 443 | assert.Equal(t, http.StatusOK, resp.StatusCode) 444 | 445 | var collections []string 446 | err := json.NewDecoder(resp.Body).Decode(&collections) 447 | assert.NoError(t, err) 448 | assert.ElementsMatch(t, []string{"collection1", "collection2"}, collections) 449 | } 450 | 451 | func TestHandlerCreateCollection(t *testing.T) { 452 | // Setup 453 | srv := &DareServer{ 454 | collectionManager: database.NewCollectionManager(), 455 | } 456 | 457 | req := httptest.NewRequest(http.MethodPost, "/test-collection", nil) 458 | w := httptest.NewRecorder() 459 | 460 | // Set PathValue as if from a router 461 | req.SetPathValue(COLLECTION_NAME_PARAM, "test-collection") 462 | 463 | // Execute 464 | srv.HandlerCreateCollection(w, req) 465 | 466 | // Assert 467 | resp := w.Result() 468 | assert.Equal(t, http.StatusCreated, resp.StatusCode) 469 | 470 | // Verify the collection was created 471 | _, exists := srv.collectionManager.GetCollection("test-collection") 472 | assert.True(t, exists) 473 | } 474 | 475 | func TestHandlerDeleteCollection(t *testing.T) { 476 | // Setup 477 | srv := &DareServer{ 478 | collectionManager: database.NewCollectionManager(), 479 | } 480 | srv.collectionManager.AddCollection("test-collection") 481 | 482 | req := httptest.NewRequest(http.MethodDelete, "/test-collection", nil) 483 | w := httptest.NewRecorder() 484 | 485 | // Set PathValue as if from a router 486 | req.SetPathValue(COLLECTION_NAME_PARAM, "test-collection") 487 | 488 | // Execute 489 | srv.HandlerDeleteCollection(w, req) 490 | 491 | // Assert 492 | resp := w.Result() 493 | assert.Equal(t, http.StatusOK, resp.StatusCode) 494 | 495 | // Verify the collection was deleted 496 | _, exists := srv.collectionManager.GetCollection("test-collection") 497 | assert.False(t, exists) 498 | } 499 | 500 | // Test handler for successful pagination 501 | func TestHandlerGetPaginatedItems_Success(t *testing.T) { 502 | collectionManager := database.NewCollectionManager() 503 | collectionManager.AddCollection(database.DEFAULT_COLLECTION) 504 | srv := &DareServer{ 505 | collectionManager: collectionManager, 506 | } 507 | // Mock the default collection and add multiple items 508 | collection := srv.collectionManager.GetDefaultCollection() 509 | collection.Set("key1", "value1") 510 | collection.Set("key2", "value2") 511 | collection.Set("key3", "value3") 512 | collection.Set("key4", "value4") 513 | 514 | // Simulate HTTP GET request for page 1 with 2 items per page 515 | req := httptest.NewRequest(http.MethodGet, "/items?page=1&pageSize=2", nil) 516 | w := httptest.NewRecorder() 517 | 518 | // Execute the handler 519 | req.SetPathValue(COLLECTION_NAME_PARAM, "default") 520 | srv.HandlerGetPaginatedCollectionItems(w, req) 521 | 522 | // Assert the response 523 | resp := w.Result() 524 | assert.Equal(t, http.StatusOK, resp.StatusCode) 525 | 526 | var responseBody map[string]interface{} 527 | err := json.NewDecoder(resp.Body).Decode(&responseBody) 528 | assert.NoError(t, err) 529 | 530 | items := responseBody["items"].([]interface{}) 531 | assert.Len(t, items, 2, "Expected 2 items in paginated response") 532 | 533 | assert.Equal(t, float64(1), responseBody["page"], "Expected page 1") 534 | assert.Equal(t, float64(2), responseBody["pageSize"], "Expected pageSize 2") 535 | } 536 | 537 | // Test handler for a different page 538 | func TestHandlerGetPaginatedItems_Page2(t *testing.T) { 539 | collectionManager := database.NewCollectionManager() 540 | collectionManager.AddCollection(database.DEFAULT_COLLECTION) 541 | srv := &DareServer{ 542 | collectionManager: collectionManager, 543 | } 544 | // Mock the default collection and add multiple items 545 | collection := srv.collectionManager.GetDefaultCollection() 546 | collection.Set("key1", "value1") 547 | collection.Set("key2", "value2") 548 | collection.Set("key3", "value3") 549 | collection.Set("key4", "value4") 550 | 551 | // Simulate HTTP GET request for page 2 with 2 items per page 552 | req := httptest.NewRequest(http.MethodGet, "/items?page=2&pageSize=2", nil) 553 | w := httptest.NewRecorder() 554 | 555 | // Execute the handler 556 | req.SetPathValue(COLLECTION_NAME_PARAM, "default") 557 | srv.HandlerGetPaginatedCollectionItems(w, req) 558 | 559 | // Assert the response 560 | resp := w.Result() 561 | assert.Equal(t, http.StatusOK, resp.StatusCode) 562 | 563 | var responseBody map[string]interface{} 564 | err := json.NewDecoder(resp.Body).Decode(&responseBody) 565 | assert.NoError(t, err) 566 | 567 | items := responseBody["items"].([]interface{}) 568 | assert.Len(t, items, 2, "Expected 2 items in paginated response") 569 | 570 | assert.Equal(t, float64(2), responseBody["page"], "Expected page 2") 571 | assert.Equal(t, float64(2), responseBody["pageSize"], "Expected pageSize 2") 572 | } 573 | 574 | // Test handler when no items are available for the given page 575 | func TestHandlerGetPaginatedItems_EmptyPage(t *testing.T) { 576 | collectionManager := database.NewCollectionManager() 577 | collectionManager.AddCollection(database.DEFAULT_COLLECTION) 578 | srv := &DareServer{ 579 | collectionManager: collectionManager, 580 | } 581 | 582 | // Mock the default collection and add only a few items 583 | collection := srv.collectionManager.GetDefaultCollection() 584 | collection.Set("key1", "value1") 585 | collection.Set("key2", "value2") 586 | 587 | // Simulate HTTP GET request for page 2 with 5 items per page (no data should exist for page 2) 588 | req := httptest.NewRequest(http.MethodGet, "/items?page=2&pageSize=5", nil) 589 | w := httptest.NewRecorder() 590 | 591 | // Execute the handler 592 | req.SetPathValue(COLLECTION_NAME_PARAM, "default") 593 | srv.HandlerGetPaginatedCollectionItems(w, req) 594 | 595 | // Assert the response 596 | resp := w.Result() 597 | assert.Equal(t, http.StatusOK, resp.StatusCode) 598 | 599 | var responseBody map[string]interface{} 600 | err := json.NewDecoder(resp.Body).Decode(&responseBody) 601 | assert.NoError(t, err) 602 | 603 | items := responseBody["items"].([]interface{}) 604 | assert.Len(t, items, 0, "Expected 0 items for empty page") 605 | 606 | assert.Equal(t, float64(2), responseBody["page"], "Expected page 2") 607 | assert.Equal(t, float64(5), responseBody["pageSize"], "Expected pageSize 5") 608 | } 609 | 610 | // Test handler for invalid method 611 | func TestHandlerGetPaginatedItems_InvalidMethod(t *testing.T) { 612 | collectionManager := database.NewCollectionManager() 613 | collectionManager.AddCollection(database.DEFAULT_COLLECTION) 614 | srv := &DareServer{ 615 | collectionManager: collectionManager, 616 | } 617 | 618 | // Simulate HTTP POST request (invalid method) 619 | req := httptest.NewRequest(http.MethodPost, "/items", nil) 620 | w := httptest.NewRecorder() 621 | 622 | // Execute the handler 623 | req.SetPathValue(COLLECTION_NAME_PARAM, "default") 624 | srv.HandlerGetPaginatedCollectionItems(w, req) 625 | 626 | // Assert the response 627 | resp := w.Result() 628 | assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode, "Expected status 405 Method Not Allowed") 629 | } 630 | 631 | func TestHandlerGetPaginatedCollectionItems_Success(t *testing.T) { 632 | srv := &DareServer{ 633 | collectionManager: database.NewCollectionManager(), 634 | } 635 | 636 | // Mock a collection with multiple items 637 | srv.collectionManager.AddCollection("test-collection") 638 | collection, _ := srv.collectionManager.GetCollection("test-collection") 639 | collection.Set("key1", "value1") 640 | collection.Set("key2", "value2") 641 | collection.Set("key3", "value3") 642 | collection.Set("key4", "value4") 643 | 644 | // Simulate HTTP GET request for page 1 with 2 items per page 645 | req := httptest.NewRequest(http.MethodGet, "/test-collection?page=1&pageSize=2", nil) 646 | w := httptest.NewRecorder() 647 | 648 | // Set path parameter as if from a router 649 | req.SetPathValue(COLLECTION_NAME_PARAM, "test-collection") 650 | 651 | // Execute the handler 652 | srv.HandlerGetPaginatedCollectionItems(w, req) 653 | 654 | // Assert the response 655 | resp := w.Result() 656 | assert.Equal(t, http.StatusOK, resp.StatusCode) 657 | 658 | var responseBody map[string]interface{} 659 | err := json.NewDecoder(resp.Body).Decode(&responseBody) 660 | assert.NoError(t, err) 661 | 662 | items := responseBody["items"].([]interface{}) 663 | assert.Len(t, items, 2, "Expected 2 items in paginated response") 664 | 665 | assert.Equal(t, float64(1), responseBody["page"], "Expected page 1") 666 | assert.Equal(t, float64(2), responseBody["pageSize"], "Expected pageSize 2") 667 | } 668 | 669 | // Test successful pagination for page 2 of a collection 670 | func TestHandlerGetPaginatedCollectionItems_Page2(t *testing.T) { 671 | srv := &DareServer{ 672 | collectionManager: database.NewCollectionManager(), 673 | } 674 | 675 | // Mock a collection with multiple items 676 | srv.collectionManager.AddCollection("test-collection") 677 | collection, _ := srv.collectionManager.GetCollection("test-collection") 678 | collection.Set("key1", "value1") 679 | collection.Set("key2", "value2") 680 | collection.Set("key3", "value3") 681 | collection.Set("key4", "value4") 682 | 683 | // Simulate HTTP GET request for page 2 with 2 items per page 684 | req := httptest.NewRequest(http.MethodGet, "/test-collection?page=2&pageSize=2", nil) 685 | w := httptest.NewRecorder() 686 | 687 | // Set path parameter as if from a router 688 | req.SetPathValue(COLLECTION_NAME_PARAM, "test-collection") 689 | 690 | // Execute the handler 691 | srv.HandlerGetPaginatedCollectionItems(w, req) 692 | 693 | // Assert the response 694 | resp := w.Result() 695 | assert.Equal(t, http.StatusOK, resp.StatusCode) 696 | 697 | var responseBody map[string]interface{} 698 | err := json.NewDecoder(resp.Body).Decode(&responseBody) 699 | assert.NoError(t, err) 700 | 701 | items := responseBody["items"].([]interface{}) 702 | assert.Len(t, items, 2, "Expected 2 items in paginated response") 703 | 704 | assert.Equal(t, float64(2), responseBody["page"], "Expected page 2") 705 | assert.Equal(t, float64(2), responseBody["pageSize"], "Expected pageSize 2") 706 | } 707 | 708 | // Test handler when no items are available for the given page 709 | func TestHandlerGetPaginatedCollectionItems_EmptyPage(t *testing.T) { 710 | srv := &DareServer{ 711 | collectionManager: database.NewCollectionManager(), 712 | } 713 | 714 | // Mock a collection with 2 items 715 | srv.collectionManager.AddCollection("test-collection") 716 | collection, _ := srv.collectionManager.GetCollection("test-collection") 717 | collection.Set("key1", "value1") 718 | collection.Set("key2", "value2") 719 | 720 | // Simulate HTTP GET request for page 2 with 5 items per page (no data should exist for page 2) 721 | req := httptest.NewRequest(http.MethodGet, "/test-collection?page=2&pageSize=5", nil) 722 | w := httptest.NewRecorder() 723 | 724 | // Set path parameter as if from a router 725 | req.SetPathValue(COLLECTION_NAME_PARAM, "test-collection") 726 | 727 | // Execute the handler 728 | srv.HandlerGetPaginatedCollectionItems(w, req) 729 | 730 | // Assert the response 731 | resp := w.Result() 732 | assert.Equal(t, http.StatusOK, resp.StatusCode) 733 | 734 | var responseBody map[string]interface{} 735 | err := json.NewDecoder(resp.Body).Decode(&responseBody) 736 | assert.NoError(t, err) 737 | 738 | items := responseBody["items"].([]interface{}) 739 | assert.Len(t, items, 0, "Expected 0 items for empty page") 740 | 741 | assert.Equal(t, float64(2), responseBody["page"], "Expected page 2") 742 | assert.Equal(t, float64(5), responseBody["pageSize"], "Expected pageSize 5") 743 | } 744 | 745 | // Test handler for invalid method 746 | func TestHandlerGetPaginatedCollectionItems_InvalidMethod(t *testing.T) { 747 | srv := &DareServer{ 748 | collectionManager: database.NewCollectionManager(), 749 | } 750 | 751 | // Simulate HTTP POST request (invalid method) 752 | req := httptest.NewRequest(http.MethodPost, "/test-collection", nil) 753 | w := httptest.NewRecorder() 754 | 755 | // Execute the handler 756 | srv.HandlerGetPaginatedCollectionItems(w, req) 757 | 758 | // Assert the response 759 | resp := w.Result() 760 | assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode, "Expected status 405 Method Not Allowed") 761 | } 762 | -------------------------------------------------------------------------------- /server/factory.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/dmarro89/dare-db/logger" 5 | ) 6 | 7 | type Factory struct { 8 | configuration Config 9 | logger logger.Logger 10 | } 11 | 12 | func NewFactory(configuration Config, logger logger.Logger) *Factory { 13 | return &Factory{configuration: configuration, logger: logger} 14 | } 15 | 16 | func (f *Factory) GetWebServer(dareServer IDare) Server { 17 | if f.configuration.GetBool("security.tls_enabled") { 18 | return NewHttpsServer(dareServer, f.configuration, f.logger) 19 | } 20 | 21 | return NewHttpServer(dareServer, f.configuration, f.logger) 22 | } 23 | -------------------------------------------------------------------------------- /server/factory_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/dmarro89/dare-db/auth" 9 | "github.com/dmarro89/dare-db/database" 10 | "github.com/dmarro89/dare-db/logger" 11 | "gotest.tools/assert" 12 | ) 13 | 14 | // TestMain runs setup before tests and teardown after tests 15 | func TestMain(m *testing.M) { 16 | 17 | // Init configuration first 18 | testConf := SetupTestConfiguration() 19 | fmt.Println("Test log file should be: ", testConf.Get("log.log_file")) 20 | 21 | // Run the tests 22 | code := m.Run() 23 | 24 | // Teardown code here 25 | os.Unsetenv("DARE_TLS_ENABLED") 26 | TeardownTestConfiguration() 27 | 28 | // Exit with the proper code 29 | os.Exit(code) 30 | } 31 | 32 | func TestNewServerFactory(t *testing.T) { 33 | factory := NewFactory(NewConfiguration(""), logger.NewDareLogger()) 34 | assert.Assert(t, factory != nil, "NewServerFactory() should not return nil") 35 | } 36 | 37 | func TestNewServerWithTlsEnabled(t *testing.T) { 38 | // Set the DARE_TLS_ENABLED environment variable to "true" 39 | t.Setenv("DARE_TLS_ENABLED", "true") 40 | 41 | factory := NewFactory(NewConfiguration(""), logger.NewDareLogger()) 42 | server := factory.GetWebServer(NewDareServer(database.NewDatabase(), auth.NewUserStore())) 43 | 44 | // Assert that the server is of type HttpsServer 45 | _, isHttpsServer := server.(*HttpsServer) 46 | assert.Assert(t, isHttpsServer, "NewServer() should return an HttpsServer when TLS_ENABLED is true") 47 | } 48 | 49 | func TestNewServerWithTlsDisabled(t *testing.T) { 50 | // Set the DARE_TLS_ENABLED environment variable to "false" 51 | t.Setenv("DARE_TLS_ENABLED", "false") 52 | 53 | factory := NewFactory(NewConfiguration(""), logger.NewDareLogger()) 54 | server := factory.GetWebServer(NewDareServer(database.NewDatabase(), auth.NewUserStore())) 55 | 56 | // Assert that the server is of type HttpServer 57 | _, isHttpServer := server.(*HttpServer) 58 | assert.Assert(t, isHttpServer, "NewServer() should return an HttpServer when TLS_ENABLED is false") 59 | } 60 | 61 | /* 62 | //FIXME: pass teh right config to the factory 63 | func TestGetTlsEnabled(t *testing.T) { 64 | factory := NewFactory() 65 | 66 | // Test when DARE_TLS_ENABLED is "true" 67 | t.Setenv("DARE_TLS_ENABLED", "true") 68 | //reReadConfigsFromEnvs(factory) 69 | assert.Assert(t, factory.getTLSEnabled(), "getTLSEnabled() should return true when TLS_ENABLED is 'true'") 70 | 71 | // Test when DARE_TLS_ENABLED is "false" 72 | t.Setenv("DARE_TLS_ENABLED", "false") 73 | //reReadConfigsFromEnvs() 74 | assert.Assert(t, !factory.getTLSEnabled(), "getTLSEnabled() should return false when TLS_ENABLED is 'false'") 75 | 76 | // Test when DARE_TLS_ENABLED is not set 77 | os.Unsetenv("DARE_TLS_ENABLED") 78 | assert.Assert(t, !factory.getTLSEnabled(), "getTLSEnabled() should return false when TLS_ENABLED is not set") 79 | } 80 | */ 81 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/dmarro89/dare-db/logger" 14 | ) 15 | 16 | type Server interface { 17 | Start() 18 | Stop() 19 | } 20 | 21 | type HttpServer struct { 22 | dareServer IDare 23 | httpServer *http.Server 24 | configuration Config 25 | sigChan chan os.Signal 26 | logger logger.Logger 27 | } 28 | 29 | func NewHttpServer(dareServer IDare, configuration Config, logger logger.Logger) *HttpServer { 30 | return &HttpServer{ 31 | dareServer: dareServer, 32 | configuration: configuration, 33 | sigChan: make(chan os.Signal, 1), 34 | logger: logger, 35 | } 36 | } 37 | 38 | func (server *HttpServer) Start() { 39 | if server.configuration.IsSet("log.log_file") { 40 | server.logger.Start(server.configuration.GetString("log.log_file")) 41 | } 42 | 43 | server.httpServer = &http.Server{ 44 | Addr: fmt.Sprintf("%s:%s", server.configuration.GetString("server.host"), server.configuration.GetString("server.port")), 45 | Handler: server.dareServer.CreateMux(nil, nil), 46 | } 47 | 48 | go func() { 49 | server.logger.Info("Serving new connections on: ", server.configuration.GetString("server.host"), ":", server.configuration.GetString("server.port")) 50 | if err := server.httpServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 51 | server.logger.Fatal("HTTP server error: %v", err) 52 | } 53 | server.logger.Info("Stopped serving new connections.") 54 | }() 55 | 56 | signal.Notify(server.sigChan, syscall.SIGINT, syscall.SIGTERM) 57 | <-server.sigChan 58 | } 59 | 60 | func (server *HttpServer) Stop() { 61 | shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) 62 | defer shutdownRelease() 63 | 64 | if err := server.httpServer.Shutdown(shutdownCtx); err != nil { 65 | server.logger.Fatal("HTTP shutdown error:", err) 66 | } 67 | 68 | server.logger.Info("Graceful shutdown complete.") 69 | server.httpServer = nil 70 | 71 | server.logger.Close() 72 | } 73 | 74 | type HttpsServer struct { 75 | dareServer IDare 76 | httpsServer *http.Server 77 | configuration Config 78 | sigChan chan os.Signal 79 | logger logger.Logger 80 | } 81 | 82 | func NewHttpsServer(dareServer IDare, configuration Config, logger logger.Logger) *HttpsServer { 83 | return &HttpsServer{ 84 | sigChan: make(chan os.Signal, 1), 85 | configuration: configuration, 86 | dareServer: dareServer, 87 | logger: logger, 88 | } 89 | } 90 | 91 | func (server *HttpsServer) Start() { 92 | server.httpsServer = &http.Server{ 93 | Addr: fmt.Sprintf("%s:%s", server.configuration.GetString("server.host"), server.configuration.GetString("server.port")), 94 | Handler: server.dareServer.CreateMux(nil, nil), 95 | } 96 | 97 | go func() { 98 | server.logger.Info("Serving new connections on: ", server.configuration.GetString("server.host"), ":", server.configuration.GetString("server.port")) 99 | server.logger.Info("Using certificate files. (1) ", server.configuration.GetString("security.cert_private"), " ; (2) ", server.configuration.GetString("security.cert_public")) 100 | 101 | if err := server.httpsServer.ListenAndServeTLS(server.configuration.GetString("security.cert_public"), server.configuration.GetString("security.cert_private")); !errors.Is(err, http.ErrServerClosed) { 102 | server.logger.Fatal("HTTPS server error: ", err) 103 | } 104 | server.logger.Info("Stopped serving new connections.") 105 | server.logger.Close() 106 | }() 107 | 108 | signal.Notify(server.sigChan, syscall.SIGINT, syscall.SIGTERM) 109 | <-server.sigChan 110 | } 111 | 112 | func (server *HttpsServer) Stop() { 113 | shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) 114 | defer shutdownRelease() 115 | 116 | if err := server.httpsServer.Shutdown(shutdownCtx); err != nil { 117 | server.logger.Fatal("HTTP shutdown error:", err) 118 | } 119 | 120 | server.logger.Info("Graceful shutdown complete.") 121 | server.httpsServer = nil 122 | server.logger.Close() 123 | } 124 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/dmarro89/dare-db/auth" 10 | "github.com/dmarro89/dare-db/logger" 11 | "github.com/stretchr/testify/mock" 12 | "gotest.tools/assert" 13 | ) 14 | 15 | // constants 16 | const SLEEP_ON_START int = 3 17 | 18 | // Mock Database 19 | type MockDatabase struct { 20 | mock.Mock 21 | } 22 | 23 | // Mock DareServer 24 | type MockDareServer struct { 25 | mock.Mock 26 | } 27 | 28 | func (ds *MockDareServer) CreateMux(auth.Authorizer, auth.Authenticator) *http.ServeMux { 29 | args := ds.Called() 30 | return args.Get(0).(*http.ServeMux) 31 | } 32 | 33 | func (ds *MockDareServer) HandlerGetById(w http.ResponseWriter, r *http.Request) { 34 | } 35 | func (ds *MockDareServer) HandlerSet(w http.ResponseWriter, r *http.Request) { 36 | } 37 | func (ds *MockDareServer) HandlerDelete(w http.ResponseWriter, r *http.Request) { 38 | } 39 | func (ds *MockDareServer) HandlerLogin(w http.ResponseWriter, r *http.Request) { 40 | } 41 | 42 | func TestNewHttpServer(t *testing.T) { 43 | server := NewHttpServer(&MockDareServer{}, NewConfiguration(""), logger.NewDareLogger()) 44 | assert.Assert(t, server != nil) 45 | assert.Assert(t, server.configuration != nil) 46 | assert.Assert(t, server.sigChan != nil) 47 | assert.Assert(t, server.dareServer != nil) 48 | } 49 | 50 | func TestHttpServerStartAndStop(t *testing.T) { 51 | // Setup 52 | sigChan := make(chan os.Signal, 1) 53 | server := &HttpServer{ 54 | configuration: NewConfiguration(""), 55 | sigChan: sigChan, 56 | dareServer: &MockDareServer{}, 57 | logger: logger.NewDareLogger(), 58 | } 59 | 60 | mux := http.NewServeMux() 61 | server.dareServer.(*MockDareServer).On("CreateMux").Return(mux) 62 | 63 | // Start server 64 | go server.Start() 65 | time.Sleep(time.Duration(SLEEP_ON_START) * time.Second) // Give it time to start 66 | 67 | // Verify the server is running 68 | assert.Assert(t, server.httpServer != nil) 69 | 70 | // Stop server 71 | server.Stop() 72 | 73 | time.Sleep(time.Duration(SLEEP_ON_START) * time.Second) 74 | // Verify the server is stopped 75 | assert.Assert(t, server.httpServer == nil) 76 | } 77 | 78 | func TestNewHttpsServer(t *testing.T) { 79 | server := NewHttpsServer(&MockDareServer{}, NewConfiguration(""), logger.NewDareLogger()) 80 | assert.Assert(t, server != nil) 81 | assert.Assert(t, server.configuration != nil) 82 | assert.Assert(t, server.sigChan != nil) 83 | assert.Assert(t, server.dareServer != nil) 84 | } 85 | 86 | func TestHttpsServerStartAndStop(t *testing.T) { 87 | t.Skip("Skipping test - Configure certificates to run it") 88 | // Setup 89 | sigChan := make(chan os.Signal, 1) 90 | server := &HttpsServer{ 91 | configuration: NewConfiguration(""), 92 | sigChan: sigChan, 93 | dareServer: &MockDareServer{}, 94 | } 95 | 96 | mux := http.NewServeMux() 97 | server.dareServer.(*MockDareServer).On("CreateMux").Return(mux) 98 | 99 | // Start server 100 | go server.Start() 101 | time.Sleep(time.Duration(SLEEP_ON_START) * time.Second) // Give it time to start 102 | 103 | // Verify the server is running 104 | assert.Assert(t, server.httpsServer != nil) 105 | 106 | // Stop server 107 | server.Stop() 108 | 109 | // Verify the server is stopped 110 | assert.Assert(t, server.httpsServer == nil) 111 | } 112 | -------------------------------------------------------------------------------- /taskfiles/Taskfile_docker.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | vars: 4 | DOCKER_IMAGE_NAME_PURE: "dare-db" 5 | DOCKER_IMAGE_NAME: "dare-db-tls" 6 | 7 | tasks: 8 | 9 | build: 10 | desc: build a new docker container ("{{.DOCKER_IMAGE_NAME}}") 11 | aliases: [db] 12 | silent: true 13 | run: once 14 | cmds: 15 | - docker build -t {{.DOCKER_IMAGE_NAME}}:latest -f Dockerfile.tls.yml . 16 | 17 | delete: 18 | desc: stops and removes a docker container ("{{.DOCKER_IMAGE_NAME}}") 19 | aliases: [dd] 20 | silent: true 21 | run: once 22 | cmds: 23 | - echo "stop container \"{{.DOCKER_IMAGE_NAME}}\"" 24 | - cmd: docker stop {{.DOCKER_IMAGE_NAME}} 25 | ignore_error: true 26 | - echo "delete container \"{{.DOCKER_IMAGE_NAME}}\"" 27 | - cmd: docker rm {{.DOCKER_IMAGE_NAME}} 28 | ignore_error: true 29 | 30 | run: 31 | desc: re-runs a new docker container ("{{.DOCKER_IMAGE_NAME}}") 32 | aliases: [dr] 33 | silent: true 34 | run: once 35 | cmds: 36 | - task: delete 37 | - task: build 38 | - docker run -d -p "127.0.0.1:2605:2605" -e DARE_HOST="0.0.0.0" -e DARE_PORT=2605 -e DARE_TLS_ENABLED="True" -e DARE_CERT_PRIVATE="/app/settings/cert_private.pem" -e DARE_CERT_PUBLIC="/app/settings/cert_public.pem" --name {{.DOCKER_IMAGE_NAME}} {{.DOCKER_IMAGE_NAME}}:latest 39 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func GenerateRandomString(length int) string { 9 | 10 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 11 | var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) 12 | 13 | b := make([]byte, length) 14 | for i := range b { 15 | b[i] = charset[seededRand.Intn(len(charset))] 16 | } 17 | return string(b) 18 | } 19 | --------------------------------------------------------------------------------