diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..cdde9d4 --- /dev/null +++ b/.env.sample @@ -0,0 +1,8 @@ +DB_HOST=localhost +DB_USER=pmp_user +DB_PASSWORD=pmp1234 +DB_NAME=pmp_db +DB_PORT=5432 +STAGE=DEV +API_SECRET=myawsomeapisecret +TOKEN_HOUR_LIFESPAN=1 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..541ae67 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "docker" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f761c0c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + pull_request: + +jobs: + build: + name: test + runs-on: ubuntu-latest + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_USER: pmp_user + POSTGRES_PASSWORD: pmp1234 + POSTGRES_DB: pmp_db + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + id: go + + # Verify that go.mod and go.sum is synchronized + - name: Check Go modules + run: | + if [[ ! -z $(go mod tidy && git diff --exit-code) ]]; then + echo "Please run "go mod tidy" to sync Go modules" + exit 1 + fi + + - name: Test + env: + DB_HOST: localhost + DB_USER: pmp_user + DB_PASSWORD: pmp1234 + DB_NAME: pmp_db + DB_PORT: 5432 + API_SECRET: myawsomeapisecret + TOKEN_HOUR_LIFESPAN: 1 + run: make test + + linter: + name: lint + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.21 + cache: true + cache-dependency-path: go.sum + id: go + + - name: staticcheck + uses: dominikh/staticcheck-action@v1.3.0 + + - name: lint + uses: golangci/golangci-lint-action@v3.7.0 + with: + version: v1.52.2 + args: --timeout=5m --out-format=colored-line-number + skip-cache: true + skip-pkg-cache: true + skip-build-cache: true \ No newline at end of file diff --git a/.github/workflows/doc-generation.yml b/.github/workflows/doc-generation.yml new file mode 100644 index 0000000..7c96b5b --- /dev/null +++ b/.github/workflows/doc-generation.yml @@ -0,0 +1,31 @@ +name: Generate Swagger Documentation + +on: + workflow_dispatch: + +jobs: + generate-and-commit-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GHPUSH_PAT }} + + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Install Swaggo + run: go install github.com/swaggo/swag/cmd/swag@latest + + - name: Generate Swagger Documentation + run: | + $(go env GOPATH)/bin/swag init + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add docs/ + git commit -m "Update Swagger documentation" || echo "No changes to commit" + git push \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4b6d860 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,87 @@ +name: release + +on: + push: + # run only against tags + tags: + - "*" + +permissions: + contents: write + +jobs: + Releaser: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_USER: pmp_user + POSTGRES_PASSWORD: pmp1234 + POSTGRES_DB: pmp_db + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GHPUSH_PAT }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + - name: Check Go modules + run: | + if [[ ! -z $(go mod tidy && git diff --exit-code) ]]; then + echo "Please run "go mod tidy" to sync Go modules" + exit 1 + fi + + - name: Test + env: + DB_HOST: localhost + DB_USER: pmp_user + DB_PASSWORD: pmp1234 + DB_NAME: pmp_db + DB_PORT: 5432 + API_SECRET: myawsomeapisecret + TOKEN_HOUR_LIFESPAN: 1 + run: make test + + - name: Install Swaggo + run: go install github.com/swaggo/swag/cmd/swag@latest + - name: Generate Swagger Documentation + run: | + # Generate Swagger documentation + $(go env GOPATH)/bin/swag init + + # Configure Git + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Create a new branch for the Swagger doc update + git checkout -b swagger-doc-update + + # Commit and push if there are changes + git add docs/ + git commit -m "Update Swagger documentation" -a || echo "No changes to commit" + git push origin swagger-doc-update + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_PAT }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa1aa8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# main binary +pimpmypack + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Environment file +.env + +# MacOS files +.DS_Store + +dist/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..4ce15ed --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,46 @@ +run: + timeout: 5m + +linters: + enable: + - bodyclose + - musttag + - gocritic + - unparam + - errorlint + - gci + - rowserrcheck + - revive + +linters-settings: + revive: + rules: + - name: datarace + disabled: false + - name: deep-exit + disabled: false + - name: defer + disabled: false + - name: errorf + disabled: false + - name: function-length + disabled: false + arguments: [120, 0] # we have to decrease this value + - name: if-return + disabled: false + - name: superfluous-else + disabled: false + - name: unhandled-error + disabled: false + - name: unnecessary-stmt + disabled: false + - name: unreachable-code + disabled: false + - name: unused-parameter + disabled: false + - name: unused-receiver + disabled: false + - name: useless-break + disabled: false + - name: var-naming + disabled: true diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..00d46dc --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,40 @@ +version: 1 + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - id: pimpmypack + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + +archives: + - format: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + +dockers: + - image_templates: + - "ghcr.io/angak0k/pimpmypack:{{ .Tag }}" + - "ghcr.io/angak0k/pimpmypack:latest" + goos: linux + goarch: amd64 + ids: + - pimpmypack + dockerfile: Dockerfile + +changelog: + sort: asc + filters: + exclude: + - "^test:" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ecc37d7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM alpine:3.19.0 + +LABEL maintainer="romain@alki.earth" + +RUN apk update && \ + apk upgrade --no-cache + +COPY pimpmypack /bin/pimpmypack + +ENTRYPOINT ["/bin/pimpmypack"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..68be80d --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +NAME=pimpmypack + +test: + go test -covermode=atomic -coverprofile=coverage.out -race ./... + +api-doc: + swag init + +build: test + go build + +lint: + docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.52 golangci-lint run -v diff --git a/README.md b/README.md new file mode 100644 index 0000000..944c635 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +

⛰️ pimpmypack

