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)
+ }
+ })
+ }
+}