+ +> PimpMyPack is a set of backend APIs dedicated to CRUD operations on hiking equipment inventories and packing lists. +> It should be used in conjunction with any frontend candidates. +> It could replace [Lighterpack](https://lighterpack.com/) if this project dies (because it's not maintained anymore) + +# PimpMyPack API + +The server is based on [Gin Framework](https://github.com/gin-gonic/gin) and provides endpoints to manage Accounts, Inventories & Packs + +A dedicated API documentation is available [here](). + +## Setup for local development + +## 1. clone this repo + +```shell +git clone git@github.com:Angak0k/pimpmypack.git +``` + +## 3. Start a local postgres database + +The app need a local DB. + +You need to use docker to start a postgres database: + +```shell +docker run --name pmp_db \ + -d -p 5432:5432 \ + -e POSTGRES_PASSWORD=pmp1234 \ + -e POSTGRES_USER=pmp_user \ + -e POSTGRES_DB=pmp_db postgres:14-alpine +``` + +## 4. Configure the environment + +Pimpmypack app read its conf from the environment and/or `.env` file. + +The simplest way is to: + +* copy the `.env.sample` file to `.env` +* customize the values in the `.env` file to match your setup + + +## 5. Start the API server + +```shell +go build . && ./pimpmypack +``` + +## Run tests + +```shell +go test ./... +``` + +or with verbose mode + +```shell +go test -v ./... +``` diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..c2aab0c --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,2072 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/accounts": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all accounts - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Get all accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new account - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Create a new account", + "parameters": [ + { + "description": "Account Information", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Account" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/accounts/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get account by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Get account by ID", + "parameters": [ + { + "type": "integer", + "description": "Account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error: Account not found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update account by ID - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Update account by ID", + "parameters": [ + { + "type": "integer", + "description": "Account ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Account Information", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Account" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete account by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Delete account by ID", + "parameters": [ + { + "type": "integer", + "description": "Account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Account deleted", + "schema": { + "type": "string" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/inventories": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves a list of all inventories - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Get all inventories", + "responses": { + "200": { + "description": "List of Inventories", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.Inventory" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Creates an inventory - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Create an inventory", + "parameters": [ + { + "description": "Inventory", + "name": "inventory", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/inventories/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves an inventory by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Get an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Inventory not found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Updates an inventory by ID - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Update an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Inventory", + "name": "inventory", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Deletes an inventory by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Delete an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Inventory deleted", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/login": { + "post": { + "description": "Logs in a user by providing username and password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "User login", + "parameters": [ + { + "description": "Username", + "name": "username", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Password", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "token", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "error: credentials are incorrect or token generation failed", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/myaccount": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information of the currently logged-in user", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Get account info", + "responses": { + "200": { + "description": "Account Information", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error: Account not found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update information of the currently logged-in user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Update account info", + "parameters": [ + { + "description": "Account Information", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Account" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/myinventory": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves a list of all inventories of the user", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "Get all inventories of the user", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.Inventory" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Creates an inventory", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "Create an inventory", + "parameters": [ + { + "description": "Inventory", + "name": "inventory", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/myinventory/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves an inventory by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "Get an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "403": { + "description": "This item does not belong to you", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Inventory not found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Updates an inventory by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "Update an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Inventory", + "name": "inventory", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "403": { + "description": "This item does not belong to you", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypack": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new pack", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Create a new pack", + "parameters": [ + { + "description": "Pack", + "name": "pack", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypack/import": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Import from lighterpack csv pack file", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Import from lighterpack csv pack file", + "parameters": [ + { + "type": "file", + "description": "CSV file", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypack/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get pack by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Get pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update a pack by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Update a pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Pack", + "name": "pack", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete a pack by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Delete a pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypackcontent": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new pack content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Create a new pack content", + "parameters": [ + { + "description": "Pack Content", + "name": "packcontent", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypackcontent/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get pack content by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Get pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete a pack content by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Delete a pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypacks": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all packs - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get all packs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.Pack" + } + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypassword": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update the password of the current logged-in user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Update password", + "parameters": [ + { + "description": "Updated Password", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Updated password successfully", + "schema": { + "type": "string" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packcontents": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all pack contents - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get all pack contents", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.PackContent" + } + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new pack content - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Create a new pack content", + "parameters": [ + { + "description": "Pack Content", + "name": "packcontent", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packcontents/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get pack content by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update a pack content by ID - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Update a pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Pack Content", + "name": "packcontent", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete a pack content by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Delete a pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packs": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all packs - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get all packs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.Pack" + } + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new pack - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Create a new pack", + "parameters": [ + { + "description": "Pack", + "name": "pack", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packs/:id/packcontents": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all pack contents - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get all pack contents", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.PackContent" + } + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packs/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get pack by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update a pack by ID - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Update a pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Pack", + "name": "pack", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/register": { + "post": { + "description": "Register a new user with username, password, email, firstname, and lastname", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Register new user", + "parameters": [ + { + "description": "Register Info", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.RegisterInput" + } + } + ], + "responses": { + "200": { + "description": "registration success", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + }, + "definitions": { + "dataset.Account": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "lastname": { + "type": "string" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "dataset.Inventory": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "item_name": { + "type": "string" + }, + "price": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "weight": { + "type": "integer" + }, + "weight_unit": { + "type": "string" + } + } + }, + "dataset.Pack": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "pack_description": { + "type": "string" + }, + "pack_name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "dataset.PackContent": { + "type": "object", + "properties": { + "consumable": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "item_id": { + "type": "integer" + }, + "pack_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "worn": { + "type": "boolean" + } + } + }, + "dataset.RegisterInput": { + "type": "object", + "required": [ + "email", + "firstname", + "lastname", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "pimpmypack.alki.earth", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "PimpMyPack API", + Description: "This is an API server to manage Backpack Inventory", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..375603c --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,2048 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is an API server to manage Backpack Inventory", + "title": "PimpMyPack API", + "contact": {}, + "version": "1.0" + }, + "host": "pimpmypack.alki.earth", + "basePath": "/api/v1", + "paths": { + "/accounts": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all accounts - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Get all accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new account - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Create a new account", + "parameters": [ + { + "description": "Account Information", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Account" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/accounts/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get account by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Get account by ID", + "parameters": [ + { + "type": "integer", + "description": "Account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error: Account not found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update account by ID - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Update account by ID", + "parameters": [ + { + "type": "integer", + "description": "Account ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Account Information", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Account" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete account by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "[ADMIN] Delete account by ID", + "parameters": [ + { + "type": "integer", + "description": "Account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Account deleted", + "schema": { + "type": "string" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/inventories": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves a list of all inventories - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Get all inventories", + "responses": { + "200": { + "description": "List of Inventories", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.Inventory" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Creates an inventory - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Create an inventory", + "parameters": [ + { + "description": "Inventory", + "name": "inventory", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/inventories/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves an inventory by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Get an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Inventory not found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Updates an inventory by ID - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Update an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Inventory", + "name": "inventory", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Deletes an inventory by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "[ADMIN] Delete an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Inventory deleted", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/login": { + "post": { + "description": "Logs in a user by providing username and password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "User login", + "parameters": [ + { + "description": "Username", + "name": "username", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Password", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "token", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "error: credentials are incorrect or token generation failed", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/myaccount": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information of the currently logged-in user", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Get account info", + "responses": { + "200": { + "description": "Account Information", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error: Account not found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update information of the currently logged-in user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Update account info", + "parameters": [ + { + "description": "Account Information", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Account" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Account" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/myinventory": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves a list of all inventories of the user", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "Get all inventories of the user", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.Inventory" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Creates an inventory", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "Create an inventory", + "parameters": [ + { + "description": "Inventory", + "name": "inventory", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/myinventory/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves an inventory by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "Get an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "403": { + "description": "This item does not belong to you", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Inventory not found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Updates an inventory by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Inventories" + ], + "summary": "Update an inventory by ID", + "parameters": [ + { + "type": "integer", + "description": "Inventory ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Inventory", + "name": "inventory", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Inventory" + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "403": { + "description": "This item does not belong to you", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypack": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new pack", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Create a new pack", + "parameters": [ + { + "description": "Pack", + "name": "pack", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypack/import": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Import from lighterpack csv pack file", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Import from lighterpack csv pack file", + "parameters": [ + { + "type": "file", + "description": "CSV file", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypack/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get pack by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Get pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update a pack by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Update a pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Pack", + "name": "pack", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete a pack by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Delete a pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypackcontent": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new pack content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Create a new pack content", + "parameters": [ + { + "description": "Pack Content", + "name": "packcontent", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypackcontent/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get pack content by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Get pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete a pack content by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "Delete a pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypacks": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all packs - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get all packs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.Pack" + } + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/mypassword": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update the password of the current logged-in user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Update password", + "parameters": [ + { + "description": "Updated Password", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Updated password successfully", + "schema": { + "type": "string" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packcontents": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all pack contents - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get all pack contents", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.PackContent" + } + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new pack content - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Create a new pack content", + "parameters": [ + { + "description": "Pack Content", + "name": "packcontent", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packcontents/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get pack content by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update a pack content by ID - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Update a pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Pack Content", + "name": "packcontent", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.PackContent" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete a pack content by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Delete a pack content by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packs": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all packs - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get all packs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.Pack" + } + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new pack - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Create a new pack", + "parameters": [ + { + "description": "Pack", + "name": "pack", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packs/:id/packcontents": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get all pack contents - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get all pack contents", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dataset.PackContent" + } + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/packs/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get pack by ID - for admin use only", + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Get pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Update a pack by ID - for admin use only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Packs" + ], + "summary": "[ADMIN] Update a pack by ID", + "parameters": [ + { + "type": "integer", + "description": "Pack ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Pack", + "name": "pack", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dataset.Pack" + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/register": { + "post": { + "description": "Register a new user with username, password, email, firstname, and lastname", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Register new user", + "parameters": [ + { + "description": "Register Info", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dataset.RegisterInput" + } + } + ], + "responses": { + "200": { + "description": "registration success", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + }, + "definitions": { + "dataset.Account": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "lastname": { + "type": "string" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "dataset.Inventory": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "item_name": { + "type": "string" + }, + "price": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "weight": { + "type": "integer" + }, + "weight_unit": { + "type": "string" + } + } + }, + "dataset.Pack": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "pack_description": { + "type": "string" + }, + "pack_name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "dataset.PackContent": { + "type": "object", + "properties": { + "consumable": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "item_id": { + "type": "integer" + }, + "pack_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "worn": { + "type": "boolean" + } + } + }, + "dataset.RegisterInput": { + "type": "object", + "required": [ + "email", + "firstname", + "lastname", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..7f69762 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,1333 @@ +basePath: /api/v1 +definitions: + dataset.Account: + properties: + created_at: + type: string + email: + type: string + firstname: + type: string + id: + type: integer + lastname: + type: string + role: + type: string + status: + type: string + updated_at: + type: string + username: + type: string + type: object + dataset.Inventory: + properties: + category: + type: string + created_at: + type: string + currency: + type: string + description: + type: string + id: + type: integer + item_name: + type: string + price: + type: integer + updated_at: + type: string + url: + type: string + user_id: + type: integer + weight: + type: integer + weight_unit: + type: string + type: object + dataset.Pack: + properties: + created_at: + type: string + id: + type: integer + pack_description: + type: string + pack_name: + type: string + updated_at: + type: string + user_id: + type: integer + type: object + dataset.PackContent: + properties: + consumable: + type: boolean + created_at: + type: string + id: + type: integer + item_id: + type: integer + pack_id: + type: integer + quantity: + type: integer + updated_at: + type: string + worn: + type: boolean + type: object + dataset.RegisterInput: + properties: + email: + type: string + firstname: + type: string + lastname: + type: string + password: + type: string + username: + type: string + required: + - email + - firstname + - lastname + - password + - username + type: object +host: pimpmypack.alki.earth +info: + contact: {} + description: This is an API server to manage Backpack Inventory + title: PimpMyPack API + version: "1.0" +paths: + /accounts: + get: + description: Get all accounts - for admin use only + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Account' + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get all accounts' + tags: + - Accounts + post: + consumes: + - application/json + description: Create a new account - for admin use only + parameters: + - description: Account Information + in: body + name: input + required: true + schema: + $ref: '#/definitions/dataset.Account' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dataset.Account' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Create a new account' + tags: + - Accounts + /accounts/{id}: + delete: + description: Delete account by ID - for admin use only + parameters: + - description: Account ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Account deleted + schema: + type: string + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Delete account by ID' + tags: + - Accounts + get: + description: Get account by ID - for admin use only + parameters: + - description: Account ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Account' + "400": + description: error + schema: + additionalProperties: true + type: object + "404": + description: 'error: Account not found' + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get account by ID' + tags: + - Accounts + put: + consumes: + - application/json + description: Update account by ID - for admin use only + parameters: + - description: Account ID + in: path + name: id + required: true + type: integer + - description: Account Information + in: body + name: input + required: true + schema: + $ref: '#/definitions/dataset.Account' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Account' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Update account by ID' + tags: + - Accounts + /inventories: + get: + description: Retrieves a list of all inventories - for admin use only + produces: + - application/json + responses: + "200": + description: List of Inventories + schema: + items: + $ref: '#/definitions/dataset.Inventory' + type: array + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get all inventories' + tags: + - Inventories + post: + consumes: + - application/json + description: Creates an inventory - for admin use only + parameters: + - description: Inventory + in: body + name: inventory + required: true + schema: + $ref: '#/definitions/dataset.Inventory' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dataset.Inventory' + "400": + description: Invalid payload + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Create an inventory' + tags: + - Inventories + /inventories/{id}: + delete: + description: Deletes an inventory by ID - for admin use only + parameters: + - description: Inventory ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Inventory deleted + schema: + type: string + "400": + description: Invalid ID format + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Delete an inventory by ID' + tags: + - Inventories + get: + description: Retrieves an inventory by ID - for admin use only + parameters: + - description: Inventory ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Inventory' + "400": + description: Invalid ID format + schema: + additionalProperties: true + type: object + "404": + description: Inventory not found + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get an inventory by ID' + tags: + - Inventories + put: + consumes: + - application/json + description: Updates an inventory by ID - for admin use only + parameters: + - description: Inventory ID + in: path + name: id + required: true + type: integer + - description: Inventory + in: body + name: inventory + required: true + schema: + $ref: '#/definitions/dataset.Inventory' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Inventory' + "400": + description: Invalid payload + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Update an inventory by ID' + tags: + - Inventories + /login: + post: + consumes: + - application/json + description: Logs in a user by providing username and password + parameters: + - description: Username + in: body + name: username + required: true + schema: + type: string + - description: Password + in: body + name: password + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: token + schema: + additionalProperties: + type: string + type: object + "403": + description: 'error: credentials are incorrect or token generation failed' + schema: + additionalProperties: true + type: object + summary: User login + tags: + - Accounts + /myaccount: + get: + description: Get information of the currently logged-in user + produces: + - application/json + responses: + "200": + description: Account Information + schema: + $ref: '#/definitions/dataset.Account' + "400": + description: error + schema: + additionalProperties: true + type: object + "404": + description: 'error: Account not found' + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Get account info + tags: + - Accounts + put: + consumes: + - application/json + description: Update information of the currently logged-in user + parameters: + - description: Account Information + in: body + name: input + required: true + schema: + $ref: '#/definitions/dataset.Account' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Account' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Update account info + tags: + - Accounts + /myinventory: + get: + description: Retrieves a list of all inventories of the user + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dataset.Inventory' + type: array + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Get all inventories of the user + tags: + - Inventories + post: + consumes: + - application/json + description: Creates an inventory + parameters: + - description: Inventory + in: body + name: inventory + required: true + schema: + $ref: '#/definitions/dataset.Inventory' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dataset.Inventory' + "400": + description: Invalid payload + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Create an inventory + tags: + - Inventories + /myinventory/{id}: + get: + description: Retrieves an inventory by ID + parameters: + - description: Inventory ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Inventory' + "400": + description: Invalid ID format + schema: + additionalProperties: true + type: object + "403": + description: This item does not belong to you + schema: + additionalProperties: true + type: object + "404": + description: Inventory not found + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Get an inventory by ID + tags: + - Inventories + put: + consumes: + - application/json + description: Updates an inventory by ID + parameters: + - description: Inventory ID + in: path + name: id + required: true + type: integer + - description: Inventory + in: body + name: inventory + required: true + schema: + $ref: '#/definitions/dataset.Inventory' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Inventory' + "400": + description: Invalid payload + schema: + additionalProperties: true + type: object + "403": + description: This item does not belong to you + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Update an inventory by ID + tags: + - Inventories + /mypack: + post: + consumes: + - application/json + description: Create a new pack + parameters: + - description: Pack + in: body + name: pack + required: true + schema: + $ref: '#/definitions/dataset.Pack' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dataset.Pack' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Create a new pack + tags: + - Packs + /mypack/{id}: + delete: + description: Delete a pack by ID + parameters: + - description: Pack ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: message + schema: + additionalProperties: + type: string + type: object + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Delete a pack by ID + tags: + - Packs + get: + description: Get pack by ID + parameters: + - description: Pack ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Pack' + "400": + description: error + schema: + additionalProperties: true + type: object + "404": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Get pack by ID + tags: + - Packs + put: + consumes: + - application/json + description: Update a pack by ID + parameters: + - description: Pack ID + in: path + name: id + required: true + type: integer + - description: Pack + in: body + name: pack + required: true + schema: + $ref: '#/definitions/dataset.Pack' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Pack' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Update a pack by ID + tags: + - Packs + /mypack/import: + post: + consumes: + - multipart/form-data + description: Import from lighterpack csv pack file + parameters: + - description: CSV file + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: message + schema: + additionalProperties: + type: string + type: object + "400": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Import from lighterpack csv pack file + tags: + - Packs + /mypackcontent: + post: + consumes: + - application/json + description: Create a new pack content + parameters: + - description: Pack Content + in: body + name: packcontent + required: true + schema: + $ref: '#/definitions/dataset.PackContent' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dataset.PackContent' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Create a new pack content + tags: + - Packs + /mypackcontent/{id}: + delete: + description: Delete a pack content by ID + parameters: + - description: Pack Content ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: message + schema: + additionalProperties: + type: string + type: object + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Delete a pack content by ID + tags: + - Packs + get: + description: Get pack content by ID + parameters: + - description: Pack Content ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.PackContent' + "400": + description: error + schema: + additionalProperties: true + type: object + "404": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Get pack content by ID + tags: + - Packs + /mypacks: + get: + description: Get all packs - for admin use only + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dataset.Pack' + type: array + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get all packs' + tags: + - Packs + /mypassword: + put: + consumes: + - application/json + description: Update the password of the current logged-in user + parameters: + - description: Updated Password + in: body + name: password + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: Updated password successfully + schema: + type: string + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Update password + tags: + - Accounts + /packcontents: + get: + description: Get all pack contents - for admin use only + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dataset.PackContent' + type: array + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get all pack contents' + tags: + - Packs + post: + consumes: + - application/json + description: Create a new pack content - for admin use only + parameters: + - description: Pack Content + in: body + name: packcontent + required: true + schema: + $ref: '#/definitions/dataset.PackContent' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dataset.PackContent' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Create a new pack content' + tags: + - Packs + /packcontents/{id}: + delete: + description: Delete a pack content by ID - for admin use only + parameters: + - description: Pack Content ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: message + schema: + additionalProperties: + type: string + type: object + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Delete a pack content by ID' + tags: + - Packs + get: + description: Get pack content by ID - for admin use only + parameters: + - description: Pack Content ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.PackContent' + "400": + description: error + schema: + additionalProperties: true + type: object + "404": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get pack content by ID' + tags: + - Packs + put: + consumes: + - application/json + description: Update a pack content by ID - for admin use only + parameters: + - description: Pack Content ID + in: path + name: id + required: true + type: integer + - description: Pack Content + in: body + name: packcontent + required: true + schema: + $ref: '#/definitions/dataset.PackContent' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.PackContent' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Update a pack content by ID' + tags: + - Packs + /packs: + get: + description: Get all packs - for admin use only + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dataset.Pack' + type: array + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get all packs' + tags: + - Packs + post: + consumes: + - application/json + description: Create a new pack - for admin use only + parameters: + - description: Pack + in: body + name: pack + required: true + schema: + $ref: '#/definitions/dataset.Pack' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dataset.Pack' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Create a new pack' + tags: + - Packs + /packs/:id/packcontents: + get: + description: Get all pack contents - for admin use only + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dataset.PackContent' + type: array + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get all pack contents' + tags: + - Packs + /packs/{id}: + get: + description: Get pack by ID - for admin use only + parameters: + - description: Pack ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Pack' + "400": + description: error + schema: + additionalProperties: true + type: object + "404": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Get pack by ID' + tags: + - Packs + put: + consumes: + - application/json + description: Update a pack by ID - for admin use only + parameters: + - description: Pack ID + in: path + name: id + required: true + type: integer + - description: Pack + in: body + name: pack + required: true + schema: + $ref: '#/definitions/dataset.Pack' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dataset.Pack' + "400": + description: error + schema: + additionalProperties: true + type: object + "500": + description: error + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: '[ADMIN] Update a pack by ID' + tags: + - Packs + /register: + post: + consumes: + - application/json + description: Register a new user with username, password, email, firstname, + and lastname + parameters: + - description: Register Info + in: body + name: input + required: true + schema: + $ref: '#/definitions/dataset.RegisterInput' + produces: + - application/json + responses: + "200": + description: registration success + schema: + additionalProperties: true + type: object + "400": + description: error + schema: + additionalProperties: true + type: object + summary: Register new user + tags: + - Accounts +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5f2c802 --- /dev/null +++ b/go.mod @@ -0,0 +1,58 @@ +module github.com/Angak0k/pimpmypack + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/google/go-cmp v0.6.0 + github.com/gruntwork-io/terratest v0.46.11 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.2 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/spec v0.20.14 // indirect + github.com/go-openapi/swag v0.22.7 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + golang.org/x/tools v0.17.0 // indirect +) + +require ( + github.com/bytedance/sonic v1.10.2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.17.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-migrate/migrate/v4 v4.17.0 + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/joho/godotenv v1.5.1 + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lib/pq v1.10.9 + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/crypto v0.18.0 + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e280e3e --- /dev/null +++ b/go.sum @@ -0,0 +1,191 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= +github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M= +github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8= +github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= +github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= +github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gruntwork-io/terratest v0.46.11 h1:1Z9G18I2FNuH87Ro0YtjW4NH9ky4GDpfzE7+ivkPeB8= +github.com/gruntwork-io/terratest v0.46.11/go.mod h1:DVZG/s7eP1u3KOQJJfE6n7FDriMWpDvnj85XIlZMEM8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..681efbb --- /dev/null +++ b/go.work.sum @@ -0,0 +1,90 @@ +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.7.1/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl/v2 v2.9.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= +github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/tmccombs/hcl2json v0.3.3/go.mod h1:Y2chtz2x9bAeRTvSibVRVgbLJhLJXKlUeIvjeVdnm4w= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..41cbba2 --- /dev/null +++ b/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "log" + + _ "github.com/Angak0k/pimpmypack/docs" + "github.com/Angak0k/pimpmypack/pkg/accounts" + "github.com/Angak0k/pimpmypack/pkg/config" + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/inventories" + "github.com/Angak0k/pimpmypack/pkg/packs" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func init() { + + // init env + err := config.EnvInit(".env") + if err != nil { + log.Fatalf("Error loading .env file or environment variable : %v", err) + } + println("Environment variables loaded") + + // init DB + err = database.DatabaseInit() + if err != nil { + log.Fatalf("Error connecting database : %v", err) + } + println("Database connected") + + // init DB migration + err = database.DatabaseMigrate() + if err != nil { + log.Fatalf("Error migrating database : %v", err) + } + println("Database migrated") + +} + +// @title PimpMyPack API +// @description This is an API server to manage Backpack Inventory +// @version 1.0 +// @host pimpmypack.alki.earth +// @BasePath /api/v1 +func main() { + + if config.Stage == "DEV" { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + + router := gin.Default() + + public := router.Group("/api") + public.POST("/register", accounts.Register) + public.POST("/login", accounts.Login) + + protected := router.Group("/api/v1") + protected.Use(security.JwtAuthProcessor()) + protected.GET("/myaccount", accounts.GetMyAccount) + protected.PUT("/myaccount", accounts.PutMyAccount) + protected.PUT("/mypassword", accounts.PutMyPassword) + protected.GET("/myinventory", inventories.GetMyInventory) + protected.GET("/mypacks", packs.GetMyPacks) + protected.GET("/mypack/:id", packs.GetMyPackByID) + protected.POST("/mypack", packs.PostMyPack) + protected.PUT("/mypack/:id", packs.PutMyPackByID) + protected.DELETE("/mypack/:id", packs.DeleteMyPackByID) + protected.GET("/mypack/:id/packcontents", packs.GetMyPackContentsByPackID) + protected.POST("/mypack/:id/packcontent", packs.PostMyPackContent) + protected.PUT("/mypack/:id/packcontent/:item_id", packs.PutMyPackContentByID) + protected.DELETE("/mypack/:id/packcontent/:item_id", packs.DeleteMyPackContentByID) + protected.GET("/myinventory/:id", inventories.GetMyInventoryByID) + protected.POST("/myinventory", inventories.PostMyInventory) + protected.PUT("/myinventory/:id", inventories.PutMyInventoryByID) + protected.DELETE("/myinventory/:id", inventories.DeleteMyInventoryByID) + + protected.POST("/importfromlighterpack", packs.ImportFromLighterPack) + + private := router.Group("/api/admin") + private.Use(security.JwtAuthAdminProcessor()) + private.GET("/accounts", accounts.GetAccounts) + private.GET("/accounts/:id", accounts.GetAccountByID) + private.POST("/accounts", accounts.PostAccount) + private.PUT("/accounts/:id", accounts.PutAccountByID) + private.DELETE("/accounts/:id", accounts.DeleteAccountByID) + private.GET("/inventories", inventories.GetInventories) + private.GET("/inventories/:id", inventories.GetInventoryByID) + private.POST("/inventories", inventories.PostInventory) + private.PUT("/inventories/:id", inventories.PutInventoryByID) + private.DELETE("/inventories/:id", inventories.DeleteInventoryByID) + private.GET("/packs", packs.GetPacks) + private.GET("/packs/:id", packs.GetPackByID) + private.POST("/packs", packs.PostPack) + private.PUT("/packs/:id", packs.PutPackByID) + private.DELETE("/packs/:id", packs.DeletePackByID) + private.GET("/packcontents", packs.GetPackContents) + private.GET("/packcontents/:id", packs.GetPackContentByID) + private.POST("/packcontents", packs.PostPackContent) + private.PUT("/packcontents/:id", packs.PutPackContentByID) + private.DELETE("/packcontents/:id", packs.DeletePackContentByID) + private.GET("/packs/:id/packcontents", packs.GetPackContentsByPackID) + + if config.Stage == "DEV" { + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + } + + err := router.Run(":8080") + if err != nil { + panic(err) + } +} diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go new file mode 100644 index 0000000..7e97aca --- /dev/null +++ b/pkg/accounts/accounts.go @@ -0,0 +1,515 @@ +package accounts + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" +) + +// Register a new user account +// @Summary Register new user +// @Description Register a new user with username, password, email, firstname, and lastname +// @Tags Accounts +// @Accept json +// @Produce json +// @Param input body dataset.RegisterInput true "Register Info" +// @Success 200 {object} map[string]interface{} "registration success" +// @Failure 400 {object} map[string]interface{} "error" +// @Router /register [post] +func Register(c *gin.Context) { + + var input dataset.RegisterInput + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user := dataset.User{} + + user.Username = input.Username + user.Password = input.Password + user.Email = input.Email + user.Firstname = input.Firstname + user.Lastname = input.Lastname + user.Role = "standard" + user.Status = "pending" + user.Created_at = time.Now().Truncate(time.Second) + user.Updated_at = time.Now().Truncate(time.Second) + + err := saveUser(user) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "registration success"}) + +} + +func saveUser(u dataset.User) error { + var id int + + err := database.Db().QueryRow("INSERT INTO account (username, email, firstname, lastname, role, status, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id;", u.Username, u.Email, u.Firstname, u.Lastname, u.Role, u.Status, u.Created_at, u.Updated_at).Scan(&id) + if err != nil { + return fmt.Errorf("failed to insert user: %w", err) + } + + hashedPassword, err := security.HashPassword(u.Password) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + err = database.Db().QueryRow("INSERT INTO password (user_id, password, updated_at) VALUES ($1,$2,$3) RETURNING id;", id, hashedPassword, u.Updated_at).Scan(&id) + if err != nil { + return fmt.Errorf("failed to insert password: %w", err) + } + + return nil +} + +// Update user password +// @Summary Update password +// @Description Update the password of the current logged-in user +// @Security Bearer +// @Tags Accounts +// @Accept json +// @Produce json +// @Param password body string true "Updated Password" +// @Success 200 {string} string "Updated password successfully" +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypassword [put] +func PutMyPassword(c *gin.Context) { + + var updatedPassword string + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Call BindJSON to bind the received JSON to updatedPassword. + if err := c.BindJSON(&updatedPassword); err != nil { + return + } + + // Update the DB + err = updatePassword(user_id, updatedPassword) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, updatedPassword) +} + +func updatePassword(user_id uint, updatedPassword string) error { + var lastPassword string + // Get old password + row := database.Db().QueryRow("SELECT password FROM password WHERE user_id = $1);", user_id) + err := row.Scan(&lastPassword) + if err != nil { + return fmt.Errorf("failed to get old password: %w", err) + } + + // Update DB + statement, err := database.Db().Prepare("UPDATE password SET password = $1, last_password = $2, updated_at = $3 WHERE user_id = $4);") + if err != nil { + return fmt.Errorf("failed to prepare update statement: %w", err) + } + defer statement.Close() + + hashedPassword, err := security.HashPassword(updatedPassword) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + _, err = statement.Exec(hashedPassword, lastPassword, time.Now().Truncate(time.Second), user_id) + if err != nil { + return fmt.Errorf("failed to execute update query: %w", err) + } + + return nil +} + +// User login +// @Summary User login +// @Description Logs in a user by providing username and password +// @Tags Accounts +// @Accept json +// @Produce json +// @Param username body string true "Username" +// @Param password body string true "Password" +// @Success 200 {object} map[string]string "token" +// @Failure 403 {object} map[string]interface{} "error: credentials are incorrect or token generation failed" +// @Router /login [post] +func Login(c *gin.Context) { + + var input dataset.LoginInput + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + token, err := loginCheck(input.Username, input.Password) + + if err != nil { + c.JSON(http.StatusForbidden, gin.H{"error": "credentials are incorrect or token generation failed."}) + return + } + + c.JSON(http.StatusOK, gin.H{"token": token}) + +} + +func loginCheck(username string, password string) (string, error) { + + var err error + var storedPassword string + var id uint + + row := database.Db().QueryRow("SELECT password, user_id FROM password WHERE user_id = (SELECT id FROM account WHERE username = $1);", username) + err = row.Scan(&storedPassword, &id) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return "", nil + } + return "", err + } + + err = security.VerifyPassword(password, storedPassword) + + if err != nil && errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + return "", err + } + + token, err := security.GenerateToken(id) + + if err != nil { + return "", err + } + + return token, nil + +} + +// Get my account information +// @Summary Get account info +// @Description Get information of the currently logged-in user +// @Security Bearer +// @Tags Accounts +// @Produce json +// @Success 200 {object} dataset.Account "Account Information" +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 404 {object} map[string]interface{} "error: Account not found" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /myaccount [get] +func GetMyAccount(c *gin.Context) { + + user_id, err := security.ExtractTokenID(c) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + account, err := findAccountById(user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if account != nil { + c.IndentedJSON(http.StatusOK, *account) + } else { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "Account not found"}) + } + +} + +// Update my account information +// @Summary Update account info +// @Description Update information of the currently logged-in user +// @Security Bearer +// @Tags Accounts +// @Accept json +// @Produce json +// @Param input body dataset.Account true "Account Information" +// @Success 200 {object} dataset.Account +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /myaccount [put] +func PutMyAccount(c *gin.Context) { + + var updatedAccount dataset.Account + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Call BindJSON to bind the received JSON to updatedAccount. + if err := c.BindJSON(&updatedAccount); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update the DB + err = updateAccountById(user_id, &updatedAccount) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, updatedAccount) +} + +// Get all accounts +// @Summary [ADMIN] Get all accounts +// @Description Get all accounts - for admin use only +// @Security Bearer +// @Tags Accounts +// @Produce json +// @Success 200 {object} dataset.Account +// @Failure 500 {object} map[string]interface{} "error" +// @Router /accounts [get] +func GetAccounts(c *gin.Context) { + accounts, err := returnAccounts() + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, *accounts) + +} + +func returnAccounts() (*dataset.Accounts, error) { + var accounts dataset.Accounts + + rows, err := database.Db().Query("SELECT id, username, email, firstname, lastname, role, status, created_at, updated_at FROM account;") + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var account dataset.Account + err := rows.Scan(&account.ID, &account.Username, &account.Email, &account.Firstname, &account.Lastname, &account.Role, &account.Status, &account.Created_at, &account.Updated_at) + if err != nil { + return nil, err + } + accounts = append(accounts, account) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return &accounts, nil +} + +// Get account by ID +// @Summary [ADMIN] Get account by ID +// @Description Get account by ID - for admin use only +// @Security Bearer +// @Tags Accounts +// @Produce json +// @Param id path int true "Account ID" +// @Success 200 {object} dataset.Account +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 404 {object} map[string]interface{} "error: Account not found" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /accounts/{id} [get] +func GetAccountByID(c *gin.Context) { + + id64, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + id := uint(id64) + + // Call findAccountById function to lookup in database + account, err := findAccountById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if account != nil { + c.IndentedJSON(http.StatusOK, *account) // Dereference only if account is not nil + } else { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "Account not found"}) + } +} + +func findAccountById(id uint) (*dataset.Account, error) { + var account dataset.Account + + row := database.Db().QueryRow("SELECT id, username, email, firstname, lastname, role, status, created_at, updated_at FROM account WHERE id = $1;", id) + err := row.Scan(&account.ID, &account.Username, &account.Email, &account.Firstname, &account.Lastname, &account.Role, &account.Status, &account.Created_at, &account.Updated_at) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // Handle case when no rows are returned + return nil, nil + } + return nil, err + } + + return &account, nil +} + +// Create a new account +// @Summary [ADMIN] Create a new account +// @Description Create a new account - for admin use only +// @Security Bearer +// @Tags Accounts +// @Accept json +// @Produce json +// @Param input body dataset.Account true "Account Information" +// @Success 201 {object} dataset.Account +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /accounts [post] +func PostAccount(c *gin.Context) { + var newAccount dataset.Account + + // Call BindJSON to bind the received JSON to + // newAccount. + if err := c.BindJSON(&newAccount); err != nil { + return + } + + // Insert the new account into the database. + err := insertAccount(&newAccount) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusCreated, newAccount) + +} + +func insertAccount(a *dataset.Account) error { + if a == nil { + return errors.New("payload is empty") + } + a.Created_at = time.Now().Truncate(time.Second) + a.Updated_at = time.Now().Truncate(time.Second) + + err := database.Db().QueryRow("INSERT INTO account (username, email, firstname, lastname, role, status, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id;", a.Username, a.Email, a.Firstname, a.Lastname, a.Role, a.Status, a.Created_at, a.Updated_at).Scan(&a.ID) + + if err != nil { + return err + } + return nil +} + +// Update account by ID +// @Summary [ADMIN] Update account by ID +// @Description Update account by ID - for admin use only +// @Security Bearer +// @Tags Accounts +// @Accept json +// @Produce json +// @Param id path int true "Account ID" +// @Param input body dataset.Account true "Account Information" +// @Success 200 {object} dataset.Account +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /accounts/{id} [put] +func PutAccountByID(c *gin.Context) { + var updatedAccount dataset.Account + + id64, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + id := uint(id64) + + // Call BindJSON to bind the received JSON to updatedAccount. + if err := c.BindJSON(&updatedAccount); err != nil { + return + } + // Update the DB + err = updateAccountById(id, &updatedAccount) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, updatedAccount) +} + +func updateAccountById(id uint, a *dataset.Account) error { + if a == nil { + return errors.New("payload is empty") + } + + a.ID = id + a.Updated_at = time.Now().Truncate(time.Second) + statement, err := database.Db().Prepare("UPDATE account SET email=$1, firstname=$2, lastname=$3, status=$4, role=$5, updated_at=$6 WHERE id=$7 RETURNING username;") + if err != nil { + return err + } + err = statement.QueryRow(a.Email, a.Firstname, a.Lastname, a.Status, a.Role, a.Updated_at, a.ID).Scan(&a.Username) + if err != nil { + return err + } + return nil +} + +// Delete account by ID +// @Summary [ADMIN] Delete account by ID +// @Description Delete account by ID - for admin use only +// @Security Bearer +// @Tags Accounts +// @Produce json +// @Param id path int true "Account ID" +// @Success 200 {string} string "Account deleted" +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /accounts/{id} [delete] +func DeleteAccountByID(c *gin.Context) { + id := c.Param("id") + err := deleteAccountById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, gin.H{"message": "Account deleted"}) +} + +func deleteAccountById(id string) error { + statement, err := database.Db().Prepare("DELETE FROM account WHERE id=$1;") + if err != nil { + return err + } + _, err = statement.Exec(id) + if err != nil { + return err + } + + return nil + +} diff --git a/pkg/accounts/accounts_test.go b/pkg/accounts/accounts_test.go new file mode 100644 index 0000000..01e7bfa --- /dev/null +++ b/pkg/accounts/accounts_test.go @@ -0,0 +1,523 @@ +package accounts + +import ( + "bytes" + "database/sql" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/Angak0k/pimpmypack/pkg/config" + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gin-gonic/gin" + "github.com/gruntwork-io/terratest/modules/random" + "golang.org/x/crypto/bcrypt" +) + +func TestMain(m *testing.M) { + + // init env + err := config.EnvInit("../../.env") + if err != nil { + log.Fatalf("Error loading .env file or environement variable : %v", err) + } + + // init DB + err = database.DatabaseInit() + if err != nil { + log.Fatalf("Error connecting database : %v", err) + } + + // init DB migration + err = database.DatabaseMigrate() + if err != nil { + log.Fatalf("Error migrating database : %v", err) + } + + // init dataset + println("Loading Account dataset...") + err = loadingAccountDataset() + if err != nil { + log.Fatalf("Error loading dataset : %v", err) + } + + ret := m.Run() + os.Exit(ret) +} + +func TestGetAccounts(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetAccounts handler + router.GET("/accounts", GetAccounts) + + t.Run("Account List Retrieved", func(t *testing.T) { + var getAccounts dataset.Accounts + // Create a mock HTTP request to the /accounts endpoint + req, err := http.NewRequest("GET", "/accounts", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Serve the HTTP request to the Gin router + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Check the Content-Type header + expectedContentType := "application/json; charset=utf-8" + contentType := w.Header().Get("Content-Type") + if contentType != expectedContentType { + t.Errorf("Expected content type %s but got %s", expectedContentType, contentType) + } + + // Unmarshal the response body into a slice of accounts struct + if err := json.Unmarshal(w.Body.Bytes(), &getAccounts); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + // determine if the account - and only the expected account - is in the database + if len(getAccounts) < 3 { + t.Errorf("Expected almost 3 account but got %d", len(getAccounts)) + } + }) +} + +func TestGetAccountByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetAccountByID handler + router.GET("/accounts/:id", GetAccountByID) + + // Set up a test scenario: account found + t.Run("Account Found", func(t *testing.T) { + + // identify the user_id of the first user in the dataset + path := fmt.Sprintf("/accounts/%d", users[0].ID) + + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Unmarshal the response body into an account struct + var receivedAccount dataset.Account + if err := json.Unmarshal(w.Body.Bytes(), &receivedAccount); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received account with the expected account + switch { + case receivedAccount.Username != users[0].Username: + t.Errorf("Expected Username %v but got %v", users[0].Username, receivedAccount.Username) + case receivedAccount.Email != users[0].Email: + t.Errorf("Expected Email %v but got %v", users[0].Email, receivedAccount.Email) + case receivedAccount.Firstname != users[0].Firstname: + t.Errorf("Expected Firstname %v but got %v", users[0].Firstname, receivedAccount.Firstname) + case receivedAccount.Lastname != users[0].Lastname: + t.Errorf("Expected Lastname %v but got %v", users[0].Lastname, receivedAccount.Lastname) + case receivedAccount.Role != users[0].Role: + t.Errorf("Expected Role %v but got %v", users[0].Role, receivedAccount.Role) + case receivedAccount.Status != users[0].Status: + t.Errorf("Expected Status %v but got %v", users[0].Status, receivedAccount.Status) + } + }) + + // Set up a test scenario: account not found + t.Run("Account Not Found", func(t *testing.T) { + req, err := http.NewRequest("GET", "/accounts/1000", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("Expected status code %d but got %d", http.StatusNotFound, w.Code) + } + }) +} + +func TestPostAccount(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PostAccounts handler + router.POST("/accounts", PostAccount) + + // Sample account data + newAccount := dataset.Account{ + Username: "Jane", + Email: "jane.doe@example.com", + Firstname: "Jane", + Lastname: "Doe", + Role: "standard", + Status: "active", + } + + // Convert account data to JSON + jsonData, err := json.Marshal(newAccount) + if err != nil { + t.Fatalf("Failed to marshal account data: %v", err) + } + + t.Run("Insert account", func(t *testing.T) { + + // Set up a test scenario: sending a POST request with JSON data + req, err := http.NewRequest("POST", "/accounts", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusCreated { + t.Errorf("Expected status code %d but got %d", http.StatusCreated, w.Code) + } + + // Query the database to get the inserted account + var insertedAccount dataset.Account + row := database.Db().QueryRow("SELECT * FROM account WHERE username = $1;", newAccount.Username) + err = row.Scan(&insertedAccount.ID, &insertedAccount.Username, &insertedAccount.Email, &insertedAccount.Firstname, &insertedAccount.Lastname, &insertedAccount.Role, &insertedAccount.Status, &insertedAccount.Created_at, &insertedAccount.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("No rows were returned!") + } + t.Fatalf("Failed to run request: %v", err) + } + + // Unmarshal the response body into an account struct + var receivedAccount dataset.Account + if err := json.Unmarshal(w.Body.Bytes(), &receivedAccount); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received account with the expected account data + switch { + case receivedAccount.Username != insertedAccount.Username: + t.Errorf("Expected Username %v but got %v", insertedAccount.Username, receivedAccount.Username) + case receivedAccount.Email != insertedAccount.Email: + t.Errorf("Expected Email %v but got %v", insertedAccount.Email, receivedAccount.Email) + case receivedAccount.Firstname != insertedAccount.Firstname: + t.Errorf("Expected Firstname %v but got %v", insertedAccount.Firstname, receivedAccount.Firstname) + case receivedAccount.Lastname != insertedAccount.Lastname: + t.Errorf("Expected Lastname %v but got %v", insertedAccount.Lastname, receivedAccount.Lastname) + case receivedAccount.Role != insertedAccount.Role: + t.Errorf("Expected Role %v but got %v", insertedAccount.Role, receivedAccount.Role) + case receivedAccount.Status != insertedAccount.Status: + t.Errorf("Expected Status %v but got %v", insertedAccount.Status, receivedAccount.Status) + } + }) +} + +func TestPutAccountByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PostAccounts handler + router.PUT("/accounts/:id", PutAccountByID) + + // Sample account data (with the third user in the dataset) + TestUpdatedAccount := dataset.Account{ + ID: users[2].ID, + Username: users[2].Username, + Email: "joseph.doe@example.com", + Firstname: "Joseph", + Lastname: "Doe", + Role: "standard", + Status: "active", + } + + // Convert account data to JSON + jsonData, err := json.Marshal(TestUpdatedAccount) + if err != nil { + t.Fatalf("Failed to marshal account data: %v", err) + } + + t.Run("Update account", func(t *testing.T) { + + // Format the path to the account ID + path := fmt.Sprintf("/accounts/%d", TestUpdatedAccount.ID) + + // Set up a test scenario: sending a PUT request with JSON data + req, err := http.NewRequest("PUT", path, bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Query the database to get the inserted account + var updatedAccount dataset.Account + row := database.Db().QueryRow("SELECT * FROM account WHERE id = $1;", TestUpdatedAccount.ID) + err = row.Scan(&updatedAccount.ID, &updatedAccount.Username, &updatedAccount.Email, &updatedAccount.Firstname, &updatedAccount.Lastname, &updatedAccount.Role, &updatedAccount.Status, &updatedAccount.Created_at, &updatedAccount.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("No rows were returned!") + } + t.Fatalf("Failed to run request: %v", err) + } + + // Compare the data in DB with Test dataset + switch { + case updatedAccount.Username != TestUpdatedAccount.Username: + t.Errorf("Expected Username %v but got %v", TestUpdatedAccount.Username, updatedAccount.Username) + case updatedAccount.Email != TestUpdatedAccount.Email: + t.Errorf("Expected Email %v but got %v", TestUpdatedAccount.Email, updatedAccount.Email) + case updatedAccount.Firstname != TestUpdatedAccount.Firstname: + t.Errorf("Expected Firstname %v but got %v", TestUpdatedAccount.Firstname, updatedAccount.Firstname) + case updatedAccount.Lastname != TestUpdatedAccount.Lastname: + t.Errorf("Expected Lastname %v but got %v", TestUpdatedAccount.Lastname, updatedAccount.Lastname) + case updatedAccount.Role != TestUpdatedAccount.Role: + t.Errorf("Expected Role %v but got %v", TestUpdatedAccount.Role, updatedAccount.Role) + case updatedAccount.Status != TestUpdatedAccount.Status: + t.Errorf("Expected Status %v but got %v", TestUpdatedAccount.Status, updatedAccount.Status) + } + }) +} + +func TestDeleteAccountByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PostAccounts handler + router.DELETE("/accounts/:id", DeleteAccountByID) + + t.Run("Delete account", func(t *testing.T) { + + // Format the path to the third user of the dataset + path := fmt.Sprintf("/accounts/%d", users[2].ID) + + // Set up a test scenario: sending a DELETE request + req, err := http.NewRequest("DELETE", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + + } + + // check in database if the account has been deleted + var username string + row := database.Db().QueryRow("SELECT username FROM account WHERE id = $1;", users[2].ID) + err = row.Scan(&username) + if err == nil { + t.Errorf("Account ID %v associated to username %s should be deleted and it is still in DB", users[2].ID, username) + } else if !errors.Is(err, sql.ErrNoRows) { + t.Fatalf("Failed to create request: %v", err) + + } + }) +} + +func TestRegister(t *testing.T) { + + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for Register handler + router.POST("/register", Register) + + // Sample account data + newAccount := dataset.RegisterInput{ + Username: "newuser", + Password: "password", + Email: "jane.doe@pmp.com", + Firstname: "Jane", + Lastname: "Doe", + } + + // Convert account data to JSON + jsonData, err := json.Marshal(newAccount) + if err != nil { + t.Fatalf("Failed to marshal account data: %v", err) + } + + t.Run("Register account", func(t *testing.T) { + + // Set up a test scenario: sending a POST request with JSON data + req, err := http.NewRequest("POST", "/register", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Query the database to get the inserted account + var insertedUser dataset.User + row := database.Db().QueryRow("SELECT a.username, a.email, a.firstname, a.lastname, a.role, a.status, p.password, a.created_at, a.updated_at FROM account a INNER JOIN password p ON a.id = p.user_id WHERE a.username = $1;", newAccount.Username) + err = row.Scan(&insertedUser.Username, &insertedUser.Email, &insertedUser.Firstname, &insertedUser.Lastname, &insertedUser.Role, &insertedUser.Status, &insertedUser.Password, &insertedUser.Created_at, &insertedUser.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + t.Fatalf("No user founded!") + } + t.Fatalf("Failed querry database: %v", err) + } + + err = security.VerifyPassword(newAccount.Password, insertedUser.Password) + if err != nil && errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + t.Errorf("encryption issue or validation issue with password: %v", err) + } + + switch { + case newAccount.Username != insertedUser.Username: + t.Errorf("Expected Username %v but got %v", insertedUser.Username, newAccount.Username) + case newAccount.Email != insertedUser.Email: + t.Errorf("Expected Email %v but got %v", insertedUser.Email, newAccount.Email) + case newAccount.Firstname != insertedUser.Firstname: + t.Errorf("Expected Firstname %v but got %v", insertedUser.Firstname, newAccount.Firstname) + case newAccount.Lastname != insertedUser.Lastname: + t.Errorf("Expected Lastname %v but got %v", insertedUser.Lastname, newAccount.Lastname) + case insertedUser.Role != "standard": + t.Errorf("Expected Role %v but got %v", "standard", insertedUser.Role) + case insertedUser.Status != "pending": + t.Errorf("Expected Status %v but got %v", "pending", insertedUser.Status) + } + }) +} + +func TestLogin(t *testing.T) { + + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for Login handler + router.POST("/login", Login) + + // Sample account data + newUser := dataset.User{ + Username: fmt.Sprintf("user-%s", random.UniqueId()), + Password: "password2", + Email: "newuser2@pmp.com", + Firstname: "Jules", + Lastname: "Doe", + Role: "standard", + Status: "pending", + Created_at: time.Now().Truncate(time.Second), + Updated_at: time.Now().Truncate(time.Second), + } + userLogin := dataset.LoginInput{ + Username: newUser.Username, + Password: newUser.Password, + } + + // insert account in database + var id int + + err := database.Db().QueryRow("INSERT INTO account (username, email, firstname, lastname, role, status, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id;", newUser.Username, newUser.Email, newUser.Firstname, newUser.Lastname, newUser.Role, newUser.Status, newUser.Created_at, newUser.Updated_at).Scan(&id) + if err != nil { + t.Fatalf("failed to insert user: %v", err) + } + + hashedPassword, err := security.HashPassword(newUser.Password) + if err != nil { + t.Fatalf("failed to hash password: %v", err) + } + + err = database.Db().QueryRow("INSERT INTO password (user_id, password, updated_at) VALUES ($1,$2,$3) RETURNING id;", id, hashedPassword, newUser.Updated_at).Scan(&id) + if err != nil { + t.Fatalf("failed to insert password: %v", err) + } + + // Convert user data to JSON + jsonData, err := json.Marshal(userLogin) + if err != nil { + t.Fatalf("Failed to marshal login data: %v", err) + } + + t.Run("Login user", func(t *testing.T) { + + // Set up a test scenario: sending a POST request with JSON data + req, err := http.NewRequest("POST", "/login", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Unmarshal the response body into an token struct + var receivedToken dataset.Token + if err := json.Unmarshal(w.Body.Bytes(), &receivedToken); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + if receivedToken.Token == "" { + t.Errorf("Expected token but got nil") + } + }) +} diff --git a/pkg/accounts/testdata.go b/pkg/accounts/testdata.go new file mode 100644 index 0000000..8cd9f1b --- /dev/null +++ b/pkg/accounts/testdata.go @@ -0,0 +1,73 @@ +package accounts + +import ( + "fmt" + "time" + + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gruntwork-io/terratest/modules/random" +) + +var users = []dataset.User{ + { + Username: fmt.Sprintf("user-%s", random.UniqueId()), + Email: fmt.Sprintf("user-%s@exemple.com", random.UniqueId()), + Firstname: "John", + Lastname: "Doe", + Role: "admin", + Status: "active", + Password: "password", + LastPassword: "password", + }, + { + Username: fmt.Sprintf("user-%s", random.UniqueId()), + Email: fmt.Sprintf("user-%s@exemple.com", random.UniqueId()), + Firstname: "Jane", + Lastname: "Smith", + Role: "standard", + Status: "pending", + Password: "password", + LastPassword: "", + }, + { + Username: fmt.Sprintf("user-%s", random.UniqueId()), + Email: fmt.Sprintf("user-%s@exemple.com", random.UniqueId()), + Firstname: "Alice", + Lastname: "Johnson", + Role: "standard", + Status: "inactive", + Password: "password", + LastPassword: "old_password", + }, +} + +func loadingAccountDataset() error { + + // Load accounts dataset + for i := range users { + var id uint + err := database.Db().QueryRow("INSERT INTO account (username, email, firstname, lastname, role, status, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id;", + users[i].Username, users[i].Email, users[i].Firstname, users[i].Lastname, users[i].Role, users[i].Status, time.Now().Truncate(time.Second), time.Now().Truncate(time.Second)).Scan(&users[i].ID) + if err != nil { + return err + } + + hashedPassword, err := security.HashPassword(users[i].Password) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + hashedLastPassword, err := security.HashPassword(users[i].LastPassword) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + err = database.Db().QueryRow("INSERT INTO password (user_id, password, last_password, updated_at) VALUES ($1,$2,$3,$4) RETURNING id;", users[i].ID, hashedPassword, hashedLastPassword, time.Now().Truncate(time.Second)).Scan(&id) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/config/env.go b/pkg/config/env.go new file mode 100644 index 0000000..7343b5b --- /dev/null +++ b/pkg/config/env.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +var ( + DbHost string + DbUser string + DbPassword string + DbName string + DbPort string + Stage string + ApiSecret string + TokenLifespan int +) + +func EnvInit(envFilePath string) error { + var err error + + if _, err := os.Stat(envFilePath); err == nil { + err := godotenv.Load(envFilePath) + if err != nil { + return fmt.Errorf("error loading .env file") + } + } + + DbHost = os.Getenv("DB_HOST") + DbUser = os.Getenv("DB_USER") + DbPassword = os.Getenv("DB_PASSWORD") + DbName = os.Getenv("DB_NAME") + DbPort = os.Getenv("DB_PORT") + Stage = os.Getenv("STAGE") + ApiSecret = os.Getenv("API_SECRET") + TokenLifespan, err = strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN")) + if err != nil { + TokenLifespan = 1 + } + + return nil +} diff --git a/pkg/database/database.go b/pkg/database/database.go new file mode 100644 index 0000000..2dfb2b0 --- /dev/null +++ b/pkg/database/database.go @@ -0,0 +1,44 @@ +package database + +import ( + "database/sql" + "fmt" + + "github.com/Angak0k/pimpmypack/pkg/config" + "github.com/Angak0k/pimpmypack/pkg/database/migration" + _ "github.com/lib/pq" +) + +var db *sql.DB + +func DbUrl() string { + return fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", config.DbUser, config.DbPassword, config.DbHost, config.DbPort, config.DbName) +} + +func DatabaseInit() error { + var err error + db, err = sql.Open("postgres", DbUrl()) + if err != nil { + return err + } + + err = db.Ping() + if err != nil { + return err + } + + return nil +} + +func DatabaseMigrate() error { + err := migration.Migration(DbUrl()) + if err != nil { + return err + } + return nil +} + +// Getter for db var +func Db() *sql.DB { + return db +} diff --git a/pkg/database/migration/migration.go b/pkg/database/migration/migration.go new file mode 100644 index 0000000..68be252 --- /dev/null +++ b/pkg/database/migration/migration.go @@ -0,0 +1,34 @@ +package migration + +import ( + "embed" + "errors" + + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed migration_scripts/*.sql +var fs embed.FS + +func Migration(dbUrl string) error { + + d, err := iofs.New(fs, "migration_scripts") + if err != nil { + return err + } + m, err := migrate.NewWithSourceInstance("iofs", d, dbUrl) + if err != nil { + return err + } + defer m.Close() + + // Apply all available migrations + err = m.Up() + if err != nil && !errors.Is(err, migrate.ErrNoChange) { + return err + } + return nil +} diff --git a/pkg/database/migration/migration_scripts/000001_account.down.sql b/pkg/database/migration/migration_scripts/000001_account.down.sql new file mode 100644 index 0000000..f788756 --- /dev/null +++ b/pkg/database/migration/migration_scripts/000001_account.down.sql @@ -0,0 +1,3 @@ +DROP TABLE "account"; +DROP TYPE "UserRole"; +DROP TYPE "UserStatus"; \ No newline at end of file diff --git a/pkg/database/migration/migration_scripts/000001_account.up.sql b/pkg/database/migration/migration_scripts/000001_account.up.sql new file mode 100644 index 0000000..f57c93c --- /dev/null +++ b/pkg/database/migration/migration_scripts/000001_account.up.sql @@ -0,0 +1,16 @@ +CREATE TYPE "UserRole" AS ENUM ('admin', 'standard'); +CREATE TYPE "UserStatus" AS ENUM ('active', 'pending', 'inactive'); + +CREATE TABLE "account" +( + "id" SERIAL PRIMARY KEY, + "username" TEXT NOT NULL UNIQUE, + "email" TEXT NOT NULL, + "firstname" TEXT NOT NULL, + "lastname" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT E'standard', + "status" "UserStatus" NOT NULL DEFAULT E'pending', + "created_at" TIMESTAMP NOT NULL, + "updated_at" TIMESTAMP NOT NULL +); + diff --git a/pkg/database/migration/migration_scripts/000002_inventory.down.sql b/pkg/database/migration/migration_scripts/000002_inventory.down.sql new file mode 100644 index 0000000..d9d1af7 --- /dev/null +++ b/pkg/database/migration/migration_scripts/000002_inventory.down.sql @@ -0,0 +1,3 @@ +DROP TABLE "inventory"; +DROP TYPE "WeightUnit"; +DROP TYPE "Currency"; diff --git a/pkg/database/migration/migration_scripts/000002_inventory.up.sql b/pkg/database/migration/migration_scripts/000002_inventory.up.sql new file mode 100644 index 0000000..40226cf --- /dev/null +++ b/pkg/database/migration/migration_scripts/000002_inventory.up.sql @@ -0,0 +1,23 @@ +CREATE TYPE "WeightUnit" AS ENUM ('METRIC', 'IMPERIAL'); +CREATE TYPE "Currency" AS ENUM ('USD', 'GBP', 'EUR'); + +CREATE TABLE "inventory" +( + "id" SERIAL PRIMARY KEY, + "user_id" INT NOT NULL, + "item_name" TEXT NOT NULL, + "category" TEXT, + "description" TEXT, + "weight" INT, + "weight_unit" "WeightUnit" NOT NULL DEFAULT E'METRIC', + "url" TEXT, + "price" INT, + "currency" "Currency" DEFAULT E'EUR', + "created_at" TIMESTAMP NOT NULL, + "updated_at" TIMESTAMP NOT NULL, + CONSTRAINT fk_account + FOREIGN KEY(user_id) + REFERENCES "account"(id) + ON DELETE CASCADE +); + diff --git a/pkg/database/migration/migration_scripts/000003_pack.down.sql b/pkg/database/migration/migration_scripts/000003_pack.down.sql new file mode 100644 index 0000000..8361655 --- /dev/null +++ b/pkg/database/migration/migration_scripts/000003_pack.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "pack_content"; +DROP TABLE "pack"; \ No newline at end of file diff --git a/pkg/database/migration/migration_scripts/000003_pack.up.sql b/pkg/database/migration/migration_scripts/000003_pack.up.sql new file mode 100644 index 0000000..94e7605 --- /dev/null +++ b/pkg/database/migration/migration_scripts/000003_pack.up.sql @@ -0,0 +1,34 @@ +CREATE TABLE "pack" +( + "id" SERIAL PRIMARY KEY, + "user_id" INT NOT NULL, + "pack_name" TEXT NOT NULL, + "pack_description" TEXT, + "created_at" TIMESTAMP NOT NULL, + "updated_at" TIMESTAMP NOT NULL, + CONSTRAINT fk_account + FOREIGN KEY(user_id) + REFERENCES "account"(id) + ON DELETE CASCADE +); + +CREATE TABLE "pack_content" +( + "id" SERIAL PRIMARY KEY, + "pack_id" INT NOT NULL, + "item_id" INT NOT NULL, + "quantity" INT, + "worn" BOOLEAN, + "consumable" BOOLEAN, + "created_at" TIMESTAMP NOT NULL, + "updated_at" TIMESTAMP NOT NULL, + UNIQUE ("pack_id", "item_id"), + CONSTRAINT fk_pack + FOREIGN KEY ("pack_id") + REFERENCES "pack"(id) + ON DELETE CASCADE, + CONSTRAINT fk_item + FOREIGN KEY ("item_id") + REFERENCES "inventory"(id) + ON DELETE CASCADE +); \ No newline at end of file diff --git a/pkg/database/migration/migration_scripts/000004_password.down.sql b/pkg/database/migration/migration_scripts/000004_password.down.sql new file mode 100644 index 0000000..ef83b24 --- /dev/null +++ b/pkg/database/migration/migration_scripts/000004_password.down.sql @@ -0,0 +1 @@ +DROP TABLE "password"; diff --git a/pkg/database/migration/migration_scripts/000004_password.up.sql b/pkg/database/migration/migration_scripts/000004_password.up.sql new file mode 100644 index 0000000..0dae970 --- /dev/null +++ b/pkg/database/migration/migration_scripts/000004_password.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "password" +( + "id" SERIAL PRIMARY KEY, + "user_id" INT NOT NULL, + "password" TEXT NOT NULL, + "last_password" TEXT, + "updated_at" TIMESTAMP NOT NULL, + CONSTRAINT fk_account + FOREIGN KEY(user_id) + REFERENCES "account"(id) + ON DELETE CASCADE +); \ No newline at end of file diff --git a/pkg/dataset/dataset.go b/pkg/dataset/dataset.go new file mode 100644 index 0000000..03e759a --- /dev/null +++ b/pkg/dataset/dataset.go @@ -0,0 +1,129 @@ +package dataset + +import ( + "time" +) + +type Account struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Role string `json:"role"` + Status string `json:"status"` + Created_at time.Time `json:"created_at"` + Updated_at time.Time `json:"updated_at"` +} + +type Accounts []Account + +type Inventory struct { + ID uint `json:"id"` + User_id uint `json:"user_id"` + Item_name string `json:"item_name"` + Category string `json:"category"` + Description string `json:"description"` + Weight int `json:"weight"` + Weight_unit string `json:"weight_unit"` + Url string `json:"url"` + Price int `json:"price"` + Currency string `json:"currency"` + Created_at time.Time `json:"created_at"` + Updated_at time.Time `json:"updated_at"` +} + +type Inventories []Inventory + +type Pack struct { + ID uint `json:"id"` + User_id uint `json:"user_id"` + Pack_name string `json:"pack_name"` + Pack_description string `json:"pack_description"` + Created_at time.Time `json:"created_at"` + Updated_at time.Time `json:"updated_at"` +} + +type Packs []Pack + +type PackContent struct { + ID uint `json:"id"` + Pack_id uint `json:"pack_id"` + Item_id uint `json:"item_id"` + Quantity int `json:"quantity"` + Worn bool `json:"worn"` + Consumable bool `json:"consumable"` + Created_at time.Time `json:"created_at"` + Updated_at time.Time `json:"updated_at"` +} + +type PackContents []PackContent + +type PackContentWithItem struct { + Pack_content_id uint `json:"pack_content_id"` + Pack_id uint `json:"pack_id"` + Inventory_id uint `json:"inventory_id"` + Item_name string `json:"item_name"` + Category string `json:"category"` + Item_description string `json:"item_description"` + Weight int `json:"weight"` + Weight_unit string `json:"weight_unit"` + Item_url string `json:"item_url"` + Price int `json:"price"` + Currency string `json:"currency"` + Quantity int `json:"quantity"` + Worn bool `json:"worn"` + Consumable bool `json:"consumable"` +} + +type PackContentWithItems []PackContentWithItem + +type LighterPackItem struct { + Item_name string `json:"item_name"` + Category string `json:"category"` + Desc string `json:"desc"` + Qty int `json:"qty"` + Weight int `json:"weight"` + Unit string `json:"unit"` + Url string `json:"url"` + Price int `json:"price"` + Worn bool `json:"worn"` + Consumable bool `json:"consumable"` +} + +type LighterPack []LighterPackItem + +type RegisterInput struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Email string `json:"email" binding:"required"` + Firstname string `json:"firstname" binding:"required"` + Lastname string `json:"lastname" binding:"required"` +} + +type User struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Role string `json:"role"` + Status string `json:"status"` + Password string `json:"password"` + LastPassword string `json:"last_password"` + Created_at time.Time `json:"created_at"` + Updated_at time.Time `json:"updated_at"` +} + +type LoginInput struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type Token struct { + Token string `json:"token"` +} + +type ErrorResponse struct { + Error string `json:"error"` +} diff --git a/pkg/helper/helper.go b/pkg/helper/helper.go new file mode 100644 index 0000000..db801d3 --- /dev/null +++ b/pkg/helper/helper.go @@ -0,0 +1,67 @@ +package helper + +import ( + "strconv" + + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/joho/godotenv" +) + +func StringToUint(s string) (uint, error) { + // Convert a string to an unsigned int. + i, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return 0, err + } + return uint(i), nil +} + +func ConvertWeightUnit(unit string) string { + // Convert a weight unit to an enum, METRIC by default + switch unit { + case "gram": + return "METRIC" + case "oz": + return "IMPERIAL" + } + return "METRIC" +} + +func EnvInit() error { + // Load environment variables from .env file + err := godotenv.Load() + if err != nil { + return err + } + return nil +} + +func FinUserIDByUsername(users []dataset.User, username string) uint { + // Find a user ID by username + for _, user := range users { + if user.Username == username { + return user.ID + } + } + return 0 +} + +func FinPackIDByPackName(packs dataset.Packs, packname string) uint { + // Find a pack ID by packname + for _, pack := range packs { + if pack.Pack_name == packname { + return pack.ID + } + } + return 0 +} + +func FinItemIDByItemName(inventories dataset.Inventories, itemname string) uint { + // Find an item ID by itemname + for _, item := range inventories { + if item.Item_name == itemname { + return item.ID + } + } + return 0 +} diff --git a/pkg/inventories/inventories.go b/pkg/inventories/inventories.go new file mode 100644 index 0000000..e3e4829 --- /dev/null +++ b/pkg/inventories/inventories.go @@ -0,0 +1,510 @@ +package inventories + +import ( + "database/sql" + "errors" + "net/http" + "time" + + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/helper" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gin-gonic/gin" +) + +// GetInventories gets all inventories +// @Summary [ADMIN] Get all inventories +// @Description Retrieves a list of all inventories - for admin use only +// @Security Bearer +// @Tags Inventories +// @Produce json +// @Success 200 {array} dataset.Inventory "List of Inventories" +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /inventories [get] +func GetInventories(c *gin.Context) { + + inventories, err := returnInventories() + + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if len(*inventories) != 0 { + c.IndentedJSON(http.StatusOK, *inventories) + } else { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "No inventories empty"}) + } +} + +func returnInventories() (*dataset.Inventories, error) { + var inventories dataset.Inventories + + rows, err := database.Db().Query("SELECT id, user_id, item_name, category, description, weight, weight_unit, url, price, currency, created_at, updated_at FROM inventory;") + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + defer rows.Close() + + for rows.Next() { + var inventory dataset.Inventory + err := rows.Scan(&inventory.ID, &inventory.User_id, &inventory.Item_name, &inventory.Category, &inventory.Description, &inventory.Weight, &inventory.Weight_unit, &inventory.Url, &inventory.Price, &inventory.Currency, &inventory.Created_at, &inventory.Updated_at) + if err != nil { + return nil, err + } + inventories = append(inventories, inventory) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return &inventories, nil +} + +// GetMyInventories gets all inventories of the user +// @Summary Get all inventories of the user +// @Description Retrieves a list of all inventories of the user +// @Security Bearer +// @Tags Inventories +// @Produce json +// @Success 200 {array} dataset.Inventory +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /myinventory [get] +func GetMyInventory(c *gin.Context) { + + user_id, err := security.ExtractTokenID(c) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + inventories, err := returnInventoriesByUserID(user_id) + + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if len(*inventories) != 0 { + c.IndentedJSON(http.StatusOK, *inventories) + } else { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "No inventories empty"}) + } + +} + +func returnInventoriesByUserID(user_id uint) (*dataset.Inventories, error) { + var inventories dataset.Inventories + + rows, err := database.Db().Query("SELECT id, user_id, item_name, category, description, weight, weight_unit, url, price, currency, created_at, updated_at FROM inventory WHERE user_id = $1 ORDER BY id;", user_id) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var inventory dataset.Inventory + err := rows.Scan(&inventory.ID, &inventory.User_id, &inventory.Item_name, &inventory.Category, &inventory.Description, &inventory.Weight, &inventory.Weight_unit, &inventory.Url, &inventory.Price, &inventory.Currency, &inventory.Created_at, &inventory.Updated_at) + if err != nil { + return nil, err + } + inventories = append(inventories, inventory) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return &inventories, nil +} + +// GetInventoryByID gets an inventory by ID +// @Summary [ADMIN] Get an inventory by ID +// @Description Retrieves an inventory by ID - for admin use only +// @Security Bearer +// @Tags Inventories +// @Produce json +// @Param id path int true "Inventory ID" +// @Success 200 {object} dataset.Inventory +// @Failure 400 {object} map[string]interface{} "Invalid ID format" +// @Failure 404 {object} map[string]interface{} "Inventory not found" +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /inventories/{id} [get] +func GetInventoryByID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + inventory, err := findInventoryById(id) + + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if inventory == nil { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "Inventory not found"}) + return + } + + c.IndentedJSON(http.StatusOK, inventory) +} + +// GetMyInventoryByID gets an inventory by ID +// @Summary Get an inventory by ID +// @Description Retrieves an inventory by ID +// @Security Bearer +// @Tags Inventories +// @Produce json +// @Param id path int true "Inventory ID" +// @Success 200 {object} dataset.Inventory +// @Failure 400 {object} map[string]interface{} "Invalid ID format" +// @Failure 403 {object} map[string]interface{} "This item does not belong to you" +// @Failure 404 {object} map[string]interface{} "Inventory not found" +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /myinventory/{id} [get] +func GetMyInventoryByID(c *gin.Context) { + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + myInventory, err := checkInventoryOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myInventory { + inventory, err := findInventoryById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } else { + c.IndentedJSON(http.StatusOK, inventory) + } + } else { + c.IndentedJSON(http.StatusForbidden, gin.H{"error": "This item does not belong to you"}) + } +} + +func findInventoryById(id uint) (*dataset.Inventory, error) { + var inventory dataset.Inventory + + row := database.Db().QueryRow("SELECT id, user_id, item_name, category, description, weight, weight_unit, url, price, currency, created_at, updated_at FROM inventory WHERE id = $1;", id) + err := row.Scan(&inventory.ID, &inventory.User_id, &inventory.Item_name, &inventory.Category, &inventory.Description, &inventory.Weight, &inventory.Weight_unit, &inventory.Url, &inventory.Price, &inventory.Currency, &inventory.Created_at, &inventory.Updated_at) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + return &inventory, nil +} + +// PostInventory creates an inventory +// @Summary [ADMIN] Create an inventory +// @Description Creates an inventory - for admin use only +// @Security Bearer +// @Tags Inventories +// @Accept json +// @Produce json +// @Param inventory body dataset.Inventory true "Inventory" +// @Success 201 {object} dataset.Inventory +// @Failure 400 {object} map[string]interface{} "Invalid payload" +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /inventories [post] +func PostInventory(c *gin.Context) { + var newInventory dataset.Inventory + + if err := c.BindJSON(&newInventory); err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + err := InsertInventory(&newInventory) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusCreated, newInventory) + +} + +// PostMyInventory creates an inventory +// @Summary Create an inventory +// @Description Creates an inventory +// @Security Bearer +// @Tags Inventories +// @Accept json +// @Produce json +// @Param inventory body dataset.Inventory true "Inventory" +// @Success 201 {object} dataset.Inventory +// @Failure 400 {object} map[string]interface{} "Invalid payload" +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /myinventory [post] +func PostMyInventory(c *gin.Context) { + var newInventory dataset.Inventory + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + newInventory.User_id = user_id + + if err := c.BindJSON(&newInventory); err != nil { + return + } + + err = InsertInventory(&newInventory) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusCreated, newInventory) + +} + +func InsertInventory(i *dataset.Inventory) error { + + if i == nil { + return errors.New("payload is empty") + } + + i.Created_at = time.Now().Truncate(time.Second) + i.Updated_at = time.Now().Truncate(time.Second) + + err := database.Db().QueryRow("INSERT INTO inventory (user_id, item_name, category, description, weight, weight_unit, url, price, currency, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id;", i.User_id, i.Item_name, i.Category, i.Description, i.Weight, i.Weight_unit, i.Url, i.Price, i.Currency, i.Created_at, i.Updated_at).Scan(&i.ID) + + if err != nil { + return err + } + + return nil + +} + +// PutInventoryByID updates an inventory by ID +// @Summary [ADMIN] Update an inventory by ID +// @Description Updates an inventory by ID - for admin use only +// @Security Bearer +// @Tags Inventories +// @Accept json +// @Produce json +// @Param id path int true "Inventory ID" +// @Param inventory body dataset.Inventory true "Inventory" +// @Success 200 {object} dataset.Inventory +// @Failure 400 {object} map[string]interface{} "Invalid ID format" +// @Failure 400 {object} map[string]interface{} "Invalid payload" +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /inventories/{id} [put] +func PutInventoryByID(c *gin.Context) { + var updatedInventory dataset.Inventory + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + if err := c.BindJSON(&updatedInventory); err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + updatedInventory.ID = id + + err = updateInventoryById(id, &updatedInventory) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, updatedInventory) + +} + +// PutMyInventoryByID updates an inventory by ID +// @Summary Update an inventory by ID +// @Description Updates an inventory by ID +// @Security Bearer +// @Tags Inventories +// @Accept json +// @Produce json +// @Param id path int true "Inventory ID" +// @Param inventory body dataset.Inventory true "Inventory" +// @Success 200 {object} dataset.Inventory +// @Failure 400 {object} map[string]interface{} "Invalid ID format" +// @Failure 400 {object} map[string]interface{} "Invalid payload" +// @Failure 403 {object} map[string]interface{} "This item does not belong to you" +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /myinventory/{id} [put] +func PutMyInventoryByID(c *gin.Context) { + var updatedInventory dataset.Inventory + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + myInventory, err := checkInventoryOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myInventory { + updatedInventory.User_id = user_id + if err := c.BindJSON(&updatedInventory); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + err = updateInventoryById(id, &updatedInventory) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } else { + c.IndentedJSON(http.StatusOK, updatedInventory) + } + } else { + c.IndentedJSON(http.StatusForbidden, gin.H{"error": "This item does not belong to you"}) + } +} + +func updateInventoryById(id uint, i *dataset.Inventory) error { + + if i == nil { + return errors.New("payload is empty") + } + + i.Updated_at = time.Now().Truncate(time.Second) + statement, err := database.Db().Prepare("UPDATE inventory SET user_id=$1, item_name=$2, category=$3, description=$4, weight=$5, weight_unit=$6, url=$7, price=$8, currency=$9, updated_at=$10 WHERE id=$11;") + if err != nil { + return err + } + _, err = statement.Exec(i.User_id, i.Item_name, i.Category, i.Description, i.Weight, i.Weight_unit, i.Url, i.Price, i.Currency, i.Updated_at, id) + if err != nil { + return err + } + + return nil +} + +// DeleteInventoryByID deletes an inventory by ID +// @Summary [ADMIN] Delete an inventory by ID +// @Description Deletes an inventory by ID - for admin use only +// @Security Bearer +// @Tags Inventories +// @Produce json +// @Param id path int true "Inventory ID" +// @Success 200 {object} string "Inventory deleted" +// @Failure 400 {object} map[string]interface{} "Invalid ID format" +// @Failure 500 {object} map[string]interface{} "Internal Server Error" +// @Router /inventories/{id} [delete] +func DeleteInventoryByID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + err = deleteInventoryById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, gin.H{"message": "Inventory deleted"}) + +} + +func DeleteMyInventoryByID(c *gin.Context) { + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + myInventory, err := checkInventoryOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myInventory { + err = deleteInventoryById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, gin.H{"message": "Inventory deleted"}) + } else { + c.IndentedJSON(http.StatusForbidden, gin.H{"error": "This item does not belong to you"}) + } +} + +func deleteInventoryById(id uint) error { + statement, err := database.Db().Prepare("DELETE FROM inventory WHERE id=$1;") + if err != nil { + return err + } + _, err = statement.Exec(id) + if err != nil { + return err + } + + return nil +} + +func checkInventoryOwnership(id uint, user_id uint) (bool, error) { + var rows int + + row := database.Db().QueryRow("SELECT COUNT(id) FROM inventory WHERE id = $1 AND user_id = $2;", id, user_id) + err := row.Scan(&rows) + if err != nil { + return false, err + } + + if rows == 0 { + return false, nil + } else { + return true, nil + } +} diff --git a/pkg/inventories/inventories_test.go b/pkg/inventories/inventories_test.go new file mode 100644 index 0000000..b954940 --- /dev/null +++ b/pkg/inventories/inventories_test.go @@ -0,0 +1,601 @@ +package inventories + +import ( + "bytes" + "database/sql" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/Angak0k/pimpmypack/pkg/config" + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gin-gonic/gin" + "github.com/google/go-cmp/cmp" +) + +func TestMain(m *testing.M) { + + // init env + err := config.EnvInit("../../.env") + if err != nil { + log.Fatalf("Error loading .env file or environement variable : %v", err) + } + + // init DB + err = database.DatabaseInit() + if err != nil { + log.Fatalf("Error connecting database : %v", err) + } + + // init DB migration + err = database.DatabaseMigrate() + if err != nil { + log.Fatalf("Error migrating database : %v", err) + } + + // init dataset + err = loadingInventoryDataset() + if err != nil { + log.Fatalf("Error loading dataset : %v", err) + } + + ret := m.Run() + os.Exit(ret) +} + +func TestGetInventories(t *testing.T) { + var getInventories dataset.Inventories + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetInventories handler + router.GET("/inventories", GetInventories) + + t.Run("Inventories List Retrieved", func(t *testing.T) { + // Create a mock HTTP request to the /inventories endpoint + req, err := http.NewRequest("GET", "/inventories", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Serve the HTTP request to the Gin router + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Check the Content-Type header + expectedContentType := "application/json; charset=utf-8" + contentType := w.Header().Get("Content-Type") + if contentType != expectedContentType { + t.Errorf("Expected content type %s but got %s", expectedContentType, contentType) + } + + // Unmarshal the response body into a slice of inventories struct + if err := json.Unmarshal(w.Body.Bytes(), &getInventories); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + // determine if the inventory - and only the expected inventory - is in the database + if len(getInventories) < 3 { + t.Errorf("Expected almost 3 inventory but got %d", len(getInventories)) + } else { + switch { + case !cmp.Equal(getInventories[0].User_id, inventories[0].User_id): + t.Errorf("Expected User_id %v but got %v", inventories[0].User_id, getInventories[0].User_id) + case !cmp.Equal(getInventories[0].Item_name, inventories[0].Item_name): + t.Errorf("Expected Item Name %v but got %v", inventories[0].Item_name, getInventories[0].Item_name) + case !cmp.Equal(getInventories[0].Category, inventories[0].Category): + t.Errorf("Expected Category %v but got %v", inventories[0].Category, getInventories[0].Category) + case !cmp.Equal(getInventories[0].Description, inventories[0].Description): + t.Errorf("Expected Description %v but got %v", inventories[0].Description, getInventories[0].Description) + case !cmp.Equal(getInventories[0].Weight, inventories[0].Weight): + t.Errorf("Expected Weight %v but got %v", inventories[0].Weight, getInventories[0].Weight) + case !cmp.Equal(getInventories[0].Weight_unit, inventories[0].Weight_unit): + t.Errorf("Expected Weight_unit %v but got %v", inventories[0].Weight_unit, getInventories[0].Weight_unit) + case !cmp.Equal(getInventories[0].Url, inventories[0].Url): + t.Errorf("Expected Url %v but got %v", inventories[0].Url, getInventories[0].Url) + case !cmp.Equal(getInventories[0].Price, inventories[0].Price): + t.Errorf("Expected Price %v but got %v", inventories[0].Price, getInventories[0].Price) + case !cmp.Equal(getInventories[0].Currency, inventories[0].Currency): + t.Errorf("Expected Currency %v but got %v", inventories[0].Currency, getInventories[0].Currency) + } + } + }) +} + +func TestGetMyInventory(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetMyInventory handler + router.GET("/myinventory", GetMyInventory) + + t.Run("Inventories List Retrieved", func(t *testing.T) { + token, err := security.GenerateToken(users[0].ID) + if err != nil { + t.Fatalf("Failed to generate token: %v", err) + } + // Create a mock HTTP request to the /myinventory endpoint + req, err := http.NewRequest("GET", "/myinventory", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Set up a test scenario: sending a GET request + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Serve the HTTP request to the Gin router + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Check the Content-Type header + expectedContentType := "application/json; charset=utf-8" + contentType := w.Header().Get("Content-Type") + if contentType != expectedContentType { + t.Errorf("Expected content type %s but got %s", expectedContentType, contentType) + } + + // Unmarshal the response body into a slice of inventories struct + var myInventories dataset.Inventories + if err := json.Unmarshal(w.Body.Bytes(), &myInventories); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // determine if the inventory - and only the expected inventory - is the expected content + if len(myInventories) < 3 { + t.Errorf("Expected almost 3 inventory but got %d", len(myInventories)) + } else { + switch { + case !cmp.Equal(myInventories[0].User_id, inventories[0].User_id): + t.Errorf("Expected User_id %v but got %v", inventories[0].User_id, myInventories[0].User_id) + case !cmp.Equal(myInventories[0].Item_name, inventories[0].Item_name): + t.Errorf("Expected Item Name %v but got %v", inventories[0].Item_name, myInventories[0].Item_name) + case !cmp.Equal(myInventories[0].Category, inventories[0].Category): + t.Errorf("Expected Category %v but got %v", inventories[0].Category, myInventories[0].Category) + case !cmp.Equal(myInventories[0].Description, inventories[0].Description): + t.Errorf("Expected Description %v but got %v", inventories[0].Description, myInventories[0].Description) + case !cmp.Equal(myInventories[0].Weight, inventories[0].Weight): + t.Errorf("Expected Weight %v but got %v", inventories[0].Weight, myInventories[0].Weight) + case !cmp.Equal(myInventories[0].Weight_unit, inventories[0].Weight_unit): + t.Errorf("Expected Weight_unit %v but got %v", inventories[0].Weight_unit, myInventories[0].Weight_unit) + case !cmp.Equal(myInventories[0].Url, inventories[0].Url): + t.Errorf("Expected Url %v but got %v", inventories[0].Url, myInventories[0].Url) + case !cmp.Equal(myInventories[0].Price, inventories[0].Price): + t.Errorf("Expected Price %v but got %v", inventories[0].Price, myInventories[0].Price) + case !cmp.Equal(myInventories[0].Currency, inventories[0].Currency): + t.Errorf("Expected Currency %v but got %v", inventories[0].Currency, myInventories[0].Currency) + case cmp.Equal(myInventories[1].Updated_at, inventories[1].Updated_at): + t.Errorf("Expected Updated_at %v should be different than %v", inventories[1].Updated_at, myInventories[1].Updated_at) + } + } + }) +} + +func TestGetInventoryByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetInventoryByID handler + router.GET("/inventories/:id", GetInventoryByID) + + t.Run("Inventory Retrieved", func(t *testing.T) { + // Create a mock HTTP request to the /inventories endpoint + path := fmt.Sprintf("/inventories/%d", inventories[0].ID) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Serve the HTTP request to the Gin router + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Unmarshal the response body into an inventory struct + var receivedInventory dataset.Inventory + if err := json.Unmarshal(w.Body.Bytes(), &receivedInventory); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received Inventory with the expected Inventory + switch { + case receivedInventory.User_id != inventories[0].User_id: + t.Errorf("Expected User_id %v but got %v", inventories[0].User_id, receivedInventory.User_id) + case receivedInventory.Item_name != inventories[0].Item_name: + t.Errorf("Expected Item_name %v but got %v", inventories[0].Item_name, receivedInventory.Item_name) + case receivedInventory.Category != inventories[0].Category: + t.Errorf("Expected Category %v but got %v", inventories[0].Category, receivedInventory.Category) + case receivedInventory.Description != inventories[0].Description: + t.Errorf("Expected Description %v but got %v", inventories[0].Description, receivedInventory.Description) + case receivedInventory.Weight != inventories[0].Weight: + t.Errorf("Expected Weight %v but got %v", inventories[0].Weight, receivedInventory.Weight) + case receivedInventory.Weight_unit != inventories[0].Weight_unit: + t.Errorf("Expected Weight_unit %v but got %v", inventories[0].Weight_unit, receivedInventory.Weight_unit) + case receivedInventory.Url != inventories[0].Url: + t.Errorf("Expected Url %v but got %v", inventories[0].Url, receivedInventory.Url) + case receivedInventory.Price != inventories[0].Price: + t.Errorf("Expected Price %v but got %v", inventories[0].Price, receivedInventory.Price) + case receivedInventory.Currency != inventories[0].Currency: + t.Errorf("Expected Currency %v but got %v", inventories[0].Currency, receivedInventory.Currency) + } + }) + + // Set up a test scenario: inventory not found + t.Run("Inventory Not Found", func(t *testing.T) { + req, err := http.NewRequest("GET", "/inventories/1000", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("Expected status code %d but got %d", http.StatusNotFound, w.Code) + } + }) +} + +func TestPostInventory(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PostInventory handler + router.POST("/inventories", PostInventory) + + // Sample inventory data + newInventory := dataset.Inventory{ + User_id: users[0].ID, + Item_name: "Light", + Category: "Outdoor Gear", + Description: "Headed Light", + Weight: 29, + Weight_unit: "METRIC", + Url: "https://example.com/light", + Price: 30, + Currency: "USD", + } + + // Convert inventory data to JSON + jsonData, err := json.Marshal(newInventory) + if err != nil { + t.Fatalf("Failed to marshal inventory data: %v", err) + } + + t.Run("Insert Inventory", func(t *testing.T) { + + // Set up a test scenario: sending a POST request with JSON data + req, err := http.NewRequest("POST", "/inventories", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusCreated { + t.Errorf("Expected status code %d but got %d", http.StatusCreated, w.Code) + } + + // Query the database to get the inserted inventory + var insertedInventory dataset.Inventory + row := database.Db().QueryRow("SELECT * FROM inventory WHERE item_name = $1;", newInventory.Item_name) + err = row.Scan(&insertedInventory.ID, &insertedInventory.User_id, &insertedInventory.Item_name, &insertedInventory.Category, &insertedInventory.Description, &insertedInventory.Weight, &insertedInventory.Weight_unit, &insertedInventory.Url, &insertedInventory.Price, &insertedInventory.Currency, &insertedInventory.Created_at, &insertedInventory.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("No rows were returned!") + } + t.Fatalf("Failed to run request: %v", err) + } + + // Unmarshal the response body into an inventory struct + var receivedInventory dataset.Inventory + if err := json.Unmarshal(w.Body.Bytes(), &receivedInventory); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received inventory with the expected inventory data + switch { + case receivedInventory.ID != insertedInventory.ID: + t.Errorf("Expected ID %v but got %v", insertedInventory.ID, receivedInventory.ID) + case receivedInventory.User_id != insertedInventory.User_id: + t.Errorf("Expected User_ID %v but got %v", insertedInventory.User_id, receivedInventory.User_id) + case receivedInventory.Item_name != insertedInventory.Item_name: + t.Errorf("Expected Item_name %v but got %v", insertedInventory.Item_name, receivedInventory.Item_name) + case receivedInventory.Category != insertedInventory.Category: + t.Errorf("Expected Category %v but got %v", insertedInventory.Category, receivedInventory.Category) + case receivedInventory.Description != insertedInventory.Description: + t.Errorf("Expected Description %v but got %v", insertedInventory.Description, receivedInventory.Description) + case receivedInventory.Weight != insertedInventory.Weight: + t.Errorf("Expected Weight %v but got %v", insertedInventory.Weight, receivedInventory.Weight) + case receivedInventory.Weight_unit != insertedInventory.Weight_unit: + t.Errorf("Expected Weight_unit %v but got %v", insertedInventory.Weight_unit, receivedInventory.Weight_unit) + case receivedInventory.Url != insertedInventory.Url: + t.Errorf("Expected Url %v but got %v", insertedInventory.Url, receivedInventory.Url) + case receivedInventory.Price != insertedInventory.Price: + t.Errorf("Expected Price %v but got %v", insertedInventory.Price, receivedInventory.Price) + case receivedInventory.Currency != insertedInventory.Currency: + t.Errorf("Expected Currency %v but got %v", insertedInventory.Currency, receivedInventory.Currency) + } + }) +} + +func TestPutInventoryByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for Postinventorys handler + router.PUT("/inventories/:id", PutInventoryByID) + + // Sample inventory data (with the first user of the dataset and the second inventory of the dataset) + TestUpdatedInventory := dataset.Inventory{ + ID: inventories[1].ID, + User_id: users[0].ID, + Item_name: "Tent", + Category: "Outdoor Gear", + Description: "Lightweight tent for camping", + Weight: 1200, + Weight_unit: "METRIC", + Url: "https://example.com/tent", + Price: 200, + Currency: "USD", + } + + // Convert inventory data to JSON + jsonData, err := json.Marshal(TestUpdatedInventory) + if err != nil { + t.Fatalf("Failed to marshal inventory data: %v", err) + } + + t.Run("Update Inventory", func(t *testing.T) { + + // Set up a test scenario: sending a PUT request with JSON data + path := fmt.Sprintf("/inventories/%d", TestUpdatedInventory.ID) + req, err := http.NewRequest("PUT", path, bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Query the database to get the updated inventories + var updatedInventory dataset.Inventory + row := database.Db().QueryRow("SELECT * FROM inventory WHERE id = $1;", TestUpdatedInventory.ID) + err = row.Scan(&updatedInventory.ID, &updatedInventory.User_id, &updatedInventory.Item_name, &updatedInventory.Category, &updatedInventory.Description, &updatedInventory.Weight, &updatedInventory.Weight_unit, &updatedInventory.Url, &updatedInventory.Price, &updatedInventory.Currency, &updatedInventory.Created_at, &updatedInventory.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("No rows were returned!") + } + t.Fatalf("Failed to run request: %v", err) + } + + // Compare the data in DB with Test dataset + switch { + case updatedInventory.Item_name != TestUpdatedInventory.Item_name: + t.Errorf("Expected Item_name %v but got %v", TestUpdatedInventory.Item_name, updatedInventory.Item_name) + case updatedInventory.Category != TestUpdatedInventory.Category: + t.Errorf("Expected Category %v but got %v", TestUpdatedInventory.Category, updatedInventory.Category) + case updatedInventory.Description != TestUpdatedInventory.Description: + t.Errorf("Expected Description %v but got %v", TestUpdatedInventory.Description, updatedInventory.Description) + case updatedInventory.Weight != TestUpdatedInventory.Weight: + t.Errorf("Expected Weight %v but got %v", TestUpdatedInventory.Weight, updatedInventory.Weight) + case updatedInventory.Weight_unit != TestUpdatedInventory.Weight_unit: + t.Errorf("Expected Weight_unit %v but got %v", TestUpdatedInventory.Weight_unit, updatedInventory.Weight_unit) + case updatedInventory.Url != TestUpdatedInventory.Url: + t.Errorf("Expected Url %v but got %v", TestUpdatedInventory.Url, updatedInventory.Url) + case updatedInventory.Price != TestUpdatedInventory.Price: + t.Errorf("Expected Price %v but got %v", TestUpdatedInventory.Price, updatedInventory.Price) + } + }) +} + +func TestDeleteInventoryByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PostInventorys handler + router.DELETE("/inventories/:id", DeleteInventoryByID) + + t.Run("Delete inventory", func(t *testing.T) { + + // Set up a test scenario: sending a DELETE request with the third inventory of the dataset + path := fmt.Sprintf("/inventories/%d", inventories[2].ID) + req, err := http.NewRequest("DELETE", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + + } + + // check in database if the inventory has been deleted + var item_name string + row := database.Db().QueryRow("SELECT item_name FROM inventory WHERE id = $1;", inventories[2].ID) + err = row.Scan(&item_name) + if err == nil { + t.Errorf("Inventory ID 3 associated to item_name %s should be deleted and it is still in DB", item_name) + } else if !errors.Is(err, sql.ErrNoRows) { + t.Fatalf("Failed to create request: %v", err) + } + }) +} + +func TestCheckInventoryOwnership(t *testing.T) { + + t.Run("Test Pack Ownership", func(t *testing.T) { + + inventoryID := inventories[0].ID + userID := users[0].ID + wrongUserID := users[1].ID + + myInventory, err := checkInventoryOwnership(inventoryID, userID) + if err != nil { + t.Fatalf("Failed to check inventory ownership: %v", err) + } + if !myInventory { + t.Errorf("Expected true but got false") + } + + notmyInventory, err := checkInventoryOwnership(inventoryID, wrongUserID) + if err != nil { + t.Fatalf("Failed to check inventory ownership: %v", err) + } + if notmyInventory { + t.Errorf("Expected false but got true") + } + + }) +} + +func TestGetMyInventoryByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + // Define the endpoint for GetMyInventoryByID handler + router.GET("/myinventory/:id", GetMyInventoryByID) + + t.Run("Item Retrieved", func(t *testing.T) { + token, err := security.GenerateToken(users[0].ID) + if err != nil { + t.Fatalf("Failed to generate token: %v", err) + } + // Create a mock HTTP request to the /myinventory endpoint + path := fmt.Sprintf("/myinventory/%d", inventories[0].ID) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Set up a test scenario: sending a GET request + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Serve the HTTP request to the Gin router + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Unmarshal the response body into an inventory struct + var receivedInventory dataset.Inventory + if err := json.Unmarshal(w.Body.Bytes(), &receivedInventory); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received Inventory with the expected Inventory + switch { + case receivedInventory.User_id != inventories[0].User_id: + t.Errorf("Expected User_id %v but got %v", inventories[0].User_id, receivedInventory.User_id) + case receivedInventory.Item_name != inventories[0].Item_name: + t.Errorf("Expected Item_name %v but got %v", inventories[0].Item_name, receivedInventory.Item_name) + case receivedInventory.Category != inventories[0].Category: + t.Errorf("Expected Category %v but got %v", inventories[0].Category, receivedInventory.Category) + case receivedInventory.Description != inventories[0].Description: + t.Errorf("Expected Description %v but got %v", inventories[0].Description, receivedInventory.Description) + case receivedInventory.Weight != inventories[0].Weight: + t.Errorf("Expected Weight %v but got %v", inventories[0].Weight, receivedInventory.Weight) + case receivedInventory.Weight_unit != inventories[0].Weight_unit: + t.Errorf("Expected Weight_unit %v but got %v", inventories[0].Weight_unit, receivedInventory.Weight_unit) + case receivedInventory.Url != inventories[0].Url: + t.Errorf("Expected Url %v but got %v", inventories[0].Url, receivedInventory.Url) + case receivedInventory.Price != inventories[0].Price: + t.Errorf("Expected Price %v but got %v", inventories[0].Price, receivedInventory.Price) + case receivedInventory.Currency != inventories[0].Currency: + t.Errorf("Expected Currency %v but got %v", inventories[0].Currency, receivedInventory.Currency) + } + }) + t.Run("Item Forbiden", func(t *testing.T) { + token, err := security.GenerateToken(users[1].ID) + if err != nil { + t.Fatalf("Failed to generate token: %v", err) + } + // Create a mock HTTP request to the /myinventory endpoint + path := fmt.Sprintf("/myinventory/%d", inventories[0].ID) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Set up a test scenario: sending a GET request + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Serve the HTTP request to the Gin router + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusForbidden { + t.Errorf("Expected status code %d but got %d", http.StatusForbidden, w.Code) + } + }) +} diff --git a/pkg/inventories/testdata.go b/pkg/inventories/testdata.go new file mode 100644 index 0000000..dd815b2 --- /dev/null +++ b/pkg/inventories/testdata.go @@ -0,0 +1,126 @@ +package inventories + +import ( + "fmt" + "time" + + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/helper" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gruntwork-io/terratest/modules/random" +) + +var users = []dataset.User{ + { + Username: fmt.Sprintf("user-%s", random.UniqueId()), + Email: fmt.Sprintf("user-%s@exemple.com", random.UniqueId()), + Firstname: "Joseph", + Lastname: "Doe", + Role: "standard", + Status: "active", + Password: "password", + LastPassword: "password", + }, + { + Username: fmt.Sprintf("user-%s", random.UniqueId()), + Email: fmt.Sprintf("user-%s@exemple.com", random.UniqueId()), + Firstname: "Syvie", + Lastname: "Doe", + Role: "standard", + Status: "active", + Password: "password", + LastPassword: "password", + }, +} + +var inventories = dataset.Inventories{ + { + User_id: 1, + Item_name: "Backpack", + Category: "Outdoor Gear", + Weight: 950, + Weight_unit: "METRIC", + Url: "https://example.com/backpack", + Price: 50, + Currency: "USD", + }, + { + User_id: 1, + Item_name: "Tent", + Category: "Shelter", + Weight: 1200, + Weight_unit: "METRIC", + Url: "https://example.com/tent", + Price: 150, + Currency: "USD", + }, + { + User_id: 1, + Item_name: "Sleeping Bag", + Category: "Sleeping", + Weight: 800, + Weight_unit: "METRIC", + Url: "https://example.com/sleeping-bag", + Price: 120, + Currency: "EUR", + }, + { + User_id: 2, + Item_name: "Sleeping Bag", + Category: "Sleeping", + Weight: 800, + Weight_unit: "METRIC", + Url: "https://example.com/sleeping-bag", + Price: 120, + Currency: "EUR", + }, +} + +func loadingInventoryDataset() error { + + // Load accounts datasetx + for i := range users { + var id uint + err := database.Db().QueryRow("INSERT INTO account (username, email, firstname, lastname, role, status, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id;", + users[i].Username, users[i].Email, users[i].Firstname, users[i].Lastname, users[i].Role, users[i].Status, time.Now().Truncate(time.Second), time.Now().Truncate(time.Second)).Scan(&users[i].ID) + if err != nil { + return err + } + + hashedPassword, err := security.HashPassword(users[i].Password) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + hashedLastPassword, err := security.HashPassword(users[i].LastPassword) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + err = database.Db().QueryRow("INSERT INTO password (user_id, password, last_password, updated_at) VALUES ($1,$2,$3,$4) RETURNING id;", users[i].ID, hashedPassword, hashedLastPassword, time.Now().Truncate(time.Second)).Scan(&id) + if err != nil { + return err + } + } + + // Transform inventories dataset by using the real user_id + for i := range inventories { + switch inventories[i].User_id { + case 1: + inventories[i].User_id = helper.FinUserIDByUsername(users, users[0].Username) + case 2: + inventories[i].User_id = helper.FinUserIDByUsername(users, users[0].Username) + } + } + + // Insert inventories dataset + for i := range inventories { + err := database.Db().QueryRow("INSERT INTO inventory (user_id, item_name, category, description, weight, weight_unit, url, price, currency, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id;", + inventories[i].User_id, inventories[i].Item_name, inventories[i].Category, inventories[i].Description, inventories[i].Weight, inventories[i].Weight_unit, inventories[i].Url, inventories[i].Price, inventories[i].Currency, time.Now().Truncate(time.Second), time.Now().Truncate(time.Second)).Scan(&inventories[i].ID) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/packs/packs.go b/pkg/packs/packs.go new file mode 100644 index 0000000..9f99bbf --- /dev/null +++ b/pkg/packs/packs.go @@ -0,0 +1,1189 @@ +package packs + +import ( + "database/sql" + "encoding/csv" + "errors" + "io" + "net/http" + "strconv" + "time" + + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/helper" + "github.com/Angak0k/pimpmypack/pkg/inventories" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gin-gonic/gin" +) + +// Get all packs +// @Summary [ADMIN] Get all packs +// @Description Get all packs - for admin use only +// @Security Bearer +// @Tags Packs +// @Produce json +// @Success 200 {object} dataset.Packs +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packs [get] +func GetPacks(c *gin.Context) { + + packs, err := returnPacks() + + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if len(packs) != 0 { + c.IndentedJSON(http.StatusOK, packs) + } else { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "No packs founded"}) + } +} + +func returnPacks() (dataset.Packs, error) { + var packs dataset.Packs + + rows, err := database.Db().Query("SELECT id, user_id, pack_name, pack_description, created_at, updated_at FROM pack;") + if err != nil { + return nil, err + } + defer rows.Close() + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + for rows.Next() { + var pack dataset.Pack + err := rows.Scan(&pack.ID, &pack.User_id, &pack.Pack_name, &pack.Pack_description, &pack.Created_at, &pack.Updated_at) + if err != nil { + return nil, err + } + packs = append(packs, pack) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return packs, nil + +} + +// Get pack by ID +// @Summary [ADMIN] Get pack by ID +// @Description Get pack by ID - for admin use only +// @Security Bearer +// @Tags Packs +// @Produce json +// @Param id path int true "Pack ID" +// @Success 200 {object} dataset.Pack +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 404 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packs/{id} [get] +func GetPackByID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + pack, err := findPackById(id) + + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if pack == nil { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "Pack not found"}) + return + } + + c.IndentedJSON(http.StatusOK, *pack) + +} + +// Get pack by ID +// @Summary Get pack by ID +// @Description Get pack by ID +// @Security Bearer +// @Tags Packs +// @Produce json +// @Param id path int true "Pack ID" +// @Success 200 {object} dataset.Pack +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 404 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypack/{id} [get] +func GetMyPackByID(c *gin.Context) { + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + myPack, err := checkPackOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myPack { + pack, err := findPackById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if pack != nil { + c.IndentedJSON(http.StatusOK, *pack) + } else { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "Pack not found"}) + return + } + } else { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "This pack does not belong to you"}) + return + } +} + +func findPackById(id uint) (*dataset.Pack, error) { + var pack dataset.Pack + + row := database.Db().QueryRow("SELECT id, user_id, pack_name, pack_description, created_at, updated_at FROM pack WHERE id = $1;", id) + err := row.Scan(&pack.ID, &pack.User_id, &pack.Pack_name, &pack.Pack_description, &pack.Created_at, &pack.Updated_at) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + return &pack, nil +} + +// Create a new pack +// @Summary [ADMIN] Create a new pack +// @Description Create a new pack - for admin use only +// @Security Bearer +// @Tags Packs +// @Accept json +// @Produce json +// @Param pack body dataset.Pack true "Pack" +// @Success 201 {object} dataset.Pack +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packs [post] +func PostPack(c *gin.Context) { + var newPack dataset.Pack + + if err := c.BindJSON(&newPack); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + err := insertPack(&newPack) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusCreated, newPack) + +} + +// Create a new pack +// @Summary Create a new pack +// @Description Create a new pack +// @Security Bearer +// @Tags Packs +// @Accept json +// @Produce json +// @Param pack body dataset.Pack true "Pack" +// @Success 201 {object} dataset.Pack +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypack [post] +func PostMyPack(c *gin.Context) { + var newPack dataset.Pack + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := c.BindJSON(&newPack); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + newPack.User_id = user_id + + err = insertPack(&newPack) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusCreated, newPack) + +} + +func insertPack(p *dataset.Pack) error { + if p == nil { + return errors.New("payload is empty") + } + p.Created_at = time.Now().Truncate(time.Second) + p.Updated_at = time.Now().Truncate(time.Second) + + err := database.Db().QueryRow("INSERT INTO pack (user_id, pack_name, pack_description, created_at, updated_at) VALUES ($1,$2,$3,$4,$5) RETURNING id;", p.User_id, p.Pack_name, p.Pack_description, p.Created_at, p.Updated_at).Scan(&p.ID) + if err != nil { + return err + } + + return nil + +} + +// Update a pack by ID +// @Summary [ADMIN] Update a pack by ID +// @Description Update a pack by ID - for admin use only +// @Security Bearer +// @Tags Packs +// @Accept json +// @Produce json +// @Param id path int true "Pack ID" +// @Param pack body dataset.Pack true "Pack" +// @Success 200 {object} dataset.Pack +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packs/{id} [put] +func PutPackByID(c *gin.Context) { + var updatedPack dataset.Pack + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + if err := c.BindJSON(&updatedPack); err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid Body format"}) + return + } + + err = updatePackById(id, &updatedPack) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, updatedPack) + +} + +// Update a pack by ID +// @Summary Update a pack by ID +// @Description Update a pack by ID +// @Security Bearer +// @Tags Packs +// @Accept json +// @Produce json +// @Param id path int true "Pack ID" +// @Param pack body dataset.Pack true "Pack" +// @Success 200 {object} dataset.Pack +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypack/{id} [put] +func PutMyPackByID(c *gin.Context) { + var updatedPack dataset.Pack + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + if err := c.BindJSON(&updatedPack); err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid Json body"}) + return + } + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + myPack, err := checkPackOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myPack { + updatedPack.User_id = user_id + err = updatePackById(id, &updatedPack) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, updatedPack) + } else { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "This pack does not belong to you"}) + return + } +} + +func updatePackById(id uint, p *dataset.Pack) error { + if p == nil { + return errors.New("payload is empty") + } + p.Updated_at = time.Now().Truncate(time.Second) + + statement, err := database.Db().Prepare("UPDATE pack SET user_id=$1, pack_name=$2, pack_description=$3, updated_at=$4 WHERE id=$5;") + if err != nil { + return err + } + _, err = statement.Exec(p.User_id, p.Pack_name, p.Pack_description, p.Updated_at, id) + if err != nil { + return err + } + + return nil + +} + +// Delete a pack by ID +// @Summary [ADMIN] Delete a pack by ID +// @Description Delete a pack by ID - for admin use only +// @Security Bearer +// @Tags Packs +// @Produce json +// @Param id path int true "Pack ID" +// @Success 200 {object} map[string]string "message" +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packs/{id} [delete] + +func DeletePackByID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + err = deletePackById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, gin.H{"message": "Pack deleted"}) + +} + +// Delete a pack by ID +// @Summary Delete a pack by ID +// @Description Delete a pack by ID +// @Security Bearer +// @Tags Packs +// @Produce json +// @Param id path int true "Pack ID" +// @Success 200 {object} map[string]string "message" +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypack/{id} [delete] +func DeleteMyPackByID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + myPack, err := checkPackOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myPack { + err := deletePackById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, gin.H{"message": "Pack deleted"}) + } else { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "This pack does not belong to you"}) + return + } +} + +func deletePackById(id uint) error { + statement, err := database.Db().Prepare("DELETE FROM pack WHERE id=$1;") + if err != nil { + return err + } + _, err = statement.Exec(id) + if err != nil { + return err + } + + return nil + +} + +// Get all pack contents +// @Summary [ADMIN] Get all pack contents +// @Description Get all pack contents - for admin use only +// @Security Bearer +// @Tags Packs +// @Produce json +// @Success 200 {object} dataset.PackContents +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packcontents [get] +func GetPackContents(c *gin.Context) { + + packContents, err := returnPackContents() + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, packContents) + +} + +func returnPackContents() (*dataset.PackContents, error) { + var packContents dataset.PackContents + + rows, err := database.Db().Query("SELECT id, pack_id, item_id, quantity, worn, consumable, created_at, updated_at FROM pack_content;") + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var packContent dataset.PackContent + err := rows.Scan(&packContent.ID, &packContent.Pack_id, &packContent.Item_id, &packContent.Quantity, &packContent.Worn, &packContent.Consumable, &packContent.Created_at, &packContent.Updated_at) + if err != nil { + return nil, err + } + packContents = append(packContents, packContent) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return &packContents, nil +} + +// Get pack content by ID +// @Summary [ADMIN] Get pack content by ID +// @Description Get pack content by ID - for admin use only +// @Security Bearer +// @Tags Packs +// @Produce json +// @Param id path int true "Pack Content ID" +// @Success 200 {object} dataset.PackContent +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 404 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packcontents/{id} [get] +func GetPackContentByID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + packcontent, err := findPackContentById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if packcontent == nil { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "Pack Item not found"}) + return + } + + c.IndentedJSON(http.StatusOK, *packcontent) + +} + +func findPackContentById(id uint) (*dataset.PackContent, error) { + var packcontent dataset.PackContent + + row := database.Db().QueryRow("SELECT id, pack_id, item_id, quantity, worn, consumable, created_at, updated_at FROM pack_content WHERE id = $1;", id) + err := row.Scan(&packcontent.ID, &packcontent.Pack_id, &packcontent.Item_id, &packcontent.Quantity, &packcontent.Worn, &packcontent.Consumable, &packcontent.Created_at, &packcontent.Updated_at) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + return &packcontent, nil +} + +// Create a new pack content +// @Summary [ADMIN] Create a new pack content +// @Description Create a new pack content - for admin use only +// @Security Bearer +// @Tags Packs +// @Accept json +// @Produce json +// @Param packcontent body dataset.PackContent true "Pack Content" +// @Success 201 {object} dataset.PackContent +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packcontents [post] +func PostPackContent(c *gin.Context) { + var newPackContent dataset.PackContent + + if err := c.BindJSON(&newPackContent); err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid Body format"}) + return + } + + err := insertPackContent(&newPackContent) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusCreated, newPackContent) + +} + +// Create a new pack content +// @Summary Create a new pack content +// @Description Create a new pack content +// @Security Bearer +// @Tags Packs +// @Accept json +// @Produce json +// @Param packcontent body dataset.PackContent true "Pack Content" +// @Success 201 {object} dataset.PackContent +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypackcontent [post] +func PostMyPackContent(c *gin.Context) { + var newPackContent dataset.PackContent + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := c.BindJSON(&newPackContent); err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid Body format"}) + return + } + + myPack, err := checkPackOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myPack { + newPackContent.Pack_id = id + err := insertPackContent(&newPackContent) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.IndentedJSON(http.StatusCreated, newPackContent) + } else { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "This pack does not belong to you"}) + return + } +} + +func insertPackContent(pc *dataset.PackContent) error { + if pc == nil { + return errors.New("payload is empty") + } + pc.Created_at = time.Now().Truncate(time.Second) + pc.Updated_at = time.Now().Truncate(time.Second) + + err := database.Db().QueryRow("INSERT INTO pack_content (pack_id, item_id, quantity, worn, consumable, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id;", pc.Pack_id, pc.Item_id, pc.Quantity, pc.Worn, pc.Consumable, pc.Created_at, pc.Updated_at).Scan(&pc.ID) + + if err != nil { + return err + } + return nil +} + +// Update a pack content by ID +// @Summary [ADMIN] Update a pack content by ID +// @Description Update a pack content by ID - for admin use only +// @Security Bearer +// @Tags Packs +// @Accept json +// @Produce json +// @Param id path int true "Pack Content ID" +// @Param packcontent body dataset.PackContent true "Pack Content" +// @Success 200 {object} dataset.PackContent +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packcontents/{id} [put] +func PutPackContentByID(c *gin.Context) { + + var updatedPackContent dataset.PackContent + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + if err := c.BindJSON(&updatedPackContent); err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid Body format"}) + return + } + + err = updatePackContentByID(id, &updatedPackContent) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, updatedPackContent) + +} + +func PutMyPackContentByID(c *gin.Context) { + + var updatedPackContent dataset.PackContent + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + item_id, err := helper.StringToUint(c.Param("item_id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := c.BindJSON(&updatedPackContent); err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid Body format"}) + return + } + + myPack, err := checkPackOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myPack { + updatedPackContent.Pack_id = id + err := updatePackContentByID(item_id, &updatedPackContent) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, updatedPackContent) + } else { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "This pack does not belong to you"}) + return + } +} + +func updatePackContentByID(id uint, pc *dataset.PackContent) error { + if pc == nil { + return errors.New("payload is empty") + } + pc.Updated_at = time.Now().Truncate(time.Second) + + statement, err := database.Db().Prepare("UPDATE pack_content SET pack_id=$1, item_id=$2, quantity=$3, worn=$4, consumable=$5, updated_at=$6 WHERE id=$7;") + if err != nil { + return err + } + _, err = statement.Exec(pc.Pack_id, pc.Item_id, pc.Quantity, pc.Worn, pc.Consumable, pc.Updated_at, id) + if err != nil { + return err + } + return nil +} + +// Delete a pack content by ID +// @Summary [ADMIN] Delete a pack content by ID +// @Description Delete a pack content by ID - for admin use only +// @Security Bearer +// @Tags Packs +// @Produce json +// @Param id path int true "Pack Content ID" +// @Success 200 {object} map[string]string "message" +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packcontents/{id} [delete] +func DeletePackContentByID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + err = deletePackContentById(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, gin.H{"message": "Pack Item deleted"}) + +} + +// Delete a pack content by ID +// @Summary Delete a pack content by ID +// @Description Delete a pack content by ID +// @Security Bearer +// @Tags Packs +// @Produce json +// @Param id path int true "Pack Content ID" +// @Success 200 {object} map[string]string "message" +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypackcontent/{id} [delete] +func DeleteMyPackContentByID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + item_id, err := helper.StringToUint(c.Param("item_id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + myPack, err := checkPackOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myPack { + err := deletePackContentById(item_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, gin.H{"message": "Pack Item deleted"}) + } else { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "This pack does not belong to you"}) + return + } +} + +func deletePackContentById(id uint) error { + statement, err := database.Db().Prepare("DELETE FROM pack_content WHERE id=$1;") + if err != nil { + return err + } + _, err = statement.Exec(id) + if err != nil { + return err + } + return nil +} + +// Get all pack contents +// @Summary [ADMIN] Get all pack contents +// @Description Get all pack contents - for admin use only +// @Security Bearer +// @Tags Packs +// @Produce json +// @Success 200 {object} dataset.PackContents +// @Failure 500 {object} map[string]interface{} "error" +// @Router /packs/:id/packcontents [get] +func GetPackContentsByPackID(c *gin.Context) { + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + packContents, err := returnPackContentsByPackID(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if packContents == nil { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "Pack not found"}) + return + } + + c.IndentedJSON(http.StatusOK, packContents) + +} + +// Get pack content by ID +// @Summary Get pack content by ID +// @Description Get pack content by ID +// @Security Bearer +// @Tags Packs +// @Produce json +// @Param id path int true "Pack Content ID" +// @Success 200 {object} dataset.PackContent +// @Failure 400 {object} map[string]interface{} "error" +// @Failure 404 {object} map[string]interface{} "error" +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypackcontent/{id} [get] +func GetMyPackContentsByPackID(c *gin.Context) { + var packContents *dataset.PackContentWithItems + + id, err := helper.StringToUint(c.Param("id")) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + myPack, err := checkPackOwnership(id, user_id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if myPack { + packContents, err = returnPackContentsByPackID(id) + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if packContents == nil { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "Pack not found"}) + return + } + c.IndentedJSON(http.StatusOK, packContents) + } else { + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "This pack does not belong to you"}) + return + } +} + +func returnPackContentsByPackID(id uint) (*dataset.PackContentWithItems, error) { + var packWithItems dataset.PackContentWithItems + + rows, err := database.Db().Query("SELECT pc.id AS pack_content_id, pc.pack_id as pack_id, i.id AS inventory_id, i.item_name, i.category, i.description AS item_description, i.weight, i.weight_unit, i.url AS item_url, i.price, i.currency, pc.quantity, pc.worn, pc.consumable FROM pack_content pc JOIN inventory i ON pc.item_id = i.id WHERE pc.pack_id = $1;", id) + + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var item dataset.PackContentWithItem + err := rows.Scan(&item.Pack_content_id, &item.Pack_id, &item.Inventory_id, &item.Item_name, &item.Category, &item.Item_description, &item.Weight, &item.Weight_unit, &item.Item_url, &item.Price, &item.Currency, &item.Quantity, &item.Worn, &item.Consumable) + if err != nil { + return nil, err + } + packWithItems = append(packWithItems, item) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + if len(packWithItems) == 0 { + return nil, nil + } + return &packWithItems, nil +} + +// Get all packs +// @Summary [ADMIN] Get all packs +// @Description Get all packs - for admin use only +// @Security Bearer +// @Tags Packs +// @Produce json +// @Success 200 {object} []dataset.Pack +// @Failure 500 {object} map[string]interface{} "error" +// @Router /mypacks [get] +func GetMyPacks(c *gin.Context) { + user_id, err := security.ExtractTokenID(c) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + packs, err := findPacksByUserId(user_id) + + if err != nil { + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if packs == nil { + c.IndentedJSON(http.StatusNotFound, gin.H{"error": "No pack found"}) + return + } + + c.IndentedJSON(http.StatusOK, *packs) + +} + +func findPacksByUserId(id uint) (*dataset.Packs, error) { + var packs dataset.Packs + + rows, err := database.Db().Query("SELECT id, user_id, pack_name, pack_description, created_at, updated_at FROM pack WHERE user_id = $1;", id) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var pack dataset.Pack + err := rows.Scan(&pack.ID, &pack.User_id, &pack.Pack_name, &pack.Pack_description, &pack.Created_at, &pack.Updated_at) + if err != nil { + return nil, err + } + packs = append(packs, pack) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + if len(packs) == 0 { + return nil, nil + } + + return &packs, nil +} + +func checkPackOwnership(id uint, user_id uint) (bool, error) { + var rows int + + row := database.Db().QueryRow("SELECT COUNT(id) FROM pack WHERE id = $1 AND user_id = $2;", id, user_id) + err := row.Scan(&rows) + if err != nil { + return false, err + } + + if rows == 0 { + return false, nil + } else { + return true, nil + } +} + +// Import from lighterpack +// @Summary Import from lighterpack csv pack file +// @Description Import from lighterpack csv pack file +// @Security Bearer +// @Tags Packs +// @Accept multipart/form-data +// @Produce json +// @Param file formData file true "CSV file" +// @Success 200 {object} map[string]string "message" +// @Failure 400 {object} map[string]interface{} "error" +// @Router /mypack/import [post] +func ImportFromLighterPack(c *gin.Context) { + var lighterPack dataset.LighterPack + + user_id, err := security.ExtractTokenID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + file, _, err := c.Request.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + defer file.Close() + + // Parse the CSV file + reader := csv.NewReader(file) + reader.Comma = ',' + + // Read and discard the first line (header) after checking it + record, err := reader.Read() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read the CSV header"}) + return + } + if len(record) < 10 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid CSV format - wrong number of columns"}) + return + } + + // Iterate through CSV records and process them + for { + var lighterPackItem dataset.LighterPackItem + record, err := reader.Read() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Assuming the CSV columns order is: Item Name,Category,desc,qty,weight,unit,url,price,worn,consumable + if len(record) < 10 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid CSV format - wrong number of columns"}) + return + } + + lighterPackItem.Item_name = record[0] + lighterPackItem.Category = record[1] + lighterPackItem.Desc = record[2] + lighterPackItem.Unit = record[5] + lighterPackItem.Url = record[6] + + qty, err := strconv.Atoi(record[3]) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid CSV format - failed to convert quantity to number"}) + return + } else { + lighterPackItem.Qty = qty + } + + weight, err := strconv.Atoi(record[4]) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid CSV format - failed to convert weight to number"}) + return + } else { + lighterPackItem.Weight = weight + } + + price, err := strconv.Atoi(record[7]) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid CSV format - failed to convert price to number"}) + return + } else { + lighterPackItem.Price = price + } + + if record[8] == "worn" { + lighterPackItem.Worn = true + } + + if record[9] == "consumable" { + lighterPackItem.Consumable = true + } + + lighterPack = append(lighterPack, lighterPackItem) + } + + // Perform your database insertion + err = insertLighterPack(&lighterPack, user_id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "CSV data imported successfully"}) +} + +func insertLighterPack(lp *dataset.LighterPack, user_id uint) error { + if lp == nil { + return errors.New("payload is empty") + } + + // Create new pack + var newPack dataset.Pack + newPack.User_id = user_id + newPack.Pack_name = "LighterPack Import" + newPack.Pack_description = "LighterPack Import" + err := insertPack(&newPack) + if err != nil { + return err + } + + // Insert content in new pack with insertPackContent + for _, item := range *lp { + var i dataset.Inventory + i.User_id = user_id + i.Item_name = item.Item_name + i.Category = item.Category + i.Description = item.Desc + i.Weight = item.Weight + i.Weight_unit = helper.ConvertWeightUnit(item.Unit) + i.Url = item.Url + i.Price = item.Price + i.Currency = "USD" + err := inventories.InsertInventory(&i) + if err != nil { + return err + } + var pc dataset.PackContent + pc.Pack_id = newPack.ID + pc.Item_id = i.ID + pc.Quantity = item.Qty + pc.Worn = item.Worn + pc.Consumable = item.Consumable + err = insertPackContent(&pc) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/packs/packs_test.go b/pkg/packs/packs_test.go new file mode 100644 index 0000000..2e3ec42 --- /dev/null +++ b/pkg/packs/packs_test.go @@ -0,0 +1,841 @@ +package packs + +import ( + "bytes" + "database/sql" + "encoding/json" + "errors" + "fmt" + "log" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/Angak0k/pimpmypack/pkg/config" + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gin-gonic/gin" + "github.com/google/go-cmp/cmp" +) + +func TestMain(m *testing.M) { + + // init env + err := config.EnvInit("../../.env") + if err != nil { + log.Fatalf("Error loading .env file or environement variable : %v", err) + } + println("Environement variables loaded") + + // init DB + err = database.DatabaseInit() + if err != nil { + log.Fatalf("Error connecting database : %v", err) + } + println("Database connected") + + // init DB migration + err = database.DatabaseMigrate() + if err != nil { + log.Fatalf("Error migrating database : %v", err) + } + println("Database migrated") + + // init dataset + println("Loading dataset...") + err = loadingPackDataset() + if err != nil { + log.Fatalf("Error loading dataset : %v", err) + } + println("Dataset loaded...") + ret := m.Run() + os.Exit(ret) +} + +func TestGetPacks(t *testing.T) { + var getPacks dataset.Packs + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetPacks handler + router.GET("/packs", GetPacks) + + t.Run("Pack List Retrieved", func(t *testing.T) { + // Create a mock HTTP request to the /packs endpoint + req, err := http.NewRequest("GET", "/packs", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Serve the HTTP request to the Gin router + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Check the Content-Type header + expectedContentType := "application/json; charset=utf-8" + contentType := w.Header().Get("Content-Type") + if contentType != expectedContentType { + t.Errorf("Expected content type %s but got %s", expectedContentType, contentType) + } + + // Unmarshal the response body into a slice of packs struct + if err := json.Unmarshal(w.Body.Bytes(), &getPacks); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + // determine if the pack - and only the expected pack - is in the database + if len(getPacks) < 3 { + t.Errorf("Expected almost 3 pack but got %d", len(getPacks)) + } else { + switch { + case !cmp.Equal(getPacks[0].User_id, packs[0].User_id): + t.Errorf("Expected User ID %v but got %v", packs[0].User_id, getPacks[0].User_id) + case !cmp.Equal(getPacks[0].Pack_name, packs[0].Pack_name): + t.Errorf("Expected Pack Name %v but got %v", packs[0].Pack_name, getPacks[0].Pack_name) + case !cmp.Equal(getPacks[0].Pack_description, packs[0].Pack_description): + t.Errorf("Expected Pack Description %v but got %v", packs[0].Pack_description, getPacks[0].Pack_description) + case !cmp.Equal(getPacks[1].User_id, packs[1].User_id): + t.Errorf("Expected User ID %v but got %v", packs[1].User_id, getPacks[1].User_id) + case !cmp.Equal(getPacks[1].Pack_name, packs[1].Pack_name): + t.Errorf("Expected Pack Name %v but got %v", packs[1].Pack_name, getPacks[1].Pack_name) + case !cmp.Equal(getPacks[1].Pack_description, packs[1].Pack_description): + t.Errorf("Expected Pack Description %v but got %v", packs[1].Pack_description, getPacks[1].Pack_description) + case !cmp.Equal(getPacks[2].User_id, packs[2].User_id): + t.Errorf("Expected User ID %v but got %v", packs[2].User_id, getPacks[2].User_id) + case !cmp.Equal(getPacks[2].Pack_name, packs[2].Pack_name): + t.Errorf("Expected Pack Name %v but got %v", packs[2].Pack_name, getPacks[2].Pack_name) + case !cmp.Equal(getPacks[2].Pack_description, packs[2].Pack_description): + t.Errorf("Expected Pack Description %v but got %v", packs[2].Pack_description, getPacks[2].Pack_description) + } + } + }) +} + +func TestGetPackByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetPackByID handler + router.GET("/packs/:id", GetPackByID) + + // Set up a test scenario: pack found + t.Run("Pack Found", func(t *testing.T) { + path := fmt.Sprintf("/packs/%d", packs[0].ID) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Unmarshal the response body into a pack struct + var receivedPack dataset.Pack + if err := json.Unmarshal(w.Body.Bytes(), &receivedPack); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received pack with the expected pack + switch { + case receivedPack.User_id != packs[0].User_id: + t.Errorf("Expected User ID %v but got %v", packs[0].User_id, receivedPack.User_id) + case receivedPack.Pack_name != packs[0].Pack_name: + t.Errorf("Expected Pack Name %v but got %v", packs[0].Pack_name, receivedPack.Pack_name) + case receivedPack.Pack_description != packs[0].Pack_description: + t.Errorf("Expected Pack Description %v but got %v", packs[0].Pack_description, receivedPack.Pack_description) + } + }) + + // Set up a test scenario: pack not found + t.Run("Pack Not Found", func(t *testing.T) { + req, err := http.NewRequest("GET", "/packs/1000", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("Expected status code %d but got %d", http.StatusNotFound, w.Code) + } + }) +} + +func TestPostPack(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PostPacks handler + router.POST("/packs", PostPack) + + // Sample pack data + newPack := dataset.Pack{ + User_id: users[0].ID, + Pack_name: "SomePack", + Pack_description: "This is a new pack", + } + + // Convert pack data to JSON + jsonData, err := json.Marshal(newPack) + if err != nil { + t.Fatalf("Failed to marshal pack data: %v", err) + } + + t.Run("Insert pack", func(t *testing.T) { + + // Set up a test scenario: sending a POST request with JSON data + req, err := http.NewRequest("POST", "/packs", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusCreated { + t.Errorf("Expected status code %d but got %d", http.StatusCreated, w.Code) + } + + // Query the database to get the inserted pack + var insertedPack dataset.Pack + row := database.Db().QueryRow("SELECT * FROM pack WHERE pack_name = $1;", newPack.Pack_name) + err = row.Scan(&insertedPack.ID, &insertedPack.User_id, &insertedPack.Pack_name, &insertedPack.Pack_description, &insertedPack.Created_at, &insertedPack.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("No rows were returned!") + } + t.Fatalf("Failed to run request: %v", err) + } + + // Unmarshal the response body into a pack struct + var receivedPack dataset.Pack + if err := json.Unmarshal(w.Body.Bytes(), &receivedPack); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received pack with the expected pack data + switch { + case receivedPack.User_id != insertedPack.User_id: + t.Errorf("Expected User ID %v but got %v", insertedPack.User_id, receivedPack.User_id) + case receivedPack.Pack_name != insertedPack.Pack_name: + t.Errorf("Expected Pack Name %v but got %v", insertedPack.Pack_name, receivedPack.Pack_name) + case receivedPack.Pack_description != insertedPack.Pack_description: + t.Errorf("Expected Pack Description %v but got %v", insertedPack.Pack_description, receivedPack.Pack_description) + } + }) +} + +func TestPutPackByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PutPacks handler + router.PUT("/packs/:id", PutPackByID) + + // Sample pack data + TestUpdatedPack := dataset.Pack{ + User_id: users[1].ID, + Pack_name: "Amazing Pack", + Pack_description: "Updated pack description", + } + + // Convert pack data to JSON + jsonData, err := json.Marshal(TestUpdatedPack) + if err != nil { + t.Fatalf("Failed to marshal pack data: %v", err) + } + + t.Run("Update pack", func(t *testing.T) { + + // Set up a test scenario: sending a PUT request with JSON data + path := fmt.Sprintf("/packs/%d", packs[2].ID) + req, err := http.NewRequest("PUT", path, bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Query the database to get the updated pack + var updatedPack dataset.Pack + row := database.Db().QueryRow("SELECT * FROM pack WHERE id = $1;", packs[2].ID) + err = row.Scan(&updatedPack.ID, &updatedPack.User_id, &updatedPack.Pack_name, &updatedPack.Pack_description, &updatedPack.Created_at, &updatedPack.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("No rows were returned!") + } + t.Fatalf("Failed to run request: %v", err) + } + + // Compare the data in DB with Test dataset + switch { + case updatedPack.User_id != TestUpdatedPack.User_id: + t.Errorf("Expected User ID %v but got %v", TestUpdatedPack.User_id, updatedPack.User_id) + case updatedPack.Pack_name != TestUpdatedPack.Pack_name: + t.Errorf("Expected Pack Name %v but got %v", TestUpdatedPack.Pack_name, updatedPack.Pack_name) + case updatedPack.Pack_description != TestUpdatedPack.Pack_description: + t.Errorf("Expected Pack Description %v but got %v", TestUpdatedPack.Pack_description, updatedPack.Pack_description) + } + }) +} + +func TestDeletePackByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for DeletePackByID handler + router.DELETE("/packs/:id", DeletePackByID) + + t.Run("Delete pack", func(t *testing.T) { + + // Set up a test scenario: sending a DELETE request + path := fmt.Sprintf("/packs/%d", packs[2].ID) + req, err := http.NewRequest("DELETE", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Check in the database if the pack has been deleted + var packName string + row := database.Db().QueryRow("SELECT pack_name FROM pack WHERE id = $1;", packs[2].ID) + err = row.Scan(&packName) + if err == nil { + t.Errorf("Pack ID 3 associated to pack name %s should be deleted and it is still in DB", packName) + } else if !errors.Is(err, sql.ErrNoRows) { + t.Fatalf("Failed to create request: %v", err) + } + }) +} + +func TestGetPackContents(t *testing.T) { + var packContents dataset.PackContents + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetPackContents handler + router.GET("/packcontents", GetPackContents) + + t.Run("PackContent List Retrieved", func(t *testing.T) { + // Create a mock HTTP request to the /packs endpoint + req, err := http.NewRequest("GET", "/packcontents", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Serve the HTTP request to the Gin router + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Check the Content-Type header + expectedContentType := "application/json; charset=utf-8" + contentType := w.Header().Get("Content-Type") + if contentType != expectedContentType { + t.Errorf("Expected content type %s but got %s", expectedContentType, contentType) + } + + // Unmarshal the response body into a slice of PackContent struct + if err := json.Unmarshal(w.Body.Bytes(), &packContents); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + // determine if the packcontent - and only the expected packcontent - is in the database + if len(packContents) < 4 { + t.Errorf("Expected almost 4 packcontent but got %d", len(packContents)) + } else { + switch { + case !cmp.Equal(packContents[0].Pack_id, packItems[0].Pack_id): + t.Errorf("Expected Pack ID %v but got %v", packItems[0].Pack_id, packContents[0].Pack_id) + case !cmp.Equal(packContents[0].Item_id, packItems[0].Item_id): + t.Errorf("Expected Item ID %v but got %v", packItems[0].Item_id, packContents[0].Item_id) + case !cmp.Equal(packContents[0].Quantity, packItems[0].Quantity): + t.Errorf("Expected Quantity %v but got %v", packItems[0].Quantity, packContents[0].Quantity) + case !cmp.Equal(packContents[0].Worn, packItems[0].Worn): + t.Errorf("Expected Worn %v but got %v", packItems[0].Worn, packContents[0].Worn) + case !cmp.Equal(packContents[0].Consumable, packItems[0].Consumable): + t.Errorf("Expected Consumable %v but got %v", packItems[0].Consumable, packContents[0].Consumable) + case !cmp.Equal(packContents[1].Pack_id, packItems[1].Pack_id): + t.Errorf("Expected Pack ID %v but got %v", packItems[1].Pack_id, packContents[1].Pack_id) + case !cmp.Equal(packContents[1].Item_id, packItems[1].Item_id): + t.Errorf("Expected Item ID %v but got %v", packItems[1].Item_id, packContents[1].Item_id) + case !cmp.Equal(packContents[1].Quantity, packItems[1].Quantity): + t.Errorf("Expected Quantity %v but got %v", packItems[1].Quantity, packContents[1].Quantity) + case !cmp.Equal(packContents[1].Worn, packItems[1].Worn): + t.Errorf("Expected Worn %v but got %v", packItems[1].Worn, packContents[1].Worn) + case !cmp.Equal(packContents[1].Consumable, packItems[1].Consumable): + t.Errorf("Expected Consumable %v but got %v", packItems[1].Consumable, packContents[1].Consumable) + case !cmp.Equal(packContents[2].Pack_id, packItems[2].Pack_id): + t.Errorf("Expected Pack ID %v but got %v", packItems[2].Pack_id, packContents[2].Pack_id) + case !cmp.Equal(packContents[2].Item_id, packItems[2].Item_id): + t.Errorf("Expected Item ID %v but got %v", packItems[2].Item_id, packContents[2].Item_id) + case !cmp.Equal(packContents[2].Quantity, packItems[2].Quantity): + t.Errorf("Expected Quantity %v but got %v", packItems[2].Quantity, packContents[2].Quantity) + case !cmp.Equal(packContents[2].Worn, packItems[2].Worn): + t.Errorf("Expected Worn %v but got %v", packItems[2].Worn, packContents[2].Worn) + case !cmp.Equal(packContents[2].Consumable, packItems[2].Consumable): + t.Errorf("Expected Consumable %v but got %v", packItems[2].Consumable, packContents[2].Consumable) + } + } + }) +} + +func TestGetPackContentByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for GetPackByID handler + router.GET("/packcontents/:id", GetPackContentByID) + + // Set up a test scenario: PackContent found + t.Run("PackContent Found", func(t *testing.T) { + path := fmt.Sprintf("/packcontents/%d", packItems[0].ID) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Unmarshal the response body into a PackContent struct + var receivedPackContent dataset.PackContent + if err := json.Unmarshal(w.Body.Bytes(), &receivedPackContent); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received PackContent with the expected PackContent + switch { + case receivedPackContent.Pack_id != packItems[0].Pack_id: + t.Errorf("Expected Pack ID %v but got %v", packItems[0].Pack_id, receivedPackContent.Pack_id) + case receivedPackContent.Item_id != packItems[0].Item_id: + t.Errorf("Expected Item ID %v but got %v", packItems[0].Item_id, receivedPackContent.Item_id) + case receivedPackContent.Quantity != packItems[0].Quantity: + t.Errorf("Expected Quantity %v but got %v", packItems[0].Quantity, receivedPackContent.Quantity) + case receivedPackContent.Worn != packItems[0].Worn: + t.Errorf("Expected Worn %v but got %v", packItems[0].Worn, receivedPackContent.Worn) + case receivedPackContent.Consumable != packItems[0].Consumable: + t.Errorf("Expected Consumable %v but got %v", packItems[0].Consumable, receivedPackContent.Consumable) + } + }) + + // Set up a test scenario: PackContent not found + t.Run("PackContent Not Found", func(t *testing.T) { + req, err := http.NewRequest("GET", "/packcontents/1000", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("Expected status code %d but got %d", http.StatusNotFound, w.Code) + } + }) +} + +func TestPostPackContent(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PostPackContents handler + router.POST("/packcontents", PostPackContent) + + // Sample pack content data + newPackContent := dataset.PackContent{ + Pack_id: packs[1].ID, + Item_id: inventories_user_pack1[2].ID, + Quantity: 10, + Worn: false, + Consumable: false, + } + + // Convert pack content data to JSON + jsonData, err := json.Marshal(newPackContent) + if err != nil { + t.Fatalf("Failed to marshal pack content data: %v", err) + } + + t.Run("Insert pack content", func(t *testing.T) { + + // Set up a test scenario: sending a POST request with JSON data + req, err := http.NewRequest("POST", "/packcontents", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusCreated { + t.Errorf("Expected status code %d but got %d", http.StatusCreated, w.Code) + } + + // Query the database to get the inserted pack content + var insertedPackContent dataset.PackContent + row := database.Db().QueryRow("SELECT id, pack_id, item_id, quantity, worn, consumable, created_at, updated_at FROM pack_content WHERE pack_id = $1 AND item_id = $2;", packs[1].ID, newPackContent.Item_id) + err = row.Scan(&insertedPackContent.ID, &insertedPackContent.Pack_id, &insertedPackContent.Item_id, &insertedPackContent.Quantity, &insertedPackContent.Worn, &insertedPackContent.Consumable, &insertedPackContent.Created_at, &insertedPackContent.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("No rows were returned!") + } + t.Fatalf("Failed to run request: %v", err) + } + + // Unmarshal the response body into a pack content struct + var receivedPackContent dataset.PackContent + if err := json.Unmarshal(w.Body.Bytes(), &receivedPackContent); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // Compare the received pack content with the expected pack content data + switch { + case receivedPackContent.Pack_id != insertedPackContent.Pack_id: + t.Errorf("Expected Pack ID %v but got %v", insertedPackContent.Pack_id, receivedPackContent.Pack_id) + case receivedPackContent.Item_id != insertedPackContent.Item_id: + t.Errorf("Expected Item ID %v but got %v", insertedPackContent.Item_id, receivedPackContent.Item_id) + case receivedPackContent.Quantity != insertedPackContent.Quantity: + t.Errorf("Expected Quantity %v but got %v", insertedPackContent.Quantity, receivedPackContent.Quantity) + case receivedPackContent.Worn != insertedPackContent.Worn: + t.Errorf("Expected Worn %v but got %v", insertedPackContent.Worn, receivedPackContent.Worn) + case receivedPackContent.Consumable != insertedPackContent.Consumable: + t.Errorf("Expected Consumable %v but got %v", insertedPackContent.Consumable, receivedPackContent.Consumable) + + } + }) +} + +func TestPutPackContentByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for PutPackContents handler + router.PUT("/packcontents/:id", PutPackContentByID) + + // Sample pack content data + TestUpdatedPackContent := dataset.PackContent{ + Pack_id: packs[1].ID, + Item_id: packItems[2].Item_id, + Quantity: 10, + Worn: false, + Consumable: false, + } + + // Convert pack content data to JSON + jsonData, err := json.Marshal(TestUpdatedPackContent) + if err != nil { + t.Fatalf("Failed to marshal pack content data: %v", err) + } + + t.Run("Update pack content", func(t *testing.T) { + + // Set up a test scenario: sending a PUT request with JSON data + path := fmt.Sprintf("/packcontents/%d", packItems[2].ID) + req, err := http.NewRequest("PUT", path, bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Query the database to get the updated pack content + var updatedPackContent dataset.PackContent + row := database.Db().QueryRow("SELECT id, pack_id, item_id, quantity, worn, consumable, created_at, updated_at FROM pack_content WHERE id = $1;", packItems[2].ID) + err = row.Scan(&updatedPackContent.ID, &updatedPackContent.Pack_id, &updatedPackContent.Item_id, &updatedPackContent.Quantity, &updatedPackContent.Worn, &updatedPackContent.Consumable, &updatedPackContent.Created_at, &updatedPackContent.Updated_at) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("No rows were returned!") + } + t.Fatalf("Failed to run request: %v", err) + } + + // Compare the data in DB with Test dataset + switch { + case updatedPackContent.Pack_id != TestUpdatedPackContent.Pack_id: + t.Errorf("Expected Pack ID %v but got %v", TestUpdatedPackContent.Pack_id, updatedPackContent.Pack_id) + case updatedPackContent.Item_id != TestUpdatedPackContent.Item_id: + t.Errorf("Expected Item ID %v but got %v", TestUpdatedPackContent.Item_id, updatedPackContent.Item_id) + case updatedPackContent.Quantity != TestUpdatedPackContent.Quantity: + t.Errorf("Expected Quantity %v but got %v", TestUpdatedPackContent.Quantity, updatedPackContent.Quantity) + case updatedPackContent.Worn != TestUpdatedPackContent.Worn: + t.Errorf("Expected Worn %v but got %v", TestUpdatedPackContent.Worn, updatedPackContent.Worn) + case updatedPackContent.Consumable != TestUpdatedPackContent.Consumable: + t.Errorf("Expected Consumable %v but got %v", TestUpdatedPackContent.Consumable, updatedPackContent.Consumable) + } + }) +} + +func TestDeletePackContentByID(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for DeletePackByID handler + router.DELETE("/packscontent/:id", DeletePackContentByID) + + t.Run("Delete pack Item", func(t *testing.T) { + + // Set up a test scenario: sending a DELETE request + path := fmt.Sprintf("/packscontent/%d", packItems[2].ID) + req, err := http.NewRequest("DELETE", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Check in the database if the pack has been deleted + var pack_id int + row := database.Db().QueryRow("SELECT pack_id FROM pack_content WHERE id = $1;", packItems[2].ID) + err = row.Scan(&pack_id) + if err == nil { + t.Errorf("Pack Item ID 3 associated to pack content id %d should be deleted and it is still in DB", pack_id) + } else if !errors.Is(err, sql.ErrNoRows) { + t.Fatalf("Failed to create request: %v", err) + } + }) +} + +func TestGetPackContentsByPackID(t *testing.T) { + var packContentWithItems dataset.PackContentWithItems + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for DeletePackByID handler + router.GET("/packs/:id/packcontents", GetPackContentsByPackID) + + t.Run("Retrieve fourth pack", func(t *testing.T) { + + path := fmt.Sprintf("/packs/%d/packcontents", packs[3].ID) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check the HTTP status code + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, w.Code) + } + + // Unmarshal the response body into a slice of packs struct + if err := json.Unmarshal(w.Body.Bytes(), &packContentWithItems); err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + + // determine if the answer is correct + if len(packContentWithItems) != len(packWithItems) { + t.Errorf("Expected same number of items in the pack but got %d instead of %d", len(packContentWithItems), len(packWithItems)) + } + switch { + case packContentWithItems[0].Pack_id != packWithItems[0].Pack_id: + t.Errorf("Expected Pack ID %v but got %v", packWithItems[0].Pack_id, packContentWithItems[0].Pack_id) + case packContentWithItems[0].Item_name != packWithItems[0].Item_name: + t.Errorf("Expected Item Name %v but got %v", packWithItems[0].Item_name, packContentWithItems[0].Item_name) + case packContentWithItems[0].Category != packWithItems[0].Category: + t.Errorf("Expected Category %v but got %v", packWithItems[0].Category, packContentWithItems[0].Category) + case packContentWithItems[0].Item_description != packWithItems[0].Item_description: + t.Errorf("Expected Item Description %v but got %v", packWithItems[0].Item_description, packContentWithItems[0].Item_description) + case packContentWithItems[0].Weight != packWithItems[0].Weight: + t.Errorf("Expected Weight %v but got %v", packWithItems[0].Weight, packContentWithItems[0].Weight) + case packContentWithItems[0].Weight_unit != packWithItems[0].Weight_unit: + t.Errorf("Expected Weight Unit %v but got %v", packWithItems[0].Weight_unit, packContentWithItems[0].Weight_unit) + case packContentWithItems[0].Item_url != packWithItems[0].Item_url: + t.Errorf("Expected Item URL %v but got %v", packWithItems[0].Item_url, packContentWithItems[0].Item_url) + case packContentWithItems[0].Price != packWithItems[0].Price: + t.Errorf("Expected Price %v but got %v", packWithItems[0].Price, packContentWithItems[0].Price) + case packContentWithItems[0].Currency != packWithItems[0].Currency: + t.Errorf("Expected Currency %v but got %v", packWithItems[0].Currency, packContentWithItems[0].Currency) + case packContentWithItems[0].Quantity != packWithItems[0].Quantity: + t.Errorf("Expected Quantity %v but got %v", packWithItems[0].Quantity, packContentWithItems[0].Quantity) + case packContentWithItems[0].Worn != packWithItems[0].Worn: + t.Errorf("Expected Worn %v but got %v", packWithItems[0].Worn, packContentWithItems[0].Worn) + case packContentWithItems[0].Consumable != packWithItems[0].Consumable: + t.Errorf("Expected Consumable %v but got %v", packWithItems[0].Consumable, packContentWithItems[0].Consumable) + } + }) + + t.Run("Pack Not Found", func(t *testing.T) { + req, err := http.NewRequest("GET", "/packs/1000/packcontents", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("Expected status code %d but got %d", http.StatusNotFound, w.Code) + } + }) +} + +func TestImportFromLighterPack(t *testing.T) { + // Read the CSV file + csvData := `Item Name,Category,desc,qty,weight,unit,url,price,worn,consumable +item1,category1,description1,1,100,g,http://example.com,10,worn,consumable +item2,category2,description2,2,150,g,http://example2.com,20,,consumable` + + tests := []struct { + name string + fileContents string + expectedCode int + }{ + { + name: "Valid CSV", + fileContents: "Item Name,Category,desc,qty,weight,unit,url,price,worn,consumable\nitem1,category1,description1,1,100,g,http://example.com,10,worn,consumable", + expectedCode: http.StatusOK, + }, + { + name: "Invalid CSV File", + fileContents: "some plain text", + expectedCode: http.StatusBadRequest, + }, + { + name: "CSV Data from File", + fileContents: csvData, + expectedCode: http.StatusOK, + }, + } + + token, err := security.GenerateToken(1) + if err != nil { + t.Fatalf("Failed to generate token: %v", err) + } + + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a Gin router instance + router := gin.Default() + + // Define the endpoint for DeletePackByID handler + router.POST("/importfromlighterpack", ImportFromLighterPack) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + + // Create a form file part with the CSV content + fileWriter, err := bodyWriter.CreateFormFile("file", "test.csv") + if err != nil { + t.Fatalf("Failed to create form file: %v", err) + } + + if _, err = fileWriter.Write([]byte(tt.fileContents)); err != nil { + t.Fatalf("Failed to write file contents: %v", err) + } + + contentType := bodyWriter.FormDataContentType() + bodyWriter.Close() + + req, err := http.NewRequest(http.MethodPost, "/importfromlighterpack", bodyBuf) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + req.Header.Set("Content-Type", contentType) + req.Header.Set("Authorization", "Bearer "+token) + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != tt.expectedCode { + t.Errorf("expected status code %d, got %d", tt.expectedCode, w.Code) + } + }) + } +} diff --git a/pkg/packs/testdata.go b/pkg/packs/testdata.go new file mode 100644 index 0000000..0df804e --- /dev/null +++ b/pkg/packs/testdata.go @@ -0,0 +1,284 @@ +package packs + +import ( + "fmt" + "time" + + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/Angak0k/pimpmypack/pkg/dataset" + "github.com/Angak0k/pimpmypack/pkg/helper" + "github.com/Angak0k/pimpmypack/pkg/security" + "github.com/gruntwork-io/terratest/modules/random" +) + +var users = []dataset.User{ + { + Username: fmt.Sprintf("user-%s", random.UniqueId()), + Email: fmt.Sprintf("user-%s@exemple.com", random.UniqueId()), + Firstname: "John", + Lastname: "Doe", + Role: "standard", + Status: "active", + Password: "password", + LastPassword: "password", + }, + { + Username: fmt.Sprintf("user-%s", random.UniqueId()), + Email: fmt.Sprintf("user-%s@exemple.com", random.UniqueId()), + Firstname: "John", + Lastname: "Doe", + Role: "standard", + Status: "active", + Password: "password", + LastPassword: "password", + }, +} + +var inventories_user_pack1 = dataset.Inventories{ + { + User_id: 1, + Item_name: "Backpack", + Category: "Outdoor Gear", + Description: "Spacious backpack for hiking", + Weight: 950, + Weight_unit: "METRIC", + Url: "https://example.com/backpack", + Price: 50, + Currency: "USD", + }, + { + User_id: 1, + Item_name: "Tent", + Category: "Shelter", + Description: "Spacious tent for hiking", + Weight: 1200, + Weight_unit: "METRIC", + Url: "https://example.com/tent", + Price: 150, + Currency: "USD", + }, + { + User_id: 1, + Item_name: "Sleeping Bag", + Category: "Sleeping", + Description: "Spacious sleeping bag for hiking", + Weight: 800, + Weight_unit: "METRIC", + Url: "https://example.com/sleeping-bag", + Price: 120, + Currency: "EUR", + }, +} + +var packs = dataset.Packs{ + { + User_id: 1, + Pack_name: "First Pack", + Pack_description: "Description for the first pack", + }, + { + User_id: 1, + Pack_name: "Second Pack", + Pack_description: "Description for the second pack", + }, + { + User_id: 2, + Pack_name: "Third Pack", + Pack_description: "Description for the third pack", + }, + { + User_id: 1, + Pack_name: "Special Pack", + Pack_description: "Description for the second pack", + }, +} + +var packItems = dataset.PackContents{ + { + Pack_id: 1, + Item_id: 1, + Quantity: 2, + Worn: true, + Consumable: false, + }, + { + Pack_id: 1, + Item_id: 2, + Quantity: 3, + Worn: false, + Consumable: true, + }, + { + Pack_id: 2, + Item_id: 2, + Quantity: 1, + Worn: true, + Consumable: false, + }, + { + Pack_id: 2, + Item_id: 1, + Quantity: 4, + Worn: true, + Consumable: true, + }, + { + Pack_id: 3, + Item_id: 1, + Quantity: 2, + Worn: false, + Consumable: false, + }, + { + Pack_id: 4, + Item_id: 1, + Quantity: 2, + Worn: false, + Consumable: false, + }, +} + +var packWithItems = dataset.PackContentWithItems{ + { + Pack_content_id: 1, + Pack_id: 4, + Item_name: "Backpack", + Category: "Outdoor Gear", + Item_description: "Spacious backpack for hiking", + Weight: 950, + Weight_unit: "METRIC", + Item_url: "https://example.com/backpack", + Price: 50, + Currency: "USD", + Quantity: 2, + Worn: false, + Consumable: false, + }, +} + +func loadingPackDataset() error { + + // Load accounts dataset + println("-> Loading accounts and passwords ...") + for i := range users { + var id uint + err := database.Db().QueryRow("INSERT INTO account (username, email, firstname, lastname, role, status, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id;", + users[i].Username, users[i].Email, users[i].Firstname, users[i].Lastname, users[i].Role, users[i].Status, time.Now().Truncate(time.Second), time.Now().Truncate(time.Second)).Scan(&users[i].ID) + if err != nil { + return err + } + + hashedPassword, err := security.HashPassword(users[i].Password) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + hashedLastPassword, err := security.HashPassword(users[i].LastPassword) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + err = database.Db().QueryRow("INSERT INTO password (user_id, password, last_password, updated_at) VALUES ($1,$2,$3,$4) RETURNING id;", users[i].ID, hashedPassword, hashedLastPassword, time.Now().Truncate(time.Second)).Scan(&id) + if err != nil { + return err + } + } + + println("-> Accounts Loaded...") + + // Transform inventories dataset by using the real user_id + for i := range inventories_user_pack1 { + switch inventories_user_pack1[i].User_id { + case 1: + inventories_user_pack1[i].User_id = helper.FinUserIDByUsername(users, users[0].Username) + case 2: + inventories_user_pack1[i].User_id = helper.FinUserIDByUsername(users, users[0].Username) + } + } + + // Transform packs dataset + for i := range packs { + switch packs[i].User_id { + case 1: + packs[i].User_id = helper.FinUserIDByUsername(users, users[0].Username) + case 2: + packs[i].User_id = helper.FinUserIDByUsername(users, users[0].Username) + } + } + + // Load inventories dataset + println("-> Loading Inventories...") + + // Insert inventories dataset + for i := range inventories_user_pack1 { + err := database.Db().QueryRow("INSERT INTO inventory (user_id, item_name, category, description, weight, weight_unit, url, price, currency, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id;", + inventories_user_pack1[i].User_id, inventories_user_pack1[i].Item_name, inventories_user_pack1[i].Category, inventories_user_pack1[i].Description, inventories_user_pack1[i].Weight, inventories_user_pack1[i].Weight_unit, inventories_user_pack1[i].Url, inventories_user_pack1[i].Price, inventories_user_pack1[i].Currency, time.Now().Truncate(time.Second), time.Now().Truncate(time.Second)).Scan(&inventories_user_pack1[i].ID) + if err != nil { + return err + } + } + println("-> Inventories Loaded...") + + // Load packs dataset + println("-> Loading Packs...") + + // Insert packs dataset + for i := range packs { + err := database.Db().QueryRow("INSERT INTO pack (user_id, pack_name, pack_description, created_at, updated_at) VALUES ($1,$2,$3,$4,$5) RETURNING id;", + packs[i].User_id, packs[i].Pack_name, packs[i].Pack_description, time.Now().Truncate(time.Second), time.Now().Truncate(time.Second)).Scan(&packs[i].ID) + if err != nil { + return err + } + } + println("-> Packs Loaded...") + + // Transform packs_contents dataset + + for i := range packItems { + switch packItems[i].Pack_id { + case 1: + packItems[i].Pack_id = helper.FinPackIDByPackName(packs, "First Pack") + case 2: + packItems[i].Pack_id = helper.FinPackIDByPackName(packs, "Second Pack") + case 3: + packItems[i].Pack_id = helper.FinPackIDByPackName(packs, "Third Pack") + case 4: + packItems[i].Pack_id = helper.FinPackIDByPackName(packs, "Special Pack") + } + switch packItems[i].Item_id { + case 1: + packItems[i].Item_id = helper.FinItemIDByItemName(inventories_user_pack1, "Backpack") + case 2: + packItems[i].Item_id = helper.FinItemIDByItemName(inventories_user_pack1, "Tent") + case 3: + packItems[i].Item_id = helper.FinItemIDByItemName(inventories_user_pack1, "Sleeping Bag") + } + } + + for i := range packWithItems { + switch packWithItems[i].Pack_id { + case 1: + packWithItems[i].Pack_id = helper.FinPackIDByPackName(packs, "First Pack") + case 2: + packWithItems[i].Pack_id = helper.FinPackIDByPackName(packs, "Second Pack") + case 3: + packWithItems[i].Pack_id = helper.FinPackIDByPackName(packs, "Third Pack") + case 4: + packWithItems[i].Pack_id = helper.FinPackIDByPackName(packs, "Special Pack") + } + } + + // Load pack_contents dataset + println("-> Loading Pack Contents...") + + // Insert pack_contents dataset + for i := range packItems { + err := database.Db().QueryRow("INSERT INTO pack_content (pack_id, item_id, quantity, worn, consumable, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id;", + packItems[i].Pack_id, packItems[i].Item_id, packItems[i].Quantity, packItems[i].Worn, packItems[i].Consumable, time.Now().Truncate(time.Second), time.Now().Truncate(time.Second)).Scan(&packItems[i].ID) + if err != nil { + return err + } + } + println("-> Pack Contents Loaded...") + return nil +} diff --git a/pkg/security/security.go b/pkg/security/security.go new file mode 100644 index 0000000..d203644 --- /dev/null +++ b/pkg/security/security.go @@ -0,0 +1,153 @@ +package security + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/Angak0k/pimpmypack/pkg/config" + "github.com/Angak0k/pimpmypack/pkg/database" + "github.com/gin-gonic/gin" + jwt "github.com/golang-jwt/jwt" + "golang.org/x/crypto/bcrypt" +) + +// Security Definitions: +// securityDefinitions: +// Bearer: +// type: apiKey +// name: Authorization +// in: header + +func HashPassword(password string) (string, error) { + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashedPassword), err +} + +func VerifyPassword(password, hashedPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + +func GenerateToken(user_id uint) (string, error) { + + claims := jwt.MapClaims{} + claims["authorized"] = true + claims["user_id"] = user_id + claims["exp"] = time.Now().Add(time.Hour * time.Duration(config.TokenLifespan)).Unix() + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return token.SignedString([]byte(config.ApiSecret)) + +} + +func TokenValid(c *gin.Context) error { + tokenString := ExtractToken(c) + _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(config.ApiSecret), nil + }) + if err != nil { + return err + } + return nil +} + +func ExtractToken(c *gin.Context) string { + token := c.Query("token") + if token != "" { + return token + } + bearerToken := c.Request.Header.Get("Authorization") + if len(strings.Split(bearerToken, " ")) == 2 { + return strings.Split(bearerToken, " ")[1] + } + return "" +} + +func ExtractTokenID(c *gin.Context) (uint, error) { + + tokenString := ExtractToken(c) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(os.Getenv("API_SECRET")), nil + }) + if err != nil { + return 0, err + } + claims, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32) + if err != nil { + return 0, err + } + return uint(uid), nil + } + return 0, nil +} + +func JwtAuthProcessor() gin.HandlerFunc { + return func(c *gin.Context) { + err := TokenValid(c) + if err != nil { + c.String(http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + c.Next() + } +} + +func JwtAuthAdminProcessor() gin.HandlerFunc { + return func(c *gin.Context) { + // check if token is valid + err := TokenValid(c) + if err != nil { + c.String(http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + + // get user_id from token + user_id, err := ExtractTokenID(c) + if err != nil { + c.String(http.StatusUnauthorized, "Invalid Token") + c.Abort() + return + } + + // check if user is admin + var role string + row := database.Db().QueryRow("SELECT role FROM account WHERE id = $1;", user_id) + err = row.Scan(&role) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + c.String(http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + c.String(http.StatusInternalServerError, "Something goes wrong") + c.Abort() + return + } + if role != "admin" { + c.String(http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + + c.Next() + } +} diff --git a/pkg/security/security_test.go b/pkg/security/security_test.go new file mode 100644 index 0000000..3cfd69f --- /dev/null +++ b/pkg/security/security_test.go @@ -0,0 +1,54 @@ +package security + +import ( + "errors" + "reflect" + "testing" +) + +func TestVerifyPassword(t *testing.T) { + + firsthashedPassword, err := HashPassword("password") + if err != nil { + t.Fatalf("failed to hash password: %v", err) + } + + secondhashedPassword, err := HashPassword("password1") + if err != nil { + t.Fatalf("failed to hash password: %v", err) + } + + type args struct { + password string + hashedPassword string + } + tests := []struct { + name string + args args + want error + }{ + { + name: "TestVerifyPassword", + args: args{ + password: "password", + hashedPassword: firsthashedPassword, + }, + want: nil, + }, + { + name: "TestVerifyPassword", + args: args{ + password: "password", + hashedPassword: secondhashedPassword, + }, + want: errors.New("crypto/bcrypt: hashedPassword is not the hash of the given password"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := VerifyPassword(tt.args.password, tt.args.hashedPassword); !reflect.DeepEqual(got, tt.want) { + t.Errorf("VerifyPassword() = %v, want %v", got, tt.want) + } + }) + } +}