├── .github ├── CODEOWNERS ├── FUNDING.yml ├── media │ ├── demo.gif │ ├── demo.mp4 │ ├── banner.png │ ├── screenshots │ │ ├── carts.png │ │ ├── pages.png │ │ ├── products.png │ │ ├── settings.png │ │ └── product-edit.png │ └── platforms │ │ ├── windows.svg │ │ ├── apple.svg │ │ └── docker.svg ├── dependabot.yml ├── labeler.yml ├── SECURITY.md ├── pull_request_template.md ├── workflows │ ├── analyze.yml │ └── release.yml ├── CONTRIBUTING.md └── ISSUE_TEMPLATE │ ├── question.yaml │ └── feature-request.yaml ├── web ├── site │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ └── assets │ │ │ ├── img │ │ │ ├── noimage.png │ │ │ └── loading.svg │ │ │ ├── js │ │ │ └── form │ │ │ │ └── button.js │ │ │ └── css │ │ │ └── main.css │ ├── bun.lockb │ ├── 404.html │ ├── tailwind.config.js │ ├── pages.html │ ├── cancel.html │ ├── success.html │ ├── package.json │ └── layouts │ │ └── clear.html ├── admin │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ └── assets │ │ │ └── img │ │ │ └── noimage.png │ ├── src │ │ ├── layouts │ │ │ └── Blank.vue │ │ ├── assets │ │ │ ├── tippy.css │ │ │ ├── svg │ │ │ │ ├── minus.svg │ │ │ │ ├── plus.svg │ │ │ │ ├── x-mark.svg │ │ │ │ ├── paragraph.svg │ │ │ │ ├── arrow-right.svg │ │ │ │ ├── redo.svg │ │ │ │ ├── undo.svg │ │ │ │ ├── h1.svg │ │ │ │ ├── cube.svg │ │ │ │ ├── at-symbol.svg │ │ │ │ ├── italic.svg │ │ │ │ ├── queue-list.svg │ │ │ │ ├── open.svg │ │ │ │ ├── code.svg │ │ │ │ ├── exit.svg │ │ │ │ ├── user.svg │ │ │ │ ├── link.svg │ │ │ │ ├── paper-clip.svg │ │ │ │ ├── arrow-left-on-rectangle.svg │ │ │ │ ├── arrow-path.svg │ │ │ │ ├── lock-open.svg │ │ │ │ ├── credit-card.svg │ │ │ │ ├── lock-closed.svg │ │ │ │ ├── exclamation.svg │ │ │ │ ├── key.svg │ │ │ │ ├── bulletlist.svg │ │ │ │ ├── bold.svg │ │ │ │ ├── h2.svg │ │ │ │ ├── envelope.svg │ │ │ │ ├── pencil-square.svg │ │ │ │ ├── cart.svg │ │ │ │ ├── webhook.svg │ │ │ │ ├── list-bullet.svg │ │ │ │ ├── cube-transparent.svg │ │ │ │ ├── eye.svg │ │ │ │ ├── social │ │ │ │ │ ├── facebook.svg │ │ │ │ │ ├── youtube.svg │ │ │ │ │ ├── twitter.svg │ │ │ │ │ ├── other.svg │ │ │ │ │ ├── github.svg │ │ │ │ │ ├── dribbble.svg │ │ │ │ │ └── instagram.svg │ │ │ │ ├── bag.svg │ │ │ │ ├── server.svg │ │ │ │ ├── photo.svg │ │ │ │ ├── finger-print.svg │ │ │ │ ├── fire.svg │ │ │ │ ├── codeblock.svg │ │ │ │ ├── eye-slash.svg │ │ │ │ ├── strike.svg │ │ │ │ ├── trash.svg │ │ │ │ ├── h3.svg │ │ │ │ ├── rocket.svg │ │ │ │ ├── docs.svg │ │ │ │ ├── glob-alt.svg │ │ │ │ ├── blockquote.svg │ │ │ │ ├── money.svg │ │ │ │ ├── user-group.svg │ │ │ │ ├── home.svg │ │ │ │ ├── orderedlist.svg │ │ │ │ └── booth.svg │ │ │ ├── app.css │ │ │ ├── table.css │ │ │ ├── main.css │ │ │ └── nprogress.css │ │ ├── pages │ │ │ ├── 404.vue │ │ │ ├── settings │ │ │ │ ├── webhook.vue │ │ │ │ ├── main.vue │ │ │ │ └── social.vue │ │ │ ├── Signin.vue │ │ │ ├── Install.vue │ │ │ └── Carts.vue │ │ ├── store │ │ │ └── system.js │ │ ├── utils │ │ │ ├── message │ │ │ │ └── index.js │ │ │ └── api │ │ │ │ └── index.js │ │ ├── App.vue │ │ ├── components │ │ │ ├── SvgIcon.vue │ │ │ ├── DetailList.vue │ │ │ ├── Badge.vue │ │ │ ├── form │ │ │ │ ├── Button.vue │ │ │ │ ├── Input.vue │ │ │ │ ├── Select.vue │ │ │ │ ├── Textarea.vue │ │ │ │ ├── Toggle.vue │ │ │ │ └── Upload.vue │ │ │ ├── index.js │ │ │ ├── page │ │ │ │ └── Seo.vue │ │ │ └── product │ │ │ │ └── Seo.vue │ │ └── main.js │ ├── bun.lockb │ ├── prettier.config.js │ ├── postcss.config.js │ ├── tailwind.config.js │ ├── index.html │ ├── vite.config.js │ └── package.json └── embed.go ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── tailwind.json ├── fixtures ├── digitals │ ├── 1ca0a335-7cde-4ba1-a700-138cca9ca852.png │ └── ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec.png └── uploads │ ├── 0f8e7e98-1639-40a3-97f6-0aac15538d88.png │ ├── 165d4e99-ba1b-4d03-ba6c-3abfab65830e.png │ ├── 1ca0a335-7cde-4ba1-a700-138cca9ca852.png │ ├── 32b0115f-27aa-4a9f-aebf-c7250d1a118e.png │ ├── 746becd7-59dc-4a00-aca9-e86e7290a54f.png │ ├── 76396b3e-5964-4f87-b80c-7909b2de9571.png │ ├── aa322bd6-93de-42f1-a59d-43160e67e890.png │ ├── cae6dcde-9813-4ab2-9436-7bd4b2ccea36.png │ ├── d3f08f52-b290-430f-9fc7-45456fe3319f.png │ ├── e827e0be-aaf6-4008-aacf-da35cf47952f.png │ ├── ecd77e90-2b35-49eb-a810-a1ecf74c21a7.png │ ├── f9b85683-25ee-40cd-b398-9c990d90b80b.png │ ├── ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec.png │ ├── 0f8e7e98-1639-40a3-97f6-0aac15538d88_md.png │ ├── 0f8e7e98-1639-40a3-97f6-0aac15538d88_sm.png │ ├── 165d4e99-ba1b-4d03-ba6c-3abfab65830e_md.png │ ├── 165d4e99-ba1b-4d03-ba6c-3abfab65830e_sm.png │ ├── 1ca0a335-7cde-4ba1-a700-138cca9ca852_md.png │ ├── 1ca0a335-7cde-4ba1-a700-138cca9ca852_sm.png │ ├── 32b0115f-27aa-4a9f-aebf-c7250d1a118e_md.png │ ├── 32b0115f-27aa-4a9f-aebf-c7250d1a118e_sm.png │ ├── 746becd7-59dc-4a00-aca9-e86e7290a54f_md.png │ ├── 746becd7-59dc-4a00-aca9-e86e7290a54f_sm.png │ ├── 76396b3e-5964-4f87-b80c-7909b2de9571_md.png │ ├── 76396b3e-5964-4f87-b80c-7909b2de9571_sm.png │ ├── aa322bd6-93de-42f1-a59d-43160e67e890_md.png │ ├── aa322bd6-93de-42f1-a59d-43160e67e890_sm.png │ ├── cae6dcde-9813-4ab2-9436-7bd4b2ccea36_md.png │ ├── cae6dcde-9813-4ab2-9436-7bd4b2ccea36_sm.png │ ├── d3f08f52-b290-430f-9fc7-45456fe3319f_md.png │ ├── d3f08f52-b290-430f-9fc7-45456fe3319f_sm.png │ ├── e827e0be-aaf6-4008-aacf-da35cf47952f_md.png │ ├── e827e0be-aaf6-4008-aacf-da35cf47952f_sm.png │ ├── ecd77e90-2b35-49eb-a810-a1ecf74c21a7_md.png │ ├── ecd77e90-2b35-49eb-a810-a1ecf74c21a7_sm.png │ ├── f9b85683-25ee-40cd-b398-9c990d90b80b_md.png │ ├── f9b85683-25ee-40cd-b398-9c990d90b80b_sm.png │ ├── ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec_md.png │ └── ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec_sm.png ├── .prettierrc.json ├── migrations ├── embed.go ├── 20231129131044_brief.sql ├── 20231110193417_added_payment_field.sql ├── 20240111145753_fix_smtp_port.sql ├── 20231208185127_mail.sql ├── 20230926150007_seo.sql ├── 20231124220911_paypal.sql └── 20240111145752_new_sicials.sql ├── pkg ├── litepay │ ├── provider.go │ ├── litepay.go │ └── cart.go ├── strutil │ └── strutil.go ├── update │ ├── version.go │ ├── release.go │ └── update_test.go ├── security │ ├── random_test.go │ ├── random.go │ └── password.go ├── logging │ └── logging.go ├── jwtutil │ ├── generator.go │ └── parser.go ├── fsutil │ ├── embed.go │ ├── folder.go │ └── file.go ├── errors │ └── errors.go ├── archive │ ├── archive.go │ ├── tar_test.go │ ├── zip.go │ └── tar.go └── webutil │ └── webutil.go ├── Dockerfile.goreleaser ├── docker ├── docker-compose.yml ├── nginx.conf └── just_docker-compose_example.yml ├── scripts ├── sqlite ├── clear ├── golang ├── tools ├── webscripts └── _helper ├── internal ├── models │ ├── core.go │ ├── install.go │ ├── pages.go │ ├── cart.go │ └── auth.go ├── routes │ ├── admin_routes.go │ ├── api_public_routes.go │ ├── not_found_route.go │ ├── site_routes.go │ └── api_private_routes.go ├── webhook │ ├── webhook.go │ ├── payment_test.go │ └── payment.go ├── middleware │ ├── fiber.go │ ├── jwt_test.go │ └── jwt.go ├── testutil │ └── testdir.go ├── handlers │ ├── public │ │ ├── page.go │ │ ├── product.go │ │ └── setting.go │ └── private │ │ ├── install.go │ │ ├── cart.go │ │ ├── cart_test.go │ │ └── auth_test.go ├── init.go ├── queries │ ├── auth.go │ ├── queries.go │ ├── session.go │ ├── install.go │ └── queries_test.go ├── base │ └── base.go └── mailer │ └── mailer.go ├── LICENSE ├── .gitignore ├── k8s └── litecart.yaml └── go.mod /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @shurco 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: shurco -------------------------------------------------------------------------------- /web/site/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /web/admin/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /_ -------------------------------------------------------------------------------- /web/admin/src/layouts/Blank.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/site/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/web/site/bun.lockb -------------------------------------------------------------------------------- /web/admin/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/web/admin/bun.lockb -------------------------------------------------------------------------------- /.github/media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/.github/media/demo.gif -------------------------------------------------------------------------------- /.github/media/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/.github/media/demo.mp4 -------------------------------------------------------------------------------- /.github/media/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/.github/media/banner.png -------------------------------------------------------------------------------- /web/admin/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["prettier-plugin-tailwindcss"], 3 | }; 4 | -------------------------------------------------------------------------------- /web/admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/web/admin/public/favicon.ico -------------------------------------------------------------------------------- /web/site/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/web/site/public/favicon.ico -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/media/screenshots/carts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/.github/media/screenshots/carts.png -------------------------------------------------------------------------------- /.github/media/screenshots/pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/.github/media/screenshots/pages.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm.packageManager": "bun", 3 | "css.customData": [".vscode/tailwind.json"], 4 | } 5 | -------------------------------------------------------------------------------- /.github/media/screenshots/products.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/.github/media/screenshots/products.png -------------------------------------------------------------------------------- /.github/media/screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/.github/media/screenshots/settings.png -------------------------------------------------------------------------------- /web/site/public/assets/img/noimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/web/site/public/assets/img/noimage.png -------------------------------------------------------------------------------- /web/admin/public/assets/img/noimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/web/admin/public/assets/img/noimage.png -------------------------------------------------------------------------------- /.github/media/screenshots/product-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/.github/media/screenshots/product-edit.png -------------------------------------------------------------------------------- /web/site/404.html: -------------------------------------------------------------------------------- 1 |
2 |

404 | Not Found

3 |
-------------------------------------------------------------------------------- /fixtures/digitals/1ca0a335-7cde-4ba1-a700-138cca9ca852.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/digitals/1ca0a335-7cde-4ba1-a700-138cca9ca852.png -------------------------------------------------------------------------------- /fixtures/digitals/ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/digitals/ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec.png -------------------------------------------------------------------------------- /fixtures/uploads/0f8e7e98-1639-40a3-97f6-0aac15538d88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/0f8e7e98-1639-40a3-97f6-0aac15538d88.png -------------------------------------------------------------------------------- /fixtures/uploads/165d4e99-ba1b-4d03-ba6c-3abfab65830e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/165d4e99-ba1b-4d03-ba6c-3abfab65830e.png -------------------------------------------------------------------------------- /fixtures/uploads/1ca0a335-7cde-4ba1-a700-138cca9ca852.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/1ca0a335-7cde-4ba1-a700-138cca9ca852.png -------------------------------------------------------------------------------- /fixtures/uploads/32b0115f-27aa-4a9f-aebf-c7250d1a118e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/32b0115f-27aa-4a9f-aebf-c7250d1a118e.png -------------------------------------------------------------------------------- /fixtures/uploads/746becd7-59dc-4a00-aca9-e86e7290a54f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/746becd7-59dc-4a00-aca9-e86e7290a54f.png -------------------------------------------------------------------------------- /fixtures/uploads/76396b3e-5964-4f87-b80c-7909b2de9571.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/76396b3e-5964-4f87-b80c-7909b2de9571.png -------------------------------------------------------------------------------- /fixtures/uploads/aa322bd6-93de-42f1-a59d-43160e67e890.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/aa322bd6-93de-42f1-a59d-43160e67e890.png -------------------------------------------------------------------------------- /fixtures/uploads/cae6dcde-9813-4ab2-9436-7bd4b2ccea36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/cae6dcde-9813-4ab2-9436-7bd4b2ccea36.png -------------------------------------------------------------------------------- /fixtures/uploads/d3f08f52-b290-430f-9fc7-45456fe3319f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/d3f08f52-b290-430f-9fc7-45456fe3319f.png -------------------------------------------------------------------------------- /fixtures/uploads/e827e0be-aaf6-4008-aacf-da35cf47952f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/e827e0be-aaf6-4008-aacf-da35cf47952f.png -------------------------------------------------------------------------------- /fixtures/uploads/ecd77e90-2b35-49eb-a810-a1ecf74c21a7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/ecd77e90-2b35-49eb-a810-a1ecf74c21a7.png -------------------------------------------------------------------------------- /fixtures/uploads/f9b85683-25ee-40cd-b398-9c990d90b80b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/f9b85683-25ee-40cd-b398-9c990d90b80b.png -------------------------------------------------------------------------------- /fixtures/uploads/ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec.png -------------------------------------------------------------------------------- /fixtures/uploads/0f8e7e98-1639-40a3-97f6-0aac15538d88_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/0f8e7e98-1639-40a3-97f6-0aac15538d88_md.png -------------------------------------------------------------------------------- /fixtures/uploads/0f8e7e98-1639-40a3-97f6-0aac15538d88_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/0f8e7e98-1639-40a3-97f6-0aac15538d88_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/165d4e99-ba1b-4d03-ba6c-3abfab65830e_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/165d4e99-ba1b-4d03-ba6c-3abfab65830e_md.png -------------------------------------------------------------------------------- /fixtures/uploads/165d4e99-ba1b-4d03-ba6c-3abfab65830e_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/165d4e99-ba1b-4d03-ba6c-3abfab65830e_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/1ca0a335-7cde-4ba1-a700-138cca9ca852_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/1ca0a335-7cde-4ba1-a700-138cca9ca852_md.png -------------------------------------------------------------------------------- /fixtures/uploads/1ca0a335-7cde-4ba1-a700-138cca9ca852_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/1ca0a335-7cde-4ba1-a700-138cca9ca852_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/32b0115f-27aa-4a9f-aebf-c7250d1a118e_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/32b0115f-27aa-4a9f-aebf-c7250d1a118e_md.png -------------------------------------------------------------------------------- /fixtures/uploads/32b0115f-27aa-4a9f-aebf-c7250d1a118e_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/32b0115f-27aa-4a9f-aebf-c7250d1a118e_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/746becd7-59dc-4a00-aca9-e86e7290a54f_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/746becd7-59dc-4a00-aca9-e86e7290a54f_md.png -------------------------------------------------------------------------------- /fixtures/uploads/746becd7-59dc-4a00-aca9-e86e7290a54f_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/746becd7-59dc-4a00-aca9-e86e7290a54f_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/76396b3e-5964-4f87-b80c-7909b2de9571_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/76396b3e-5964-4f87-b80c-7909b2de9571_md.png -------------------------------------------------------------------------------- /fixtures/uploads/76396b3e-5964-4f87-b80c-7909b2de9571_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/76396b3e-5964-4f87-b80c-7909b2de9571_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/aa322bd6-93de-42f1-a59d-43160e67e890_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/aa322bd6-93de-42f1-a59d-43160e67e890_md.png -------------------------------------------------------------------------------- /fixtures/uploads/aa322bd6-93de-42f1-a59d-43160e67e890_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/aa322bd6-93de-42f1-a59d-43160e67e890_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/cae6dcde-9813-4ab2-9436-7bd4b2ccea36_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/cae6dcde-9813-4ab2-9436-7bd4b2ccea36_md.png -------------------------------------------------------------------------------- /fixtures/uploads/cae6dcde-9813-4ab2-9436-7bd4b2ccea36_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/cae6dcde-9813-4ab2-9436-7bd4b2ccea36_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/d3f08f52-b290-430f-9fc7-45456fe3319f_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/d3f08f52-b290-430f-9fc7-45456fe3319f_md.png -------------------------------------------------------------------------------- /fixtures/uploads/d3f08f52-b290-430f-9fc7-45456fe3319f_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/d3f08f52-b290-430f-9fc7-45456fe3319f_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/e827e0be-aaf6-4008-aacf-da35cf47952f_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/e827e0be-aaf6-4008-aacf-da35cf47952f_md.png -------------------------------------------------------------------------------- /fixtures/uploads/e827e0be-aaf6-4008-aacf-da35cf47952f_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/e827e0be-aaf6-4008-aacf-da35cf47952f_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/ecd77e90-2b35-49eb-a810-a1ecf74c21a7_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/ecd77e90-2b35-49eb-a810-a1ecf74c21a7_md.png -------------------------------------------------------------------------------- /fixtures/uploads/ecd77e90-2b35-49eb-a810-a1ecf74c21a7_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/ecd77e90-2b35-49eb-a810-a1ecf74c21a7_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/f9b85683-25ee-40cd-b398-9c990d90b80b_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/f9b85683-25ee-40cd-b398-9c990d90b80b_md.png -------------------------------------------------------------------------------- /fixtures/uploads/f9b85683-25ee-40cd-b398-9c990d90b80b_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/f9b85683-25ee-40cd-b398-9c990d90b80b_sm.png -------------------------------------------------------------------------------- /fixtures/uploads/ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec_md.png -------------------------------------------------------------------------------- /fixtures/uploads/ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurco/litecart/HEAD/fixtures/uploads/ff0b48d1-0a75-4d67-a0ac-e6243cfd6cec_sm.png -------------------------------------------------------------------------------- /web/admin/src/assets/tippy.css: -------------------------------------------------------------------------------- 1 | .tippy-box[data-theme~='lite'] { 2 | @apply opacity-75 inline-flex self-end rounded bg-slate-900 px-2 py-1 text-slate-200; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 120, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /migrations/embed.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import "embed" 4 | 5 | //go:embed *.sql 6 | var migrations embed.FS 7 | 8 | // Embed is ... 9 | func Embed() embed.FS { 10 | return migrations 11 | } 12 | -------------------------------------------------------------------------------- /web/admin/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": "postcss-nesting", 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /pkg/litepay/provider.go: -------------------------------------------------------------------------------- 1 | package litepay 2 | 3 | type PaymentSystem string 4 | 5 | const ( 6 | STRIPE PaymentSystem = "stripe" 7 | PAYPAL PaymentSystem = "paypal" 8 | SPECTROCOIN PaymentSystem = "spectrocoin" 9 | ) 10 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | @import './nprogress.css'; 6 | @import './tippy.css'; 7 | @import './main.css'; 8 | @import './table.css'; 9 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/x-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/pages/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.github/media/platforms/windows.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/paragraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/store/system.js: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import { defineStore } from 'pinia'; 3 | 4 | export const useSystemStore = defineStore('system', () => { 5 | const version = ref({}) 6 | const payments = ref({}) 7 | 8 | return { version, payments } 9 | }) -------------------------------------------------------------------------------- /web/admin/src/assets/svg/h1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/site/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./**/*.{html,js}", "!./**/node_modules/**"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [ 8 | require('@tailwindcss/forms'), 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS certs 2 | RUN apk add --no-cache ca-certificates && update-ca-certificates 2>/dev/null || true 3 | 4 | FROM scratch 5 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 6 | COPY litecart / 7 | ENTRYPOINT ["/litecart"] 8 | CMD ["serve"] -------------------------------------------------------------------------------- /web/admin/src/utils/message/index.js: -------------------------------------------------------------------------------- 1 | // "connextSuccess" | "connextError" | "connextWarning" | "connextInfo"; 2 | export function showMessage(message, event = "connextSuccess") { 3 | const eventMessage = new CustomEvent(event, { 4 | detail: message 5 | }) 6 | dispatchEvent(eventMessage) 7 | } 8 | -------------------------------------------------------------------------------- /migrations/20231129131044_brief.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | ALTER TABLE product ADD COLUMN "brief" TEXT NOT NULL DEFAULT ''; 4 | -- +goose StatementEnd 5 | 6 | -- +goose Down 7 | -- +goose StatementBegin 8 | ALTER TABLE product DROP COLUMN "brief"; 9 | -- +goose StatementEnd 10 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("@tailwindcss/forms")({ 8 | strategy: 'class', 9 | })], 10 | }; 11 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/at-symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | image: nginx:alpine 5 | restart: always 6 | network_mode: host 7 | volumes: 8 | - "./nginx.conf:/etc/nginx/conf.d/default.conf" 9 | 10 | mailhog: 11 | image: jcalonso/mailhog 12 | restart: always 13 | network_mode: host 14 | -------------------------------------------------------------------------------- /migrations/20231110193417_added_payment_field.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | INSERT INTO setting VALUES ('7HkP2nYgR4sL8Qo', 'payment_webhook_url', ''); 4 | -- +goose StatementEnd 5 | 6 | -- +goose Down 7 | -- +goose StatementBegin 8 | DELETE FROM setting WHERE id = '7HkP2nYgR4sL8Qo'; 9 | -- +goose StatementEnd 10 | -------------------------------------------------------------------------------- /web/admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/queue-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/site/pages.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

{{content.name}}

5 |
6 |
7 |
8 |
9 |
-------------------------------------------------------------------------------- /web/admin/src/assets/svg/open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | litecart 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /migrations/20240111145753_fix_smtp_port.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | -- Fix existing smtp_port values that might be '0' to empty string 4 | UPDATE setting SET value = '' WHERE key = 'smtp_port' AND value = '0'; 5 | -- +goose StatementEnd 6 | 7 | -- +goose Down 8 | -- +goose StatementBegin 9 | -- No down migration needed for this fix 10 | -- +goose StatementEnd -------------------------------------------------------------------------------- /web/admin/src/assets/svg/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/paper-clip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/arrow-left-on-rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/arrow-path.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/site/cancel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

Payment interrupted 🥺

7 |
8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /web/admin/src/assets/svg/lock-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/credit-card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/lock-closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/exclamation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/site/success.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

Successful payment 🥰

7 |
8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /scripts/sqlite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #set -e 4 | 5 | ROOT_PATH="$(git rev-parse --show-toplevel)" 6 | source ${ROOT_PATH}/scripts/_helper 7 | 8 | LC_BASE_DIR="${ROOT_PATH}/cmd/lc_base" 9 | DB_FILE="${LC_BASE_DIR}/data.db" 10 | 11 | mkdir -p "${LC_BASE_DIR}" 12 | 13 | if ! [ -f "${DB_FILE}" ]; then 14 | sqlite3 "${DB_FILE}" "PRAGMA auto_vacuum;" 15 | fi 16 | 17 | sqlite3 "${DB_FILE}" "VACUUM;" 18 | -------------------------------------------------------------------------------- /web/site/public/assets/img/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/bulletlist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "litecart-site", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "tailwindcss -o ./public/assets/css/style.css --watch --minify", 7 | "build": "tailwindcss -o ./public/assets/css/style.css --minify", 8 | "update": "bun update" 9 | }, 10 | "devDependencies": { 11 | "@tailwindcss/forms": "=0.5.9", 12 | "tailwindcss": "=3.4.13" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /migrations/20231208185127_mail.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | INSERT INTO setting VALUES ('nbyPHZ5roJt5Z2v', 'mail_sender_name', ''); 4 | INSERT INTO setting VALUES ('T3kP16of88quy3x', 'mail_sender_email', ''); 5 | -- +goose StatementEnd 6 | 7 | -- +goose Down 8 | -- +goose StatementBegin 9 | DELETE FROM setting WHERE id = 'T3kP16of88quy3x'; 10 | DELETE FROM setting WHERE id = 'nbyPHZ5roJt5Z2v'; 11 | -- +goose StatementEnd -------------------------------------------------------------------------------- /web/admin/src/assets/svg/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/h2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pkg/strutil/strutil.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import "strings" 4 | 5 | // ToSlice split string to array. 6 | func ToSlice(s string, sep ...string) []string { 7 | if len(sep) > 0 { 8 | return strings.Split(s, sep[0]) 9 | } 10 | return strings.Split(s, ",") 11 | } 12 | 13 | // ToAny split string to anu. 14 | func ToAny(key ...string) []any { 15 | keys := make([]any, len(key)) 16 | for i, v := range key { 17 | keys[i] = v 18 | } 19 | return keys 20 | } 21 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/envelope.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/pencil-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/webhook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/list-bullet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/cube-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/social/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/models/core.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Core is ... 4 | type Core struct { 5 | ID string `json:"id"` 6 | Created int64 `json:"created"` 7 | Updated int64 `json:"updated,omitempty"` 8 | } 9 | 10 | // UpdateClause is ... 11 | type UpdateClause struct { 12 | Field string 13 | Value string 14 | } 15 | 16 | // Seo is ... 17 | type Seo struct { 18 | Title string `json:"title"` 19 | Keywords string `json:"keywords"` 20 | Description string `json:"description"` 21 | } 22 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/bag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "cart", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "program": "${workspaceFolder}/cmd", 10 | "args": ["serve", "--dev"] 11 | //"args": ["init"] 12 | //"args": ["update"] 13 | //"args": ["migrate"] 14 | //"args": ["serve", "--no-site"] 15 | //"args": ["serve"] 16 | //"args": ["serve", "--help"] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/photo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/finger-print.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/fire.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /migrations/20230926150007_seo.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | INSERT INTO setting VALUES ('EepD9r9nRHrIAXp', 'site_name', ''); 4 | ALTER TABLE product ADD COLUMN "seo" JSON DEFAULT '{}' NOT NULL; 5 | ALTER TABLE page ADD COLUMN "seo" JSON DEFAULT '{}' NOT NULL; 6 | -- +goose StatementEnd 7 | 8 | -- +goose Down 9 | -- +goose StatementBegin 10 | ALTER TABLE page DROP COLUMN "seo"; 11 | ALTER TABLE product DROP COLUMN "seo"; 12 | DELETE FROM setting WHERE id = 'EepD9r9nRHrIAXp'; 13 | -- +goose StatementEnd 14 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/codeblock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pkg/update/version.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | // Version is ... 4 | type Version struct { 5 | CurrentVersion string `json:"current_version"` 6 | GitCommit string `json:"gitCommit"` 7 | BuildDate string `json:"buildDate"` 8 | NewVersion string `json:"new,omitempty"` 9 | ReleaseURL string `json:"release_url,omitempty"` 10 | } 11 | 12 | var versionInfo *Version 13 | 14 | func SetVersion(ver *Version) { 15 | versionInfo = ver 16 | } 17 | 18 | func VersionInfo() *Version { 19 | return versionInfo 20 | } 21 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/eye-slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/social/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/strike.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | client_max_body_size 20M; 2 | 3 | upstream cart { 4 | server localhost:8080; 5 | } 6 | 7 | server { 8 | listen 80; 9 | listen [::]:80; 10 | 11 | gzip on; 12 | 13 | location / { 14 | proxy_pass http://cart; 15 | proxy_redirect off; 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 19 | proxy_set_header X-Forwarded-Host $server_name; 20 | } 21 | } -------------------------------------------------------------------------------- /pkg/security/random_test.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRandomString_LengthAndAlphabet(t *testing.T) { 8 | const iterations = 100 9 | for i := 0; i < iterations; i++ { 10 | s := RandomString() 11 | if len(s) != DefaultIdLength { 12 | t.Fatalf("unexpected length: got %d want %d", len(s), DefaultIdLength) 13 | } 14 | for _, r := range s { 15 | if r < '0' || (r > '9' && r < 'a') || r > 'z' { // alphabet is [a-z0-9] 16 | t.Fatalf("unexpected rune %q in %q", r, s) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/clear: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #set -e 4 | 5 | ROOT_PATH="$(git rev-parse --show-toplevel)" 6 | source ${ROOT_PATH}/scripts/_helper 7 | 8 | arr_process=(__debug_bin vite) 9 | for process in "${arr_process[@]}"; do 10 | if pkill -f "$process" >/dev/null; then 11 | print_header "Killing $process process" 12 | print_answer "SUCCESS" green 13 | fi 14 | done 15 | 16 | print_header "Remove old bin and dist files" 17 | rm -rf ${ROOT_PATH}/cmd/${name}/__debug_bin* 18 | rm -rf ${ROOT_PATH}/web/admin/dist 19 | print_answer "SUCCESS" green 20 | -------------------------------------------------------------------------------- /web/admin/src/assets/table.css: -------------------------------------------------------------------------------- 1 | table { 2 | @apply min-w-full divide-y-2 divide-gray-200 text-sm; 3 | 4 | thead { 5 | @apply text-left; 6 | 7 | tr { 8 | @apply bg-gray-100; 9 | } 10 | } 11 | 12 | th { 13 | @apply whitespace-nowrap px-4 py-2 font-medium text-gray-900; 14 | } 15 | 16 | tbody { 17 | @apply divide-y divide-gray-200; 18 | 19 | tr { 20 | @apply cursor-pointer hover:bg-gray-100 active:bg-gray-100; 21 | 22 | td { 23 | @apply px-4 py-2; 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.github/media/platforms/apple.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rs/zerolog" 7 | "github.com/rs/zerolog/pkgerrors" 8 | ) 9 | 10 | type Log struct { 11 | *zerolog.Logger 12 | } 13 | 14 | func New() *Log { 15 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix 16 | zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack 17 | 18 | log := zerolog.New(os.Stderr).With().Timestamp().Logger() 19 | return &Log{ 20 | &log, 21 | } 22 | } 23 | 24 | func (l *Log) ErrorStack(err error) { 25 | l.Error().Caller(1).Stack().Err(err).Send() 26 | } 27 | -------------------------------------------------------------------------------- /internal/routes/admin_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "io/fs" 5 | "net/http" 6 | 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/gofiber/fiber/v2/middleware/filesystem" 9 | "github.com/shurco/litecart/web" 10 | ) 11 | 12 | // AdminRoutes is ... 13 | func AdminRoutes(c *fiber.App) { 14 | embedAdmin, _ := fs.Sub(web.EmbedAdmin(), "admin/dist") 15 | c.Use("/_", filesystem.New(filesystem.Config{ 16 | Root: http.FS(embedAdmin), 17 | Index: "index.html", 18 | NotFoundFile: "index.html", 19 | MaxAge: 3600, 20 | })) 21 | } 22 | -------------------------------------------------------------------------------- /migrations/20231124220911_paypal.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | INSERT INTO setting VALUES ('8qADdU1QvVluLa7', 'paypal_active', 'false'); 4 | INSERT INTO setting VALUES ('NPnCcAhHgG26p1a', 'paypal_client_id', ''); 5 | INSERT INTO setting VALUES ('BCY0A3hbWwH0cay', 'paypal_secret_key', ''); 6 | -- +goose StatementEnd 7 | 8 | -- +goose Down 9 | -- +goose StatementBegin 10 | DELETE FROM setting WHERE id = 'BCY0A3hbWwH0cay'; 11 | DELETE FROM setting WHERE id = 'NPnCcAhHgG26p1a'; 12 | DELETE FROM setting WHERE id = '8qADdU1QvVluLa7'; 13 | -- +goose StatementEnd -------------------------------------------------------------------------------- /web/admin/src/components/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /web/admin/src/components/DetailList.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /web/embed.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed all:admin/dist 8 | var embedAdmin embed.FS 9 | 10 | //go:embed admin/dist/index.html 11 | var embedAdminIndex embed.FS 12 | 13 | //go:embed site/*.html site/layouts/*.html site/public/* 14 | var embedSite embed.FS 15 | 16 | // EmbedAdmin is ... 17 | func EmbedAdmin() embed.FS { 18 | return embedAdmin 19 | } 20 | 21 | // EmbedAdminIndex is ... 22 | func EmbedAdminIndex() embed.FS { 23 | return embedAdminIndex 24 | } 25 | 26 | // EmbedSite is ... 27 | func EmbedSite() embed.FS { 28 | return embedSite 29 | } 30 | -------------------------------------------------------------------------------- /internal/routes/api_public_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | 6 | handlers "github.com/shurco/litecart/internal/handlers/public" 7 | ) 8 | 9 | // ApiPublicRoutes is ... 10 | func ApiPublicRoutes(c *fiber.App) { 11 | c.Get("/ping", handlers.Ping) 12 | 13 | c.Get("/api/settings", handlers.Settings) 14 | c.Get("/api/pages/:page_slug", handlers.Page) 15 | 16 | product := c.Group("/api/products") 17 | product.Get("/", handlers.Products) 18 | product.Get("/:product_id", handlers.Product) 19 | 20 | c.Get("/api/cart/payment", handlers.PaymentList) 21 | } 22 | -------------------------------------------------------------------------------- /internal/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | func Send(url string, payload []byte) (*http.Response, error) { 10 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) 11 | if err != nil { 12 | return nil, fmt.Errorf("error creating request: %v", err) 13 | } 14 | 15 | req.Header.Set("Content-Type", "application/json") 16 | 17 | client := &http.Client{} 18 | resp, err := client.Do(req) 19 | if err != nil { 20 | return nil, fmt.Errorf("error making request: %v", err) 21 | } 22 | 23 | return resp, nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/models/install.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | validation "github.com/go-ozzo/ozzo-validation/v4" 5 | "github.com/go-ozzo/ozzo-validation/v4/is" 6 | ) 7 | 8 | // Install is ... 9 | type Install struct { 10 | Email string `json:"email"` 11 | Password string `json:"password"` 12 | Domain string `json:"domain"` 13 | } 14 | 15 | // Validate is ... 16 | func (v Install) Validate() error { 17 | return validation.ValidateStruct(&v, 18 | validation.Field(&v.Email, validation.Required, is.Email), 19 | validation.Field(&v.Password, validation.Required, validation.Length(6, 72)), 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/h3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/rocket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/social/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/just_docker-compose_example.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | image: nginx:alpine 5 | restart: always 6 | volumes: 7 | - "./nginx.conf:/etc/nginx/conf.d/default.conf" 8 | 9 | mailhog: 10 | image: jcalonso/mailhog 11 | restart: always 12 | ports: 13 | - "8025:8025" 14 | litecart: 15 | container_name: litecart 16 | restart: unless-stopped 17 | ports: 18 | - 8080:8080 19 | volumes: 20 | - ./lc_base:/lc_base 21 | - ./lc_digitals:/lc_digitals 22 | - ./lc_uploads:/lc_uploads 23 | - ./site:/site 24 | image: shurco/litecart:latest 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | commit-message: 9 | prefix: "🔧 build" 10 | include: "scope" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | commit-message: 17 | prefix: "🔧 build" 18 | include: "scope" 19 | 20 | - package-ecosystem: "npm" 21 | directory: "/web" 22 | schedule: 23 | interval: daily 24 | open-pull-requests-limit: 10 25 | commit-message: 26 | prefix: "🔧 build" 27 | include: "scope" 28 | -------------------------------------------------------------------------------- /.github/media/platforms/docker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/docs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/glob-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/blockquote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/money.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/user-group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/social/other.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/admin/src/components/Badge.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /internal/middleware/fiber.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gofiber/contrib/fiberzerolog" 5 | "github.com/gofiber/fiber/v2" 6 | "github.com/gofiber/fiber/v2/middleware/compress" 7 | "github.com/gofiber/fiber/v2/middleware/cors" 8 | "github.com/gofiber/fiber/v2/middleware/helmet" 9 | "github.com/gofiber/fiber/v2/middleware/recover" 10 | "github.com/rs/zerolog" 11 | ) 12 | 13 | // FiberMiddleware is ... 14 | func Fiber(a *fiber.App, log *zerolog.Logger) { 15 | a.Use(cors.New()) 16 | a.Use(helmet.New()) 17 | a.Use(compress.New(compress.Config{ 18 | Level: compress.LevelBestSpeed, 19 | })) 20 | a.Use(fiberzerolog.New(fiberzerolog.Config{ 21 | Logger: log, 22 | })) 23 | a.Use(recover.New()) 24 | } 25 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | labels: 3 | - label: '📒 Documentation' 4 | matcher: 5 | title: '\b(docs|doc:|\[doc\]|README|typos|comment|documentation)\b' 6 | - label: '☢️ Bug' 7 | matcher: 8 | title: '\b(fix|race|bug|missing|correct)\b' 9 | - label: '🧹 Updates' 10 | matcher: 11 | title: '\b(improve|update|refactor|deprecated|remove|unused|test)\b' 12 | - label: '🤖 Dependencies' 13 | matcher: 14 | title: '\b(bumb|bdependencies)\b' 15 | - label: '✏️ Feature' 16 | matcher: 17 | title: '\b(feature|feat|create|implement|add)\b' 18 | - label: '🤔 Question' 19 | matcher: 20 | title: '\b(question|how)\b' -------------------------------------------------------------------------------- /internal/models/pages.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import validation "github.com/go-ozzo/ozzo-validation/v4" 4 | 5 | // Page is ... 6 | type Page struct { 7 | Core 8 | Name string `json:"name"` 9 | Slug string `json:"slug"` 10 | Position string `json:"position,omitempty"` 11 | Content *string `json:"content,omitempty"` 12 | Active bool `json:"active"` 13 | Seo *Seo `json:"seo,omitempty"` 14 | } 15 | 16 | // Validate is ... 17 | func (v Page) Validate() error { 18 | return validation.ValidateStruct(&v, 19 | validation.Field(&v.ID, validation.Length(15, 15)), 20 | validation.Field(&v.Name, validation.Length(3, 50)), 21 | validation.Field(&v.Slug, validation.Length(3, 20)), 22 | validation.Field(&v.Seo), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/jwtutil/generator.go: -------------------------------------------------------------------------------- 1 | package jwtutil 2 | 3 | import ( 4 | "github.com/golang-jwt/jwt/v5" 5 | ) 6 | 7 | type Setting struct { 8 | Secret string 9 | SecretExpireHours int 10 | } 11 | 12 | // GenerateNewToken func for generate a new Access token. 13 | func GenerateNewToken(secret, id string, expires int64, credentials []string) (string, error) { 14 | claims := jwt.MapClaims{} 15 | claims["id"] = id 16 | claims["expires"] = expires 17 | 18 | for _, credential := range credentials { 19 | claims[credential] = true 20 | } 21 | 22 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 23 | accessToken, err := token.SignedString([]byte(secret)) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | return accessToken, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/testutil/testdir.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | // WithCmdTestDir changes CWD to tmp/test/ and returns a cleanup func. 10 | // All relative artifacts (e.g., ./lc_base, ./lc_uploads) will be created inside it. 11 | func WithCmdTestDir(t *testing.T) func() { 12 | t.Helper() 13 | oldwd, _ := os.Getwd() 14 | base := filepath.Join(oldwd, "tmp", "test", t.Name()) 15 | if err := os.MkdirAll(base, 0o775); err != nil { 16 | t.Fatalf("failed to create %s: %v", base, err) 17 | } 18 | if err := os.Chdir(base); err != nil { 19 | t.Fatalf("failed to chdir to %s: %v", base, err) 20 | } 21 | return func() { 22 | _ = os.Chdir(oldwd) 23 | _ = os.RemoveAll(filepath.Join(oldwd, "tmp", "test", t.Name())) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/handlers/public/page.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | 6 | "github.com/shurco/litecart/internal/queries" 7 | "github.com/shurco/litecart/pkg/errors" 8 | "github.com/shurco/litecart/pkg/logging" 9 | "github.com/shurco/litecart/pkg/webutil" 10 | ) 11 | 12 | // Page is ... 13 | // [get] /api/page/:page_slug 14 | func Page(c *fiber.Ctx) error { 15 | pageSlug := c.Params("page_slug") 16 | log := logging.New() 17 | db := queries.DB() 18 | 19 | page, err := db.Page(c.Context(), pageSlug) 20 | if err != nil { 21 | if err == errors.ErrPageNotFound { 22 | return webutil.StatusNotFound(c) 23 | } 24 | log.ErrorStack(err) 25 | return webutil.StatusInternalServerError(c) 26 | } 27 | 28 | return webutil.Response(c, fiber.StatusOK, "Page content", page) 29 | } 30 | -------------------------------------------------------------------------------- /internal/routes/not_found_route.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | 8 | "github.com/shurco/litecart/internal/queries" 9 | "github.com/shurco/litecart/pkg/webutil" 10 | ) 11 | 12 | // NotFoundRoute func for describe 404 Error route. 13 | func NotFoundRoute(a *fiber.App, noSite bool) { 14 | a.Use(func(c *fiber.Ctx) error { 15 | db := queries.DB() 16 | if db.IsPage(c.Context(), c.Path()[1:]) { 17 | return c.Render("pages", nil, "layouts/main") 18 | } 19 | 20 | if strings.HasPrefix(c.Path(), "/api") { 21 | return webutil.StatusNotFound(c) 22 | } 23 | if strings.HasPrefix(c.Path(), "/_") { 24 | return c.Next() 25 | } 26 | 27 | if noSite { 28 | return c.Next() 29 | } 30 | return c.Status(fiber.StatusNotFound).Render("404", nil, "layouts/clear") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/security/random.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "fmt" 7 | "math/big" 8 | "time" 9 | ) 10 | 11 | const ( 12 | DefaultIdLength = 15 13 | DefaultIdAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789" 14 | ) 15 | 16 | func RandomString() string { 17 | b := make([]byte, DefaultIdLength) 18 | max := big.NewInt(int64(len(DefaultIdAlphabet))) 19 | 20 | for i := range b { 21 | n, err := rand.Int(rand.Reader, max) 22 | if err != nil { 23 | // fallback: derive pseudo-random index from time hash 24 | h := sha256.Sum256([]byte(fmt.Sprintf("%d-%d", time.Now().UnixNano(), i))) 25 | idx := int(h[i%len(h)]) % len(DefaultIdAlphabet) 26 | if idx < 0 { 27 | idx = -idx 28 | } 29 | b[i] = DefaultIdAlphabet[idx] 30 | continue 31 | } 32 | b[i] = DefaultIdAlphabet[n.Int64()] 33 | } 34 | 35 | return string(b) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/litepay/litepay.go: -------------------------------------------------------------------------------- 1 | package litepay 2 | 3 | type Status string 4 | 5 | const ( 6 | NEW Status = "new" 7 | UNPAID Status = "unpaid" 8 | PAID Status = "paid" 9 | CANCELED Status = "canceled" 10 | FAILED Status = "failed" 11 | PROCESSED Status = "processed" 12 | TEST Status = "test" 13 | ) 14 | 15 | type Cfg struct { 16 | paymentSystem PaymentSystem 17 | api string // API path 18 | currency []string // support currency 19 | callbackURL string 20 | successURL string 21 | cancelURL string 22 | } 23 | 24 | type LitePay interface { 25 | Pay(cart Cart) (*Payment, error) 26 | Checkout(payment *Payment, session string) (*Payment, error) 27 | } 28 | 29 | func New(callbackURL, successURL, cancelURL string) Cfg { 30 | return Cfg{ 31 | callbackURL: callbackURL, 32 | successURL: successURL, 33 | cancelURL: cancelURL, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/init.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/shurco/litecart/internal/base" 5 | "github.com/shurco/litecart/migrations" 6 | "github.com/shurco/litecart/pkg/fsutil" 7 | ) 8 | 9 | // Init is ... 10 | func Init() error { 11 | dirsToCheck := []struct { 12 | path string 13 | name string 14 | }{ 15 | {"./lc_uploads", "lc_uploads"}, 16 | {"./lc_digitals", "lc_digitals"}, 17 | } 18 | 19 | for _, dir := range dirsToCheck { 20 | if err := fsutil.MkDirs(0o775, dir.path); err != nil { 21 | log.Err(err).Send() 22 | return err 23 | } 24 | } 25 | 26 | if _, err := base.New("./lc_base/data.db", migrations.Embed()); err != nil { 27 | log.Err(err).Send() 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | 34 | // Migrate is ... 35 | func Migrate() error { 36 | if err := base.Migrate("./lc_base/data.db", migrations.Embed()); err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/social/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/fsutil/embed.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "strings" 7 | ) 8 | 9 | // EmbedExtract is ... 10 | func EmbedExtract(fileSystem embed.FS, folder string) error { 11 | return fs.WalkDir(fileSystem, ".", func(a_file_path string, a_file_info fs.DirEntry, parent_err error) error { 12 | if parent_err != nil { 13 | return parent_err 14 | } 15 | 16 | // skip any directories entries, we just want to add files 17 | if a_file_info.IsDir() { 18 | return nil 19 | } 20 | 21 | if strings.HasPrefix(a_file_path, folder) { 22 | file, err := OpenFile(a_file_path, FsCWFlags, 0666) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | fileContent, err := fileSystem.ReadFile(a_file_path) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | if _, err := WriteOSFile(file, fileContent); err != nil { 33 | return err 34 | } 35 | } 36 | 37 | return nil 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/update/release.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | type release struct { 9 | Id int `json:"id"` 10 | Name string `json:"name"` 11 | Tag string `json:"tag_name"` 12 | Published string `json:"published_at"` 13 | Url string `json:"html_url"` 14 | Assets []*ReleaseAsset `json:"assets"` 15 | } 16 | 17 | type ReleaseAsset struct { 18 | Id int `json:"id"` 19 | Name string `json:"name"` 20 | Size int `json:"size"` 21 | DownloadUrl string `json:"browser_download_url"` 22 | } 23 | 24 | func (r *release) findAssetBySuffix(suffix string) (*ReleaseAsset, error) { 25 | if suffix != "" { 26 | for _, asset := range r.Assets { 27 | if strings.HasSuffix(asset.Name, suffix) { 28 | return asset, nil 29 | } 30 | } 31 | } 32 | 33 | return nil, errors.New("missing asset containing " + suffix) 34 | } 35 | -------------------------------------------------------------------------------- /internal/models/cart.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/shurco/litecart/pkg/litepay" 4 | 5 | // Cart is ... 6 | type Cart struct { 7 | Core 8 | Email string `json:"email"` 9 | Cart []CartProduct `json:"cart,omitempty"` 10 | AmountTotal int `json:"amount_total"` 11 | Currency string `json:"currency"` 12 | PaymentID string `json:"payment_id"` 13 | PaymentStatus litepay.Status `json:"payment_status"` 14 | PaymentSystem litepay.PaymentSystem `json:"payment_system"` 15 | } 16 | 17 | // CartProduct is ... 18 | type CartProduct struct { 19 | ProductID string `json:"id"` 20 | Quantity int `json:"quantity"` 21 | } 22 | 23 | // CartPayment is ... 24 | type CartPayment struct { 25 | Email string `json:"email"` 26 | Provider litepay.PaymentSystem `json:"provider"` 27 | Products []CartProduct `json:"products"` 28 | } 29 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/orderedlist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /internal/models/auth.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | validation "github.com/go-ozzo/ozzo-validation/v4" 5 | "github.com/go-ozzo/ozzo-validation/v4/is" 6 | ) 7 | 8 | // SignIn is ... 9 | type SignIn struct { 10 | Email string `json:"email"` 11 | Password string `json:"password"` 12 | } 13 | 14 | // Validate is ... 15 | func (v SignIn) Validate() error { 16 | return validation.ValidateStruct(&v, 17 | validation.Field(&v.Email, validation.Required, is.Email), 18 | validation.Field(&v.Password, validation.Required, validation.Length(6, 72)), 19 | ) 20 | } 21 | 22 | // JWT iss ... 23 | type JWT struct { 24 | Secret string `json:"secret"` 25 | ExpireHours int `json:"expire_hours"` 26 | } 27 | 28 | // Validate is ... 29 | func (v JWT) Validate() error { 30 | return validation.ValidateStruct(&v, 31 | validation.Field(&v.Secret, validation.Length(30, 100)), 32 | validation.Field(&v.ExpireHours, validation.Length(30, 100)), 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | const ( 6 | MsgNotFound = "not found" 7 | MsgWrongPassword = "wrong password" 8 | 9 | MsgUserNotFound = "user not found" 10 | MsgUserPasswordNotFound = "not found user password" 11 | MsgUserEmailNotFound = "user with the given email is not found" 12 | 13 | MsgProductNotFound = "product not found" 14 | MsgPageNotFound = "page not found" 15 | MsgSettingNotFound = "setting not found" 16 | ) 17 | 18 | var ( 19 | ErrNotFound = errors.New(MsgNotFound) 20 | ErrWrongPassword = errors.New(MsgWrongPassword) 21 | 22 | ErrUserNotFound = errors.New(MsgUserNotFound) 23 | ErrUserPasswordNotFound = errors.New(MsgUserPasswordNotFound) 24 | ErrUserEmailNotFound = errors.New(MsgUserEmailNotFound) 25 | 26 | ErrProductNotFound = errors.New(MsgProductNotFound) 27 | ErrPageNotFound = errors.New(MsgPageNotFound) 28 | ErrSettingNotFound = errors.New(MsgSettingNotFound) 29 | ) 30 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Security Issues 4 | 5 | We take the security of litecart code, software, and cloud platform very seriously. If you believe you have found a security vulnerability in litecart, we encourage you to let us know right away. We will investigate all legitimate reports and do our best to quickly fix the problem. 6 | 7 | Please report any issues, instead of posting a public issue in GitHub. Please include as much information as possible in your report to better help us understand and resolve the issue: 8 | 9 | - Where the security issue exists (ie. litecart, infrastructure, etc.) 10 | - The type of issue (ex. SQL injection, cross-site scripting, missing authorization, etc.) 11 | - Full paths or links to the source files where the security issue exists, if possible 12 | - Any special configuration required to reproduce the issue 13 | - Step-by-step instructions to reproduce the issue 14 | - Proof of concept or exploit code, if available -------------------------------------------------------------------------------- /web/site/layouts/clear.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | litecart 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {# embed #} 16 | 17 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pkg/update/update_test.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import "testing" 4 | 5 | func TestCompareVersions(t *testing.T) { 6 | tests := []struct { 7 | a, b string 8 | want int 9 | }{ 10 | {"0.1.0", "0.1.0", 0}, 11 | {"0.2.0", "0.1.0", -1}, 12 | {"0.1.0", "0.2.0", 1}, 13 | {"1.0.0", "0.9.9", -1}, 14 | {"0.10.0", "0.2.5", -1}, 15 | {"0.2", "0.2.1", 1}, 16 | } 17 | for _, tt := range tests { 18 | got := compareVersions(tt.a, tt.b) 19 | if got != tt.want { 20 | t.Fatalf("compareVersions(%s,%s)=%d want %d", tt.a, tt.b, got, tt.want) 21 | } 22 | } 23 | } 24 | 25 | func TestArchiveSuffix(t *testing.T) { 26 | if got := archiveSuffix("linux", "amd64"); got == "" { 27 | t.Fatal("expected suffix for linux/amd64") 28 | } 29 | if got := archiveSuffix("windows", "arm64"); got == "" { 30 | t.Fatal("expected suffix for windows/arm64") 31 | } 32 | if got := archiveSuffix("plan9", "amd64"); got != "" { 33 | t.Fatal("expected empty suffix for unsupported os") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/archive/archive.go: -------------------------------------------------------------------------------- 1 | package archive 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // Archive is ... 12 | type Archive interface { 13 | Directory(name string) error 14 | Header(os.FileInfo) (io.Writer, error) 15 | Close() error 16 | } 17 | 18 | func extractFile(path string, mode os.FileMode, data io.Reader, dest string) error { 19 | target := filepath.Join(dest, filepath.FromSlash(path)) 20 | if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) { 21 | return fmt.Errorf("path %q escapes archive destination", target) 22 | } 23 | 24 | if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { 25 | return err 26 | } 27 | 28 | file, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) 29 | if err != nil { 30 | return err 31 | } 32 | if _, err := io.Copy(file, data); err != nil { 33 | _ = file.Close() 34 | _ = os.Remove(target) 35 | return err 36 | } 37 | return file.Close() 38 | } 39 | -------------------------------------------------------------------------------- /internal/handlers/private/install.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | 6 | "github.com/shurco/litecart/internal/models" 7 | "github.com/shurco/litecart/internal/queries" 8 | "github.com/shurco/litecart/pkg/logging" 9 | "github.com/shurco/litecart/pkg/webutil" 10 | ) 11 | 12 | // Install is ... 13 | // [post] /api/install 14 | func Install(c *fiber.Ctx) error { 15 | db := queries.DB() 16 | log := logging.New() 17 | request := new(models.Install) 18 | 19 | if err := c.BodyParser(request); err != nil { 20 | log.ErrorStack(err) 21 | return webutil.StatusBadRequest(c, err.Error()) 22 | } 23 | 24 | if err := request.Validate(); err != nil { 25 | log.ErrorStack(err) 26 | return webutil.StatusBadRequest(c, err.Error()) 27 | } 28 | 29 | if err := db.Install(c.Context(), request); err != nil { 30 | log.ErrorStack(err) 31 | return webutil.StatusInternalServerError(c) 32 | } 33 | 34 | return webutil.Response(c, fiber.StatusOK, "Cart installed", nil) 35 | } 36 | -------------------------------------------------------------------------------- /internal/handlers/private/cart.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/shurco/litecart/internal/mailer" 6 | "github.com/shurco/litecart/internal/queries" 7 | "github.com/shurco/litecart/pkg/logging" 8 | "github.com/shurco/litecart/pkg/webutil" 9 | ) 10 | 11 | // Carts is ... 12 | // [get] /api/_/carts 13 | func Carts(c *fiber.Ctx) error { 14 | db := queries.DB() 15 | log := logging.New() 16 | 17 | products, err := db.Carts(c.Context()) 18 | if err != nil { 19 | log.ErrorStack(err) 20 | return webutil.StatusInternalServerError(c) 21 | } 22 | 23 | return webutil.Response(c, fiber.StatusOK, "Carts", products) 24 | } 25 | 26 | // CartSendMail 27 | // [post] /api/_/carts/:cart_id/mail 28 | func CartSendMail(c *fiber.Ctx) error { 29 | cartID := c.Params("cart_id") 30 | log := logging.New() 31 | 32 | if err := mailer.SendCartLetter(cartID); err != nil { 33 | log.ErrorStack(err) 34 | return webutil.StatusInternalServerError(c) 35 | } 36 | 37 | return webutil.Response(c, fiber.StatusOK, "Mail sended", nil) 38 | } 39 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/social/dribbble.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/handlers/public/product.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | 6 | "github.com/shurco/litecart/internal/queries" 7 | "github.com/shurco/litecart/pkg/logging" 8 | "github.com/shurco/litecart/pkg/webutil" 9 | ) 10 | 11 | // Products is ... 12 | // [get] /api/products 13 | func Products(c *fiber.Ctx) error { 14 | db := queries.DB() 15 | log := logging.New() 16 | 17 | products, err := db.ListProducts(c.Context(), false) 18 | if err != nil { 19 | log.ErrorStack(err) 20 | return webutil.StatusInternalServerError(c) 21 | } 22 | 23 | return webutil.Response(c, fiber.StatusOK, "Products", products) 24 | } 25 | 26 | // GetProduct is ... 27 | // [get] /api/products/:product_id 28 | func Product(c *fiber.Ctx) error { 29 | productID := c.Params("product_id") 30 | db := queries.DB() 31 | log := logging.New() 32 | 33 | product, err := db.Product(c.Context(), false, productID) 34 | if err != nil { 35 | log.ErrorStack(err) 36 | return webutil.StatusInternalServerError(c) 37 | } 38 | 39 | return webutil.Response(c, fiber.StatusOK, "Product info", product) 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | pnpm-debug.log* 27 | lerna-debug.log* 28 | 29 | node_modules 30 | .DS_Store 31 | dist 32 | dist-ssr 33 | coverage 34 | *.local 35 | 36 | # Go workspace file 37 | go.work 38 | 39 | cmd/__debug_bin* 40 | cmd/main 41 | cmd/cmd 42 | cmd/lc_*/ 43 | cmd/site 44 | fixtures/migration/*_private.sql 45 | .DS_Store 46 | 47 | web/site/public/assets/js/vue.js 48 | web/site/public/assets/js/nprogress.js 49 | web/site/public/assets/js/vue-demi.js 50 | web/site/public/assets/js/pinia.js 51 | web/site/public/assets/css/nprogress.css 52 | 53 | /main.go -------------------------------------------------------------------------------- /pkg/archive/tar_test.go: -------------------------------------------------------------------------------- 1 | package archive 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | func TestExtractTar(t *testing.T) { 13 | dir := t.TempDir() 14 | // create a small tar.gz archive in-memory 15 | var buf bytes.Buffer 16 | gzw := gzip.NewWriter(&buf) 17 | tw := tar.NewWriter(gzw) 18 | 19 | content := []byte("hello") 20 | hdr := &tar.Header{Name: "a/b/c.txt", Mode: 0644, Size: int64(len(content))} 21 | if err := tw.WriteHeader(hdr); err != nil { 22 | t.Fatal(err) 23 | } 24 | if _, err := tw.Write(content); err != nil { 25 | t.Fatal(err) 26 | } 27 | _ = tw.Close() 28 | _ = gzw.Close() 29 | 30 | // write to a temporary file 31 | src := filepath.Join(dir, "test.tar.gz") 32 | if err := os.WriteFile(src, buf.Bytes(), 0o644); err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | // extract 37 | if err := ExtractTar(src, filepath.Join(dir, "out")); err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | data, err := os.ReadFile(filepath.Join(dir, "out", "a", "b", "c.txt")) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if string(data) != "hello" { 46 | t.Fatalf("unexpected content: %q", string(data)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/routes/site_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | 6 | handlers "github.com/shurco/litecart/internal/handlers/public" 7 | "github.com/shurco/litecart/internal/queries" 8 | ) 9 | 10 | // SiteRoutes is ... 11 | func SiteRoutes(c *fiber.App) { 12 | c.Get("/", func(c *fiber.Ctx) error { 13 | return c.Render("index", nil, "layouts/main") 14 | }) 15 | 16 | // catalog section 17 | c.Get("/products/:product_slug", func(c *fiber.Ctx) error { 18 | productSlug := c.Params("product_slug") 19 | db := queries.DB() 20 | 21 | if !db.IsProduct(c.Context(), productSlug) { 22 | return c.Status(fiber.StatusNotFound).Render("404", fiber.Map{}, "layouts/clear") 23 | } 24 | 25 | return c.Render("product", fiber.Map{ 26 | "ProductSlug": productSlug, 27 | }, "layouts/main") 28 | }) 29 | 30 | // cart section 31 | c.Get("/cart", func(c *fiber.Ctx) error { 32 | return c.Render("cart", nil, "layouts/main") 33 | }) 34 | 35 | payment := c.Group("/cart/payment") 36 | payment.Post("/", handlers.Payment) 37 | payment.Post("/callback", handlers.PaymentCallback) 38 | payment.Get("/success", handlers.PaymentSuccess) 39 | payment.Get("/cancel", handlers.PaymentCancel) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/jwtutil/parser.go: -------------------------------------------------------------------------------- 1 | package jwtutil 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/golang-jwt/jwt/v5" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | // TokenMetadata struct to describe metadata in JWT. 10 | type TokenMetadata struct { 11 | ID string 12 | Expires int64 13 | } 14 | 15 | // ExtractTokenMetadata func to extract metadata from JWT. 16 | func ExtractTokenMetadata(c *fiber.Ctx, secret string) (*TokenMetadata, error) { 17 | token, err := verifyToken(c, secret) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | claims, ok := token.Claims.(jwt.MapClaims) 23 | if ok && token.Valid { 24 | id, err := uuid.Parse(claims["id"].(string)) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | expires := int64(claims["expires"].(float64)) 30 | return &TokenMetadata{ 31 | ID: id.String(), 32 | Expires: expires, 33 | }, nil 34 | } 35 | 36 | return nil, err 37 | } 38 | 39 | func verifyToken(c *fiber.Ctx, secret string) (*jwt.Token, error) { 40 | token, err := jwt.Parse(c.Cookies("token"), func(token *jwt.Token) (interface{}, error) { 41 | return []byte(secret), nil 42 | }) 43 | 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return token, nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/webhook/payment_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "sync/atomic" 8 | "testing" 9 | 10 | "github.com/shurco/litecart/pkg/litepay" 11 | ) 12 | 13 | func TestSendPaymentHook(t *testing.T) { 14 | var got atomic.Value 15 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | defer r.Body.Close() 17 | var p Payment 18 | if err := json.NewDecoder(r.Body).Decode(&p); err != nil { 19 | w.WriteHeader(400) 20 | return 21 | } 22 | got.Store(p) 23 | w.WriteHeader(200) 24 | })) 25 | defer srv.Close() 26 | 27 | p := &Payment{ 28 | Event: PAYMENT_INITIATION, 29 | Data: Data{PaymentSystem: litepay.STRIPE, PaymentStatus: litepay.NEW}, 30 | } 31 | resp, err := Send(srv.URL, mustJSON(p)) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | if resp.StatusCode != 200 { 36 | t.Fatalf("status %d", resp.StatusCode) 37 | } 38 | 39 | v := got.Load() 40 | if v == nil { 41 | t.Fatalf("no payload captured") 42 | } 43 | pay := v.(Payment) 44 | if pay.Event != PAYMENT_INITIATION { 45 | t.Fatalf("unexpected event") 46 | } 47 | } 48 | 49 | func mustJSON(v any) []byte { 50 | b, _ := json.Marshal(v) 51 | return b 52 | } 53 | -------------------------------------------------------------------------------- /web/admin/src/components/form/Button.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | 33 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/booth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /migrations/20240111145752_new_sicials.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | INSERT OR IGNORE INTO setting VALUES ('yLR1176FQj1BQks', 'social_facebook', ''); 4 | INSERT OR IGNORE INTO setting VALUES ('rKVq63So91kMuN7', 'social_instagram', ''); 5 | INSERT OR IGNORE INTO setting VALUES ('NVv27ea47Yo7gPm', 'social_twitter', ''); 6 | INSERT OR IGNORE INTO setting VALUES ('VjdMVG7LcUL274G', 'social_dribbble', ''); 7 | INSERT OR IGNORE INTO setting VALUES ('8sz9yVDNvNBa97b', 'social_github', ''); 8 | INSERT OR IGNORE INTO setting VALUES ('CoDDXfxF4GZxq6b', 'social_youtube', ''); 9 | INSERT OR IGNORE INTO setting VALUES ('AC3of7o9pS9HdB1', 'social_other', ''); 10 | 11 | -- Fix existing smtp_port values that might be '0' 12 | UPDATE setting SET value = '' WHERE key = 'smtp_port' AND value = '0'; 13 | -- +goose StatementEnd 14 | 15 | -- +goose Down 16 | -- +goose StatementBegin 17 | DELETE FROM setting WHERE id = 'CoDDXfxF4GZxq6b'; 18 | DELETE FROM setting WHERE id = 'AC3of7o9pS9HdB1'; 19 | DELETE FROM setting WHERE id = '8sz9yVDNvNBa97b'; 20 | DELETE FROM setting WHERE id = 'VjdMVG7LcUL274G'; 21 | DELETE FROM setting WHERE id = 'NVv27ea47Yo7gPm'; 22 | DELETE FROM setting WHERE id = 'rKVq63So91kMuN7'; 23 | DELETE FROM setting WHERE id = 'yLR1176FQj1BQks'; 24 | -- +goose StatementEnd -------------------------------------------------------------------------------- /pkg/security/password.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | 7 | "golang.org/x/crypto/bcrypt" 8 | ) 9 | 10 | // NormalizePassword func for a returning the users input as a byte slice. 11 | func NormalizePassword(p string) []byte { 12 | return []byte(p) 13 | } 14 | 15 | // GeneratePassword func for a making hash & salt with user password. 16 | func GeneratePassword(p string) string { 17 | bytePwd := NormalizePassword(p) 18 | 19 | hash, err := bcrypt.GenerateFromPassword(bytePwd, bcrypt.MinCost) 20 | if err != nil { 21 | return err.Error() 22 | } 23 | 24 | return string(hash) 25 | } 26 | 27 | // ComparePasswords func for a comparing password. 28 | func ComparePasswords(hashedPwd, inputPwd string) bool { 29 | byteHash := NormalizePassword(hashedPwd) 30 | byteInput := NormalizePassword(inputPwd) 31 | 32 | if err := bcrypt.CompareHashAndPassword(byteHash, byteInput); err != nil { 33 | return false 34 | } 35 | 36 | return true 37 | } 38 | 39 | // NewToken ... 40 | func NewToken(text string) (string, error) { 41 | hash, err := bcrypt.GenerateFromPassword([]byte(text), bcrypt.DefaultCost) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | newMD5 := md5.New() 47 | newMD5.Write(hash) 48 | return hex.EncodeToString(newMD5.Sum(nil)), nil 49 | } 50 | -------------------------------------------------------------------------------- /web/admin/src/pages/settings/webhook.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 43 | -------------------------------------------------------------------------------- /web/admin/vite.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { fileURLToPath, URL } from "node:url"; 4 | 5 | import { defineConfig } from "vite"; 6 | import vue from "@vitejs/plugin-vue"; 7 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; 8 | import VueDevTools from 'vite-plugin-vue-devtools'; 9 | 10 | export default defineConfig({ 11 | //base: process.env.NODE_ENV === 'production' ? '/_/' : '/', 12 | 13 | resolve: { 14 | alias: { 15 | "@": fileURLToPath(new URL("./src", import.meta.url)), 16 | }, 17 | }, 18 | 19 | base: "/_/", 20 | 21 | server: { 22 | proxy: { 23 | "/api": { 24 | target: "http://localhost:8080/", 25 | }, 26 | "/uploads": { 27 | target: "http://localhost:8080/", 28 | }, 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | VueDevTools(), 34 | vue(), 35 | createSvgIconsPlugin({ 36 | iconDirs: [path.resolve(process.cwd(), "./src/assets/svg")], 37 | symbolId: "icon-[dir]-[name]", 38 | }), 39 | ], 40 | 41 | 42 | build: { 43 | rollupOptions: { 44 | output: { 45 | manualChunks(id) { 46 | if (id.includes('node_modules')) { 47 | return id.toString().split('node_modules/')[1].split('/')[0].toString(); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /internal/queries/auth.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/shurco/litecart/pkg/errors" 8 | ) 9 | 10 | // AuthQueries is a struct that embeds *sql.DB to provide database functionality. 11 | // This structure can be used to create methods that will execute SQL queries related to authentication. 12 | type AuthQueries struct { 13 | *sql.DB 14 | } 15 | 16 | // GetPasswordByEmail retrieves the password for a user by their email. 17 | func (q *AuthQueries) GetPasswordByEmail(ctx context.Context, email string) (string, error) { 18 | query := `SELECT key, value FROM setting WHERE key IN ('email', 'password')` 19 | rows, err := q.DB.QueryContext(ctx, query) 20 | if err != nil { 21 | return "", err 22 | } 23 | defer func() { _ = rows.Close() }() 24 | 25 | for rows.Next() { 26 | var key, value string 27 | err := rows.Scan(&key, &value) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | switch key { 33 | case "email": 34 | if value != email { 35 | return "", errors.ErrUserEmailNotFound 36 | } 37 | case "password": 38 | if value == "" { 39 | return "", errors.ErrUserPasswordNotFound 40 | } 41 | return value, nil 42 | } 43 | } 44 | 45 | if err := rows.Err(); err != nil { 46 | return "", err 47 | } 48 | 49 | return "", errors.ErrUserNotFound 50 | } 51 | -------------------------------------------------------------------------------- /web/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "litecart-admin", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "VITE_CJS_IGNORE_WARNING=true vite --host", 7 | "build": "VITE_CJS_IGNORE_WARNING=true vite build --mode production", 8 | "preview": "vite preview --host", 9 | "update": "bun update" 10 | }, 11 | "dependencies": { 12 | "@tiptap/extension-link": "^2.26.3", 13 | "@tiptap/extension-placeholder": "^2.26.3", 14 | "@tiptap/pm": "^2.26.3", 15 | "@tiptap/starter-kit": "^2.26.3", 16 | "@tiptap/vue-3": "^2.26.3", 17 | "@vee-validate/rules": "^4.15.1", 18 | "notiwind": "^2.1.0", 19 | "nprogress": "^0.2.0", 20 | "pinia": "^3.0.3", 21 | "vee-validate": "^4.15.1", 22 | "vue": "^3.5.22", 23 | "vue-router": "^4.6.3", 24 | "vue-tippy": "^6.7.1" 25 | }, 26 | "devDependencies": { 27 | "@tailwindcss/forms": "=0.5.9", 28 | "@vitejs/plugin-vue": "^6.0.1", 29 | "autoprefixer": "^10.4.21", 30 | "postcss": "^8.5.6", 31 | "postcss-import": "^16.1.1", 32 | "postcss-nesting": "^13.0.2", 33 | "prettier": "^3.6.2", 34 | "prettier-plugin-tailwindcss": "^0.6.14", 35 | "sass": "^1.93.2", 36 | "tailwindcss": "=3.4.13", 37 | "vite": "^7.1.11", 38 | "vite-plugin-svg-icons": "^2.0.1", 39 | "vite-plugin-vue-devtools": "^7.7.7" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/base/base.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "database/sql" 5 | "embed" 6 | "fmt" 7 | 8 | "github.com/pressly/goose/v3" 9 | "github.com/shurco/litecart/pkg/fsutil" 10 | ) 11 | 12 | // New is ... 13 | func New(dbPath string, migrations embed.FS) (db *sql.DB, err error) { 14 | if !fsutil.IsFile(dbPath) { 15 | // create db 16 | if _, err = fsutil.OpenFile(dbPath, fsutil.FsCWFlags, 0o666); err != nil { 17 | return 18 | } 19 | 20 | // first migrate db 21 | if err = Migrate(dbPath, migrations); err != nil { 22 | return 23 | } 24 | } 25 | 26 | // connect to database 27 | dsn := fmt.Sprintf("%s?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)", dbPath) 28 | db, err = sql.Open("sqlite", dsn) 29 | if err != nil { 30 | return 31 | } 32 | if _, err := db.Exec("PRAGMA auto_vacuum"); err != nil { 33 | return nil, err 34 | } 35 | 36 | return 37 | } 38 | 39 | // Migrate is ... 40 | func Migrate(dbPath string, migrations embed.FS) (err error) { 41 | goose.SetBaseFS(migrations) 42 | var db *sql.DB 43 | db, err = goose.OpenDBWithDriver("sqlite", dbPath) 44 | if err != nil { 45 | return 46 | } 47 | defer func() { _ = db.Close() }() 48 | 49 | goose.SetTableName("migrate_db_version") 50 | 51 | err = goose.Up(db, ".") 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /web/admin/src/pages/Signin.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /web/admin/src/utils/api/index.js: -------------------------------------------------------------------------------- 1 | import { start, done } from "nprogress"; 2 | 3 | export async function apiGet(url) { 4 | return handleRequest(url, { 5 | credentials: "include", 6 | method: "GET", 7 | }); 8 | } 9 | 10 | export async function apiPost(url, body) { 11 | const options = createOptions("POST", body); 12 | return handleRequest(url, options); 13 | } 14 | 15 | export async function apiUpdate(url, body) { 16 | const options = createOptions("PATCH", body); 17 | return handleRequest(url, options); 18 | } 19 | 20 | export async function apiDelete(url) { 21 | return handleRequest(url, { 22 | credentials: "include", 23 | method: "DELETE", 24 | }); 25 | } 26 | 27 | async function handleRequest(url, options) { 28 | try { 29 | start(); 30 | const response = await fetch(url, options); 31 | return response.json(); 32 | } catch (error) { 33 | console.error(error); 34 | } finally { 35 | done(); 36 | } 37 | } 38 | 39 | function createOptions(method, body) { 40 | const options = { 41 | credentials: "include", 42 | method, 43 | }; 44 | 45 | if (body) { 46 | if (Object.keys(body).length > 0) { 47 | options.body = JSON.stringify(body); 48 | options.headers = { 49 | "Content-Type": "application/json", 50 | }; 51 | } else { 52 | options.body = body; 53 | } 54 | } 55 | 56 | return options; 57 | } 58 | -------------------------------------------------------------------------------- /scripts/golang: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #set -e 4 | 5 | ROOT_PATH="$(git rev-parse --show-toplevel)" 6 | source ${ROOT_PATH}/scripts/_helper 7 | 8 | print_header "Install/Update Golang" 9 | 10 | get_latest_go_version() { 11 | wget -qO- "https://golang.org/dl/" | grep -v -E 'go[0-9\.]+(beta|rc)' | grep -E -o 'go[0-9\.]+' | grep -E -o '[0-9]\.[0-9]+(\.[0-9]+)?' | sort -V | uniq | tail -1 12 | } 13 | 14 | case "$OS" in 15 | Darwin*) 16 | brew install go 17 | ;; 18 | Linux*) 19 | support_arch 20 | GO_RELEASE=$(get_latest_go_version) 21 | GO_PATH="$HOME/go" 22 | 23 | #rm -rf $GO_PATH 24 | #mkdir -p $GO_PATH 25 | 26 | if ! grep -q "export GOROOT=\"\$HOME/go\"" ~/.bashrc; then 27 | echo -e "export GOROOT=\"\$HOME/go\"" >>~/.bashrc 28 | fi 29 | 30 | if ! grep -q "export GOPATH=\"\$HOME/go/packages\"" ~/.bashrc; then 31 | echo -e "export GOPATH=\"\$HOME/go/packages\"" >>~/.bashrc 32 | fi 33 | 34 | if ! grep -q "export PATH=\$PATH:\$GOROOT/bin:\$GOPATH/bin" ~/.bashrc; then 35 | echo -e "export PATH=\$PATH:\$GOROOT/bin:\$GOPATH/bin" >>~/.bashrc 36 | fi 37 | 38 | source ~/.bashrc 39 | 40 | curl --silent https://dl.google.com/go/go${GO_RELEASE}.linux-amd64.tar.gz | tar -vxz --strip-components 1 -C ${GO_PATH} >/dev/null 2>&1 41 | ;; 42 | *) 43 | print_answer "ERROR" red 44 | echo "Unsupported OS: $OS" 45 | exit 46 | ;; 47 | esac 48 | 49 | print_answer "SUCCESS" green 50 | -------------------------------------------------------------------------------- /pkg/litepay/cart.go: -------------------------------------------------------------------------------- 1 | package litepay 2 | 3 | import validation "github.com/go-ozzo/ozzo-validation/v4" 4 | 5 | type Cart struct { 6 | ID string `json:"id"` 7 | Currency string `json:"currency"` 8 | Items []Item `json:"items"` 9 | } 10 | 11 | type Item struct { 12 | PriceData Price `json:"price"` 13 | Quantity int `json:"quantity"` 14 | } 15 | 16 | type Price struct { 17 | UnitAmount int `json:"init_amount"` 18 | Product Product `json:"product"` 19 | } 20 | 21 | type Product struct { 22 | Name string `json:"name"` 23 | Description string `json:"description,omitempty"` 24 | Images []string `json:"images"` 25 | } 26 | 27 | type Payment struct { 28 | PaymentSystem PaymentSystem `json:"provider"` 29 | MerchantID string `json:"merchant_id"` 30 | CartID string `json:"cart_id"` 31 | AmountTotal int `json:"amount_total"` 32 | Currency string `json:"currency"` 33 | Status Status `json:"status"` 34 | URL string `json:"url,omitempty"` 35 | Coin *Coin `json:"coin,omitempty"` 36 | } 37 | 38 | // Validate is ... 39 | func (v Payment) Validate() error { 40 | return validation.ValidateStruct(&v, 41 | validation.Field(&v.CartID, validation.Length(15, 15)), 42 | ) 43 | } 44 | 45 | type Coin struct { 46 | AmountTotal float64 `json:"amount_total"` 47 | Currency string `json:"currency"` 48 | } 49 | -------------------------------------------------------------------------------- /pkg/fsutil/folder.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | // IsDir reports whether the named directory exists. 9 | func IsDir(path string) bool { 10 | if path == "" || len(path) > 468 { 11 | return false 12 | } 13 | 14 | if fi, err := os.Stat(path); err == nil { 15 | return fi.IsDir() 16 | } 17 | return false 18 | } 19 | 20 | // IsEmptyDir reports whether the named directory is empty. 21 | func IsEmptyDir(dirPath string) bool { 22 | f, err := os.Open(dirPath) 23 | if err != nil { 24 | return false 25 | } 26 | defer func() { _ = f.Close() }() 27 | 28 | _, err = f.Readdirnames(1) 29 | return err == io.EOF 30 | } 31 | 32 | // Workdir get 33 | func Workdir() string { 34 | dir, _ := os.Getwd() 35 | return dir 36 | } 37 | 38 | // MkDirs batch make multi dirs at once 39 | func MkDirs(perm os.FileMode, dirPaths ...string) error { 40 | for _, dirPath := range dirPaths { 41 | if !IsDir(dirPath) { 42 | if err := os.MkdirAll(dirPath, perm); err != nil { 43 | return err 44 | } 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | // MkSubDirs batch make multi sub-dirs at once 51 | func MkSubDirs(perm os.FileMode, parentDir string, subDirs ...string) error { 52 | for _, dirName := range subDirs { 53 | dirPath := parentDir + "/" + dirName 54 | if !IsDir(dirPath) { 55 | if err := os.MkdirAll(dirPath, perm); err != nil { 56 | return err 57 | } 58 | } 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /web/site/public/assets/js/form/button.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data(props) { 3 | return { 4 | colors: { 5 | green: ["bg-green-600", "bg-green-500"], 6 | yellow: ["bg-yellow-600", "bg-yellow-500"], 7 | red: ["bg-red-600", "bg-red-500"], 8 | blue: ["bg-blue-600", "bg-blue-500"], 9 | } 10 | } 11 | }, 12 | 13 | setup(props) { }, 14 | 15 | props: { 16 | color: { 17 | type: String, 18 | required: true 19 | }, 20 | name: { 21 | type: String, 22 | default: 'Name' 23 | }, 24 | ico: String, 25 | }, 26 | 27 | template: `` 44 | } 45 | 46 | -------------------------------------------------------------------------------- /web/admin/src/pages/settings/main.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | -------------------------------------------------------------------------------- /internal/queries/queries.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "database/sql" 5 | "embed" 6 | 7 | "github.com/shurco/litecart/internal/base" 8 | _ "modernc.org/sqlite" 9 | ) 10 | 11 | var db *Base 12 | 13 | // Define the structure 'Base' that aggregates various queries related to different modules like 14 | // settings, authentication, installation, pages, products, and cart management. 15 | type Base struct { 16 | SettingQueries 17 | AuthQueries 18 | InstallQueries 19 | PageQueries 20 | ProductQueries 21 | CartQueries 22 | } 23 | 24 | // New initializes the application's database and returns an error if any occurs during the process. 25 | // It takes an 'embed.FS' which represents the file system intended to be used with embedded files. 26 | func New(embed embed.FS) (err error) { 27 | var sqlite *sql.DB 28 | sqlite, err = base.New("./lc_base/data.db", embed) 29 | if err != nil { 30 | return 31 | } 32 | 33 | db = &Base{ 34 | AuthQueries: AuthQueries{DB: sqlite}, 35 | InstallQueries: InstallQueries{DB: sqlite}, 36 | SettingQueries: SettingQueries{DB: sqlite}, 37 | PageQueries: PageQueries{DB: sqlite}, 38 | ProductQueries: ProductQueries{DB: sqlite}, 39 | CartQueries: CartQueries{DB: sqlite}, 40 | } 41 | return 42 | } 43 | 44 | // DB is a function that ensures a singleton instance of 'Base' is always returned. 45 | // If 'db' is not already initialized, it initializes it before returning. 46 | func DB() *Base { 47 | if db == nil { 48 | db = &Base{} 49 | } 50 | return db 51 | } 52 | -------------------------------------------------------------------------------- /pkg/webutil/webutil.go: -------------------------------------------------------------------------------- 1 | package webutil 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/gofiber/utils" 6 | ) 7 | 8 | // HTTPResponse represents response body of API 9 | type HTTPResponse struct { 10 | Success bool `json:"success"` 11 | Message string `json:"message"` 12 | Result any `json:"result,omitempty"` 13 | } 14 | 15 | // Response is a takes in a Fiber context object, an HTTP status code, a message string and some data. 16 | func Response(c *fiber.Ctx, status int, message string, data any) error { 17 | if len(message) > 0 { 18 | return c.Status(status).JSON(HTTPResponse{ 19 | Success: status == fiber.StatusOK, 20 | Message: message, 21 | Result: data, 22 | }) 23 | } 24 | 25 | return c.Status(status).JSON(data) 26 | } 27 | 28 | // StatusOK is ... 29 | func StatusOK(c *fiber.Ctx, message string, data any) error { 30 | return Response(c, fiber.StatusOK, message, data) 31 | } 32 | 33 | // StatusNotFound is ... 34 | func StatusNotFound(c *fiber.Ctx) error { 35 | return Response(c, fiber.StatusNotFound, utils.StatusMessage(fiber.StatusNotFound), nil) 36 | } 37 | 38 | // StatusBadRequest is ... 39 | func StatusBadRequest(c *fiber.Ctx, data any) error { 40 | return Response(c, fiber.StatusBadRequest, utils.StatusMessage(fiber.StatusBadRequest), data) 41 | } 42 | 43 | // StatusInternalServerError is ... 44 | func StatusInternalServerError(c *fiber.Ctx) error { 45 | return Response(c, fiber.StatusInternalServerError, utils.StatusMessage(fiber.StatusInternalServerError), nil) 46 | } 47 | -------------------------------------------------------------------------------- /web/admin/src/components/index.js: -------------------------------------------------------------------------------- 1 | // form section 2 | export { default as FormButton } from "./form/Button.vue"; 3 | export { default as FormInput } from "./form/Input.vue"; 4 | export { default as FormSelect } from "./form/Select.vue"; 5 | export { default as FormTextarea } from "./form/Textarea.vue"; 6 | export { default as FormToggle } from "./form/Toggle.vue"; 7 | export { default as FormUpload } from "./form/Upload.vue"; 8 | 9 | // page section 10 | export { default as PageAdd } from "./page/Add.vue"; 11 | export { default as PageSeo } from "./page/Seo.vue"; 12 | export { default as PageUpdate } from "./page/Update.vue"; 13 | 14 | // product section 15 | export { default as ProductAdd } from "./product/Add.vue"; 16 | export { default as ProductDigital } from "./product/Digital.vue"; 17 | export { default as ProductSeo } from "./product/Seo.vue"; 18 | export { default as ProductUpdate } from "./product/Update.vue"; 19 | export { default as ProductView } from "./product/View.vue"; 20 | 21 | // product section 22 | export { default as Letter } from "./setting/Letter.vue"; 23 | export { default as Paypal } from "./setting/Paypal.vue"; 24 | export { default as Spectrocoin } from "./setting/Spectrocoin.vue"; 25 | export { default as Stripe } from "./setting/Stripe.vue"; 26 | 27 | // other section 28 | export { default as Alert } from "./Alert.vue"; 29 | export { default as Badge } from "./Badge.vue"; 30 | export { default as DetailList } from "./DetailList.vue"; 31 | export { default as Drawer } from "./Drawer.vue"; 32 | export { default as Editor } from "./Editor.vue"; 33 | -------------------------------------------------------------------------------- /web/admin/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createPinia } from "pinia"; 3 | import App from "@/App.vue"; 4 | import router from "@/router"; 5 | import Notifications from "notiwind"; 6 | import VueTippy from "vue-tippy"; 7 | 8 | import SvgIcon from "@/components/SvgIcon.vue"; 9 | import "virtual:svg-icons-register"; 10 | 11 | import { defineRule } from "vee-validate"; 12 | import * as rules from "@vee-validate/rules"; 13 | 14 | import "@/assets/app.css"; 15 | 16 | // validate rules 17 | Object.keys(rules) 18 | .filter(rule => typeof rules[rule] === "function") 19 | .forEach(rule => defineRule(rule, rules[rule])); 20 | defineRule("amount", (value) => { 21 | if (!value || !value.length) { 22 | return true; 23 | } 24 | if (!/^\d+(\.\d{1,2})?$/.test(value)) { 25 | return "amount is not valid"; 26 | } 27 | return true; 28 | }); 29 | defineRule("slug", (value) => { 30 | if (!value || !value.length) { 31 | return true; 32 | } 33 | if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) { 34 | return "slug is not valid"; 35 | } 36 | return true; 37 | }); 38 | defineRule("confirmed", (value, [target], ctx) => { 39 | if (value === ctx.form[target]) { 40 | return true; 41 | } 42 | return "Passwords must match"; 43 | }); 44 | 45 | const pinia = createPinia(); 46 | const app = createApp(App); 47 | app.use(pinia); 48 | app.use(router); 49 | app.use(Notifications); 50 | app.use(VueTippy, { 51 | defaultProps: { 52 | theme: 'lite', 53 | delay: [500, null], 54 | }, 55 | }); 56 | app.component("SvgIcon", SvgIcon); 57 | app.mount("#app"); 58 | -------------------------------------------------------------------------------- /web/admin/src/pages/Install.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | Explain the *details* for making this change. What existing problem does the pull request solve? 5 | 6 | Fixes # (issue) 7 | 8 | ## Type of change 9 | 10 | Please delete options that are not relevant. 11 | 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] New feature (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] This change requires a documentation update 16 | 17 | ## Checklist: 18 | 19 | - [ ] I have performed a self-review of my own code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] I have added tests that prove my fix is effective or that my feature works 23 | - [ ] New and existing unit tests pass locally with my changes 24 | - [ ] If new dependencies exist, I have checked that they are really necessary and agreed with the maintainers/community (we want to have as few dependencies as possible) 25 | - [ ] I tried to make my code as fast as possible with as few allocations as possible 26 | - [ ] For new code I have written benchmarks so that they can be analyzed and improved 27 | 28 | ## Commit formatting: 29 | 30 | Use emojis on commit messages so it provides an easy way of identifying the purpose or intention of a commit. Check out the emoji cheatsheet here: https://gitmoji.carloscuesta.me/ -------------------------------------------------------------------------------- /internal/handlers/public/setting.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | 6 | "github.com/shurco/litecart/internal/models" 7 | "github.com/shurco/litecart/internal/queries" 8 | "github.com/shurco/litecart/pkg/logging" 9 | "github.com/shurco/litecart/pkg/webutil" 10 | ) 11 | 12 | // Ping is ... 13 | // [get] /ping 14 | func Ping(c *fiber.Ctx) error { 15 | return webutil.Response(c, fiber.StatusOK, "Pong", nil) 16 | } 17 | 18 | // Settings is ... 19 | // [get] /api/settings 20 | func Settings(c *fiber.Ctx) error { 21 | db := queries.DB() 22 | log := logging.New() 23 | 24 | settingMain, err := queries.GetSettingByGroup[models.Main](c.Context(), db) 25 | if err != nil { 26 | log.ErrorStack(err) 27 | return webutil.StatusInternalServerError(c) 28 | } 29 | 30 | settingSocial, err := queries.GetSettingByGroup[models.Social](c.Context(), db) 31 | if err != nil { 32 | log.ErrorStack(err) 33 | return webutil.StatusInternalServerError(c) 34 | } 35 | 36 | settingPayment, err := queries.GetSettingByGroup[models.Payment](c.Context(), db) 37 | if err != nil { 38 | log.ErrorStack(err) 39 | return webutil.StatusInternalServerError(c) 40 | } 41 | 42 | pages, err := db.ListPages(c.Context(), false) 43 | if err != nil { 44 | log.ErrorStack(err) 45 | return webutil.StatusInternalServerError(c) 46 | } 47 | 48 | return webutil.Response(c, fiber.StatusOK, "Settings", map[string]any{ 49 | "main": map[string]string{ 50 | "site_name": settingMain.SiteName, 51 | "domain": settingMain.Domain, 52 | "currency": settingPayment.Currency, 53 | }, 54 | "socials": settingSocial, 55 | "pages": pages, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /web/admin/src/components/form/Input.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | -------------------------------------------------------------------------------- /scripts/tools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #set -e 4 | 5 | ROOT_PATH="$(git rev-parse --show-toplevel)" 6 | source ${ROOT_PATH}/scripts/_helper 7 | 8 | support_arch 9 | 10 | case "$OS" in 11 | Darwin) 12 | brew install goose golangci-lint yq sqlite3 13 | ;; 14 | Linux) 15 | mkdir -p "${ROOT_PATH}/.vscode/tmp" 16 | mkdir -p "${HOME}/.local/bin" 17 | 18 | print_header "Install/Update sqlite3" 19 | maybe_sudo apt-get install sqlite3 -y >/dev/null 2>&1 20 | print_answer "SUCCESS" green 21 | 22 | print_header "Install/Update golangci-lint" 23 | GOLINTER_LATEST=$(get_latest_release "golangci/golangci-lint") 24 | wget "https://github.com/golangci/golangci-lint/releases/download/${GOLINTER_LATEST}/golangci-lint-${GOLINTER_LATEST#*v}-linux-amd64.tar.gz" -4 -q -O ${ROOT_PATH}/.vscode/tmp/golangci-lint.tar.gz 25 | tar --no-same-owner -xzf ${ROOT_PATH}/.vscode/tmp/golangci-lint.tar.gz -C ${ROOT_PATH}/.vscode/tmp 26 | install "${ROOT_PATH}/.vscode/tmp/golangci-lint-${GOLINTER_LATEST#*v}-linux-amd64/golangci-lint" "$HOME/.local/bin/golangci-lint" 27 | print_answer "SUCCESS" green 28 | 29 | print_header "Install/Update goose" 30 | go install github.com/pressly/goose/v3/cmd/goose@latest >/dev/null 2>&1 31 | print_answer "SUCCESS" green 32 | 33 | print_header "Install/Update yq" 34 | wget "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" -4 -q -O ${ROOT_PATH}/.vscode/tmp/yq 35 | install "${ROOT_PATH}/.vscode/tmp/yq" "$HOME/.local/bin/yq" 36 | print_answer "SUCCESS" green 37 | 38 | rm -rf ${ROOT_PATH}/.vscode/tmp 39 | 40 | source ~/.profile 41 | ;; 42 | *) 43 | print_header "Install/Update tools" 44 | print_answer "ERROR" red 45 | echo "Unsupported OS: $OS" 46 | exit 47 | ;; 48 | esac 49 | -------------------------------------------------------------------------------- /web/admin/src/components/form/Select.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 64 | -------------------------------------------------------------------------------- /web/admin/src/assets/main.css: -------------------------------------------------------------------------------- 1 | [v-cloak] { 2 | display: none; 3 | } 4 | 5 | body { 6 | @apply bg-zinc-50 text-sm; 7 | } 8 | 9 | h1 { 10 | @apply text-2xl font-bold text-gray-900 sm:text-3xl; 11 | } 12 | 13 | h2 { 14 | @apply text-xl font-bold text-gray-900 sm:text-2xl; 15 | } 16 | 17 | h3 { 18 | @apply text-xl font-bold text-gray-900; 19 | } 20 | 21 | img { 22 | @apply rounded-lg; 23 | } 24 | 25 | header { 26 | @apply grid grid-cols-[1fr_120px] gap-8 pb-4 relative; 27 | 28 | div { 29 | @apply absolute right-0; 30 | } 31 | } 32 | 33 | .content-center { 34 | @apply mx-auto max-w-screen-xl px-4 py-16 sm:px-6 lg:px-8; 35 | 36 | .header { 37 | @apply mx-auto max-w-lg text-center; 38 | 39 | p { 40 | @apply mt-4 text-gray-500; 41 | } 42 | } 43 | } 44 | 45 | .error { 46 | @apply pl-4 text-sm text-red-500; 47 | } 48 | 49 | label { 50 | @apply relative block rounded border border-gray-200 bg-white pe-10 text-sm shadow-sm focus-within:border-blue-600 focus-within:ring-1 focus-within:ring-blue-600; 51 | 52 | &.none { 53 | @apply border-none bg-transparent shadow-none; 54 | } 55 | 56 | .field { 57 | @apply w-full border-none bg-transparent placeholder-transparent focus:border-transparent focus:outline-none focus:ring-0; 58 | } 59 | 60 | .title { 61 | @apply pointer-events-none absolute start-2.5 top-0 -translate-y-1/2 bg-white px-1 text-xs text-gray-700 transition-all; 62 | } 63 | 64 | .ico { 65 | @apply absolute inset-y-0 end-0 grid place-content-center pl-4 pr-2; 66 | } 67 | } 68 | 69 | .editor { 70 | @apply rounded bg-gray-200 px-2 pb-0.5 pt-2; 71 | 72 | & button { 73 | @apply mr-1 hover:bg-white hover:text-gray-700; 74 | 75 | & svg { 76 | @apply m-1.5 h-5 w-5; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /web/admin/src/assets/svg/social/instagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/queries/session.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // GetSession retrieves the session value for a given key if it hasn't expired. 9 | // It takes a context and key as arguments and returns the session value and an error if any. 10 | func (q *SettingQueries) GetSession(ctx context.Context, key string) (string, error) { 11 | var value string 12 | expires := time.Now().Unix() 13 | err := q.DB.QueryRowContext(ctx, `SELECT value FROM session WHERE key = ? AND expires > ?`, key, expires).Scan(&value) 14 | if err != nil { 15 | return "", err 16 | } 17 | return value, nil 18 | } 19 | 20 | // AddSession is a method on the SettingQueries struct that adds a new session to the database. 21 | // It takes a context, a key-value pair representing the session data, and an expiration timestamp. 22 | func (q *SettingQueries) AddSession(ctx context.Context, key, value string, expires int64) error { 23 | _, err := q.DB.ExecContext(ctx, `INSERT INTO session (key, value, expires) VALUES (?, ?, ?)`, key, value, expires) 24 | return err 25 | } 26 | 27 | // UpdateSession updates the session with a new value and expiration time for a given key. 28 | // It takes a context, a session key, the new value to be set, and the new expiration time as arguments. 29 | func (q *SettingQueries) UpdateSession(ctx context.Context, key, value string, expires int64) error { 30 | _, err := q.DB.ExecContext(ctx, `UPDATE session SET value = ?, expires = ? WHERE key = ? `, value, expires, key) 31 | return err 32 | } 33 | 34 | // DeleteSession removes a session from the database based on the provided key. 35 | func (q *SettingQueries) DeleteSession(ctx context.Context, key string) error { 36 | _, err := q.DB.ExecContext(ctx, `DELETE FROM session WHERE key = ?`, key) 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/analyze.yml: -------------------------------------------------------------------------------- 1 | name: "analyze" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | paths-ignore: 9 | - '**.md' 10 | schedule: 11 | - cron: '34 10 * * 2' 12 | 13 | jobs: 14 | cancel-previous-runs: 15 | name: Cancel previous runs 16 | runs-on: ubuntu-latest 17 | if: "!contains(github.event.commits[0].message, '[skip ci]') && !contains(github.event.commits[0].message, '[ci skip]')" 18 | steps: 19 | - uses: styfle/cancel-workflow-action@0.12.1 20 | with: 21 | access_token: ${{ github.token }} 22 | 23 | analyze: 24 | name: Analyze 25 | runs-on: ubuntu-latest 26 | if: "!contains(github.event.commits[0].message, '[skip ci]') && !contains(github.event.commits[0].message, '[ci skip]')" 27 | needs: cancel-previous-runs 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: [ 'go' ] 37 | 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v6 41 | 42 | - name: Set up Go 43 | uses: actions/setup-go@v6 44 | with: 45 | go-version-file: 'go.mod' 46 | if: ${{ matrix.language == 'go' }} 47 | 48 | - name: Initialize CodeQL 49 | uses: github/codeql-action/init@v4 50 | with: 51 | languages: ${{ matrix.language }} 52 | timeout-minutes: 10 53 | 54 | - name: Autobuild 55 | uses: github/codeql-action/autobuild@v4 56 | if: ${{ matrix.language != 'go' }} 57 | timeout-minutes: 30 58 | 59 | - name: Perform CodeQL Analysis 60 | uses: github/codeql-action/analyze@v4 61 | with: 62 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /web/admin/src/assets/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: #29d; 8 | 9 | position: fixed; 10 | z-index: 1031; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d; 26 | opacity: 1; 27 | 28 | -webkit-transform: rotate(3deg) translate(0px, -4px); 29 | -ms-transform: rotate(3deg) translate(0px, -4px); 30 | transform: rotate(3deg) translate(0px, -4px); 31 | } 32 | 33 | /* Remove these to get rid of the spinner */ 34 | #nprogress .spinner { 35 | display: block; 36 | position: fixed; 37 | z-index: 1031; 38 | top: 15px; 39 | right: 15px; 40 | } 41 | 42 | #nprogress .spinner-icon { 43 | width: 18px; 44 | height: 18px; 45 | box-sizing: border-box; 46 | 47 | border: solid 2px transparent; 48 | border-top-color: #29d; 49 | border-left-color: #29d; 50 | border-radius: 50%; 51 | 52 | -webkit-animation: nprogress-spinner 400ms linear infinite; 53 | animation: nprogress-spinner 400ms linear infinite; 54 | } 55 | 56 | .nprogress-custom-parent { 57 | overflow: hidden; 58 | position: relative; 59 | } 60 | 61 | .nprogress-custom-parent #nprogress .spinner, 62 | .nprogress-custom-parent #nprogress .bar { 63 | position: absolute; 64 | } 65 | 66 | @-webkit-keyframes nprogress-spinner { 67 | 0% { 68 | -webkit-transform: rotate(0deg); 69 | } 70 | 100% { 71 | -webkit-transform: rotate(360deg); 72 | } 73 | } 74 | @keyframes nprogress-spinner { 75 | 0% { 76 | transform: rotate(0deg); 77 | } 78 | 100% { 79 | transform: rotate(360deg); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/queries/install.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | 8 | "github.com/shurco/litecart/internal/models" 9 | "github.com/shurco/litecart/pkg/security" 10 | ) 11 | 12 | // InstallQueries is a struct that embeds a pointer to an sql.DB. 13 | // This allows for the struct to have all the methods of sql.DB, 14 | // enabling it to perform database operations directly. 15 | type InstallQueries struct { 16 | *sql.DB 17 | } 18 | 19 | // Install performs the installation process for the cart system. 20 | func (q *InstallQueries) Install(ctx context.Context, i *models.Install) error { 21 | var installed bool 22 | 23 | query := `SELECT value FROM setting WHERE key = 'installed'` 24 | if err := q.DB.QueryRowContext(ctx, query).Scan(&installed); err != nil { 25 | return err 26 | } 27 | if installed { 28 | return fmt.Errorf("%s", "Rejected because you have already installed and configured the cart") 29 | } 30 | 31 | tx, err := q.DB.BeginTx(ctx, nil) 32 | if err != nil { 33 | return err 34 | } 35 | defer func() { _ = tx.Rollback() }() 36 | 37 | passwordHash := security.GeneratePassword(i.Password) 38 | jwt_secret, err := security.NewToken(passwordHash) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | settings := map[string]string{ 44 | "installed": "true", 45 | "domain": i.Domain, 46 | "email": i.Email, 47 | "password": passwordHash, 48 | "jwt_secret": jwt_secret, 49 | } 50 | 51 | query = `UPDATE setting SET value = ? WHERE key = ?` 52 | stmt, err := tx.PrepareContext(ctx, query) 53 | if err != nil { 54 | return err 55 | } 56 | defer func() { _ = stmt.Close() }() 57 | 58 | for key, value := range settings { 59 | if _, err := stmt.ExecContext(ctx, value, key); err != nil { 60 | return err 61 | } 62 | } 63 | 64 | return tx.Commit() 65 | } 66 | -------------------------------------------------------------------------------- /internal/middleware/jwt_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gofiber/fiber/v2" 11 | "github.com/google/uuid" 12 | 13 | "github.com/shurco/litecart/internal/models" 14 | "github.com/shurco/litecart/internal/queries" 15 | "github.com/shurco/litecart/migrations" 16 | "github.com/shurco/litecart/pkg/jwtutil" 17 | ) 18 | 19 | func TestJWTProtected_BearerFlow(t *testing.T) { 20 | // init temp DB 21 | app := fiber.New() 22 | if err := queries.New(migrations.Embed()); err != nil { 23 | t.Fatalf("init queries: %v", err) 24 | } 25 | db := queries.DB() 26 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 27 | defer cancel() 28 | 29 | // write JWT settings 30 | setting := &models.JWT{Secret: "secret", ExpireHours: 1} 31 | if err := db.UpdateSettingByGroup(ctx, setting); err != nil { 32 | t.Fatalf("update jwt setting: %v", err) 33 | } 34 | 35 | app.Use(JWTProtected()) 36 | app.Get("/api/test", func(c *fiber.Ctx) error { return c.SendStatus(200) }) 37 | 38 | // no token → 401 39 | req := httptest.NewRequest(http.MethodGet, "/api/test", nil) 40 | resp, _ := app.Test(req) 41 | if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusBadRequest { 42 | t.Fatalf("expected 401/400, got %d", resp.StatusCode) 43 | } 44 | 45 | // valid token 46 | userID := uuid.NewString() 47 | exp := time.Now().Add(time.Hour).Unix() 48 | tok, err := jwtutil.GenerateNewToken("secret", userID, exp, nil) 49 | if err != nil { 50 | t.Fatalf("token gen: %v", err) 51 | } 52 | 53 | req2 := httptest.NewRequest(http.MethodGet, "/api/test", nil) 54 | req2.Header.Set("Authorization", "Bearer "+tok) 55 | resp2, _ := app.Test(req2) 56 | if resp2.StatusCode != http.StatusOK { 57 | t.Fatalf("expected 200, got %d", resp2.StatusCode) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before making any changes to this repository, we kindly request you to initiate discussions for proposed changes that do not yet have an associated [issue](https://github.com/shurco/litecart/issues). For [issue](https://github.com/shurco/litecart/issues) that already exist, you may proceed with discussions using our [issue](https://github.com/shurco/litecart/issues) tracker or any other suitable method, in consultation with the repository owners. Your collaboration is greatly appreciated. 4 | 5 | Please note: we have a [code of conduct](https://github.com/shurco/litecart/blob/master/.github/CODE_OF_CONDUCT.md), please follow it in all your interactions with the `litecart` project. 6 | 7 | ## Pull Requests or Commits 8 | Titles always we must use prefix according to below: 9 | 10 | > 🔥 Feature, ♻️ Refactor, 🩹 Fix, 🚨 Test, 📚 Doc, 🎨 Style 11 | - 🔥 Feature: Add flow to add person 12 | - ♻️ Refactor: Rename file X to Y 13 | - 🩹 Fix: Improve flow 14 | - 🚨 Test: Validate to add a new person 15 | - 📚 Doc: Translate to Portuguese middleware redirect 16 | - 🎨 Style: Respected pattern Golint 17 | 18 | All pull request that contains a feature or fix is mandatory to have unit tests. Your PR is only to be merged if you respect this flow. 19 | 20 | # 👍 Contribute 21 | 22 | If you want to say **thank you** and/or support the active development of `litecart`: 23 | 24 | 1. Add a [GitHub Star](https://github.com/shurco/litecart/stargazers) to the project. 25 | 2. Tweet about the project [on your Twitter](https://twitter.com/intent/tweet?text=%F0%9F%9B%92%20litecart%20-%20shopping-cart%20in%201%20file%20on%20%23Go%20https%3A%2F%2Fgithub.com%2Fshurco%2Flitecart). 26 | 3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or personal blog. 27 | 4. Support the project by donating a [cup of coffee](https://github.com/sponsors/shurco). -------------------------------------------------------------------------------- /internal/handlers/private/cart_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gofiber/fiber/v2" 11 | 12 | "github.com/shurco/litecart/internal/models" 13 | "github.com/shurco/litecart/internal/queries" 14 | "github.com/shurco/litecart/internal/testutil" 15 | "github.com/shurco/litecart/migrations" 16 | ) 17 | 18 | func setupCartApp(t *testing.T) (*fiber.App, func()) { 19 | cleanup := testutil.WithCmdTestDir(t) 20 | if err := queries.New(migrations.Embed()); err != nil { 21 | t.Fatal(err) 22 | } 23 | app := fiber.New() 24 | return app, func() { cleanup() } 25 | } 26 | 27 | func TestCarts_List(t *testing.T) { 28 | app, cleanup := setupCartApp(t) 29 | defer cleanup() 30 | app.Get("/api/_/carts", Carts) 31 | // call empty list 32 | req := httptest.NewRequest(http.MethodGet, "/api/_/carts", nil) 33 | resp, _ := app.Test(req) 34 | if resp.StatusCode != http.StatusOK { 35 | t.Fatalf("status %d", resp.StatusCode) 36 | } 37 | } 38 | 39 | func TestCartSendMail_Status(t *testing.T) { 40 | app, cleanup := setupCartApp(t) 41 | defer cleanup() 42 | 43 | db := queries.DB() 44 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 45 | defer cancel() 46 | // add a cart so that handler won't fail on mailer selection 47 | _ = db.AddCart(ctx, &models.Cart{Core: models.Core{ID: "cart123456789012"}, AmountTotal: 100, Currency: "USD"}) 48 | 49 | app.Post("/api/_/carts/:cart_id/mail", CartSendMail) 50 | req := httptest.NewRequest(http.MethodPost, "/api/_/carts/cart123456789012/mail", nil) 51 | resp, _ := app.Test(req) 52 | // could be 200 or 500 depending on SMTP configuration; we assert code is within allowed set 53 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusInternalServerError { 54 | t.Fatalf("unexpected status %d", resp.StatusCode) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /web/site/public/assets/css/main.css: -------------------------------------------------------------------------------- 1 | [v-cloak] { 2 | display: none; 3 | } 4 | 5 | h1 { 6 | font-size: 1.5rem; 7 | line-height: 2rem; 8 | font-weight: 700; 9 | color: #111827; 10 | 11 | @media (min-width: 640px) { 12 | font-size: 1.875rem; 13 | line-height: 2.25rem; 14 | } 15 | } 16 | 17 | h2 { 18 | font-size: 1.25rem; 19 | line-height: 1.75rem; 20 | font-weight: 700; 21 | color: #111827; 22 | 23 | @media (min-width: 640px) { 24 | font-size: 1.5rem; 25 | line-height: 2rem; 26 | } 27 | } 28 | 29 | h3 { 30 | font-weight: 700; 31 | color: #111827; 32 | 33 | @media (min-width: 640px) { 34 | font-size: 1.25rem; 35 | line-height: 1.75rem; 36 | } 37 | } 38 | 39 | .prod_desc > * + * { 40 | margin-top: 0.75em; 41 | } 42 | .prod_desc blockquote { 43 | padding-left: 1rem; 44 | border-color: #6b7280; 45 | border-style: solid; 46 | } 47 | .prod_desc blockquote ul { 48 | padding-left: 1rem; 49 | list-style-type: disc; 50 | } 51 | .prod_desc blockquote ol { 52 | padding-left: 1rem; 53 | list-style-type: disc; 54 | } 55 | .prod_desc hr { 56 | border: none; 57 | border-top: 2px solid rgba(13, 13, 13, 0.1); 58 | margin: 2rem 0; 59 | } 60 | 61 | .prod_desc blockquote { 62 | background: #f9f9f9; 63 | border-left: 10px solid #ccc; 64 | margin: 1.5em 10px; 65 | padding: 0.5em 10px; 66 | quotes: '\201C''\201D''\2018''\2019'; 67 | } 68 | 69 | .prod_desc blockquote:before { 70 | color: #ccc; 71 | content: open-quote; 72 | font-size: 4em; 73 | line-height: 0.1em; 74 | margin-right: 0.25em; 75 | vertical-align: -0.4em; 76 | } 77 | 78 | .prod_desc blockquote p { 79 | display: inline; 80 | } 81 | 82 | .prod_desc ul { 83 | list-style: circle inside none; 84 | margin: 1.5em 10px; 85 | list-style-type: disc; 86 | } 87 | 88 | .prod_desc ol { 89 | list-style: circle inside none; 90 | margin: 1.5em 10px; 91 | list-style-type: decimal; 92 | } 93 | -------------------------------------------------------------------------------- /pkg/archive/zip.go: -------------------------------------------------------------------------------- 1 | package archive 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | type ZipArchive struct { 11 | dir string 12 | zipw *zip.Writer 13 | file io.Closer 14 | } 15 | 16 | // NewZipArchive is ... 17 | func NewZipArchive(w io.WriteCloser) Archive { 18 | return &ZipArchive{"", zip.NewWriter(w), w} 19 | } 20 | 21 | // Directory is ... 22 | func (a *ZipArchive) Directory(name string) error { 23 | a.dir = name + "/" 24 | return nil 25 | } 26 | 27 | // Header is ... 28 | func (a *ZipArchive) Header(fi os.FileInfo) (io.Writer, error) { 29 | head, err := zip.FileInfoHeader(fi) 30 | if err != nil { 31 | return nil, fmt.Errorf("can't make zip header: %v", err) 32 | } 33 | head.Name = a.dir + head.Name 34 | head.Method = zip.Deflate 35 | w, err := a.zipw.CreateHeader(head) 36 | if err != nil { 37 | return nil, fmt.Errorf("can't add zip header: %v", err) 38 | } 39 | return w, nil 40 | } 41 | 42 | // Close is ... 43 | func (a *ZipArchive) Close() error { 44 | if err := a.zipw.Close(); err != nil { 45 | return err 46 | } 47 | return a.file.Close() 48 | } 49 | 50 | // Extract extracts the zip archive at src to dest. 51 | func ExtractZip(src, dest string) error { 52 | ar, err := os.Open(src) 53 | if err != nil { 54 | return err 55 | } 56 | defer func() { _ = ar.Close() }() 57 | 58 | info, err := ar.Stat() 59 | if err != nil { 60 | return err 61 | } 62 | zr, err := zip.NewReader(ar, info.Size()) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | for _, zf := range zr.File { 68 | if !zf.Mode().IsRegular() { 69 | continue 70 | } 71 | 72 | data, err := zf.Open() 73 | if err != nil { 74 | return err 75 | } 76 | err = extractFile(zf.Name, zf.Mode(), data, dest) 77 | if cerr := data.Close(); cerr != nil && err == nil { 78 | err = cerr 79 | } 80 | if err != nil { 81 | return fmt.Errorf("extract %s: %v", zf.Name, err) 82 | } 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/webhook/payment.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/shurco/litecart/internal/models" 10 | "github.com/shurco/litecart/internal/queries" 11 | "github.com/shurco/litecart/pkg/litepay" 12 | ) 13 | 14 | type Event string 15 | 16 | const ( 17 | PAYMENT_INITIATION Event = "payment_initiation" 18 | PAYMENT_CALLBACK Event = "payment_callback" 19 | PAYMENT_SUCCESS Event = "payment_success" 20 | PAYMENT_CANCEL Event = "payment_cancel" 21 | PAYMENT_ERROR Event = "payment_error" 22 | ) 23 | 24 | type Payment struct { 25 | Event Event `json:"event"` 26 | TimeStamp int64 `json:"timestamp"` 27 | Data Data `json:"data"` 28 | } 29 | 30 | type Data struct { 31 | CartID string `json:"cart_id,omitempty"` 32 | PaymentSystem litepay.PaymentSystem `json:"payment_system"` 33 | PaymentStatus litepay.Status `json:"payment_status"` 34 | TotalAmount int `json:"total_amount,omitempty"` 35 | Currency string `json:"currency,omitempty"` 36 | CartItems []litepay.Item `json:"cart_items,omitempty"` 37 | } 38 | 39 | // SendPaymentHook is ... 40 | func SendPaymentHook(resData *Payment) error { 41 | db := queries.DB() 42 | 43 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 44 | defer cancel() 45 | 46 | webhookSetting, err := queries.GetSettingByGroup[models.Webhook](ctx, db) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if webhookSetting.Url != "" { 52 | jsonData, err := json.Marshal(resData) 53 | if err != nil { 54 | return err 55 | } 56 | // synchronous call with context deadline above 57 | res, err := Send(webhookSetting.Url, jsonData) 58 | if err != nil { 59 | return err 60 | } 61 | if res.StatusCode != 200 { 62 | return fmt.Errorf("payment webhook does not return 200 status") 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yaml: -------------------------------------------------------------------------------- 1 | name: "🤔 Question" 2 | title: "\U0001F917 [Question]: " 3 | description: Ask a question so we can help you easily. 4 | labels: ["🤔 Question"] 5 | 6 | body: 7 | - type: markdown 8 | id: notice 9 | attributes: 10 | value: | 11 | ### Notice 12 | - If you think this is just a bug, open the issue with the **☢️ Bug Report** template. 13 | - If you have a suggestion for a litecart feature you would like to see, open the issue with the **✏️ Feature Request** template. 14 | - Write your issue with clear and understandable English. 15 | - type: textarea 16 | id: description 17 | attributes: 18 | label: "Question Description" 19 | description: "A clear and detailed description of the question." 20 | placeholder: "Explain your question clearly, and in detail." 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: snippet 25 | attributes: 26 | label: "Code Snippet (optional)" 27 | description: "Code snippet may be really helpful to describe some features." 28 | placeholder: "Share a code snippet to explain the feature better." 29 | render: go 30 | value: | 31 | package main 32 | 33 | import "fmt" 34 | 35 | func main() { 36 | fmt.Print("hello word") 37 | } 38 | - type: checkboxes 39 | id: terms 40 | attributes: 41 | label: "Checklist:" 42 | description: "By submitting this issue, you confirm that:" 43 | options: 44 | - label: "I agree to follow litecart's [Code of Conduct](https://github.com/shurco/litecart/blob/master/.github/CODE_OF_CONDUCT.md)." 45 | required: true 46 | - label: "I have checked for existing issues that describe my questions prior to opening this one." 47 | required: true 48 | - label: "I understand that improperly formatted questions may be closed without explanation." 49 | required: true -------------------------------------------------------------------------------- /web/admin/src/components/form/Textarea.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 52 | 53 | 68 | -------------------------------------------------------------------------------- /pkg/fsutil/file.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path" 7 | ) 8 | 9 | // some commonly flag consts for open file 10 | const ( 11 | FsCWAFlags = os.O_CREATE | os.O_WRONLY | os.O_APPEND // create, append write-only 12 | FsCWTFlags = os.O_CREATE | os.O_WRONLY | os.O_TRUNC // create, override write-only 13 | FsCWFlags = os.O_CREATE | os.O_WRONLY // create, write-only 14 | FsRFlags = os.O_RDONLY // read-only 15 | ) 16 | 17 | // IsFile reports whether the named file or directory exists. 18 | func IsFile(path string) bool { 19 | if path == "" || len(path) > 468 { 20 | return false 21 | } 22 | 23 | if fi, err := os.Stat(path); err == nil { 24 | return !fi.IsDir() 25 | } 26 | return false 27 | } 28 | 29 | // OpenFile like os.OpenFile, but will auto create dir. 30 | func OpenFile(filepath string, flag int, perm os.FileMode) (*os.File, error) { 31 | fileDir := path.Dir(filepath) 32 | if err := os.MkdirAll(fileDir, 0775); err != nil { 33 | return nil, err 34 | } 35 | 36 | file, err := os.OpenFile(filepath, flag, perm) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return file, nil 41 | } 42 | 43 | // WriteOSFile write data to give os.File, then close file. 44 | // data type allow: string, []byte, io.Reader 45 | func WriteOSFile(f *os.File, data any) (n int, err error) { 46 | switch typData := data.(type) { 47 | case []byte: 48 | n, err = f.Write(typData) 49 | case string: 50 | n, err = f.WriteString(typData) 51 | case io.Reader: // eg: buffer 52 | var n64 int64 53 | n64, err = io.Copy(f, typData) 54 | n = int(n64) 55 | default: 56 | _ = f.Close() 57 | panic("WriteFile: data type only allow: []byte, string, io.Reader") 58 | } 59 | 60 | if err1 := f.Close(); err1 != nil && err == nil { 61 | err = err1 62 | } 63 | return n, err 64 | } 65 | 66 | // ExtName is ... 67 | func ExtName(fpath string) string { 68 | if ext := path.Ext(fpath); len(ext) > 0 { 69 | return ext[1:] 70 | } 71 | return "" 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - 'v*' 9 | 10 | permissions: 11 | contents: write # needed to write releases 12 | packages: write # needed for ghcr access 13 | 14 | jobs: 15 | goreleaser: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v6 19 | with: 20 | fetch-depth: 0 21 | - uses: docker/setup-qemu-action@v3 22 | - uses: docker/setup-buildx-action@v3 23 | - uses: docker/login-action@v3 24 | with: 25 | registry: ghcr.io 26 | username: ${{ github.repository_owner }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | - uses: docker/login-action@v3 29 | with: 30 | username: ${{ secrets.DOCKER_USERNAME }} 31 | password: ${{ secrets.DOCKER_PASSWORD }} 32 | - run: git fetch --force --tags 33 | - uses: actions/setup-go@v6 34 | with: 35 | go-version: stable 36 | - uses: crazy-max/ghaction-upx@v3 37 | with: 38 | install-only: true 39 | version: latest 40 | - uses: goreleaser/goreleaser-action@v6 41 | with: 42 | distribution: goreleaser 43 | version: latest 44 | args: release --clean 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} 48 | TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} 49 | TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} 50 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 51 | MASTODON_CLIENT_ID: ${{ secrets.MASTODON_CLIENT_ID }} 52 | MASTODON_CLIENT_SECRET: ${{ secrets.MASTODON_CLIENT_SECRET }} 53 | MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} 54 | REDDIT_SECRET: ${{ secrets.REDDIT_SECRET }} 55 | REDDIT_PASSWORD: ${{ secrets.REDDIT_PASSWORD }} -------------------------------------------------------------------------------- /web/admin/src/pages/settings/social.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 59 | -------------------------------------------------------------------------------- /web/admin/src/components/form/Toggle.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 47 | -------------------------------------------------------------------------------- /web/admin/src/components/page/Seo.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature Request" 2 | title: "\U0001F680 [Feature]: " 3 | description: Suggest an idea to improve this project. 4 | labels: ["✏️ Feature"] 5 | 6 | body: 7 | - type: markdown 8 | id: notice 9 | attributes: 10 | value: | 11 | ### Notice 12 | - If you think this is just a bug, open the issue with the **☢️ Bug Report** template. 13 | - Write your issue with clear and understandable English. 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: "Feature Description" 18 | description: "A clear and detailed description of the feature you would like to see added." 19 | placeholder: "Explain your feature clearly, and in detail." 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: additional-context 24 | attributes: 25 | label: "Additional Context (optional)" 26 | description: "If you have something else to describe, write them here." 27 | placeholder: "Write here what you can describe differently." 28 | - type: textarea 29 | id: snippet 30 | attributes: 31 | label: "Code Snippet (optional)" 32 | description: "Code snippet may be really helpful to describe some features." 33 | placeholder: "Share a code snippet to explain the feature better." 34 | render: go 35 | value: | 36 | package main 37 | 38 | import "fmt" 39 | 40 | func main() { 41 | fmt.Print("hello word") 42 | } 43 | - type: checkboxes 44 | id: terms 45 | attributes: 46 | label: "Checklist:" 47 | description: "By submitting this issue, you confirm that:" 48 | options: 49 | - label: "I agree to follow litecart's [Code of Conduct](https://github.com/shurco/litecart/blob/master/.github/CODE_OF_CONDUCT.md)." 50 | required: true 51 | - label: "I have checked for existing issues that describe my suggestion prior to opening this one." 52 | required: true 53 | - label: "I understand that improperly formatted feature requests may be closed without explanation." 54 | required: true -------------------------------------------------------------------------------- /internal/queries/queries_test.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/shurco/litecart/internal/models" 10 | "github.com/shurco/litecart/migrations" 11 | ) 12 | 13 | func withTempBase(t *testing.T) func() { 14 | t.Helper() 15 | dir := t.TempDir() 16 | oldwd, _ := os.Getwd() 17 | if err := os.Chdir(dir); err != nil { 18 | t.Fatalf("chdir to temp: %v", err) 19 | } 20 | _ = os.MkdirAll("lc_base", 0o775) 21 | _ = os.MkdirAll("lc_uploads", 0o775) 22 | _ = os.MkdirAll("lc_digitals", 0o775) 23 | return func() { _ = os.Chdir(oldwd) } 24 | } 25 | 26 | func TestQueries_InitAndSettings(t *testing.T) { 27 | cleanup := withTempBase(t) 28 | defer cleanup() 29 | 30 | if err := New(migrations.Embed()); err != nil { 31 | t.Fatalf("init queries: %v", err) 32 | } 33 | 34 | db := DB() 35 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 36 | defer cancel() 37 | 38 | setting, err := db.GetSettingByKey(ctx, "installed") 39 | if err != nil { 40 | t.Fatalf("get setting: %v", err) 41 | } 42 | if _, ok := setting["installed"]; !ok { 43 | t.Fatalf("installed key not found") 44 | } 45 | } 46 | 47 | func TestQueries_PageCRUD(t *testing.T) { 48 | cleanup := withTempBase(t) 49 | defer cleanup() 50 | if err := New(migrations.Embed()); err != nil { 51 | t.Fatalf("init queries: %v", err) 52 | } 53 | db := DB() 54 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 55 | defer cancel() 56 | 57 | page, err := db.AddPage(ctx, &models.Page{Name: "Test", Slug: "test", Position: "footer"}) 58 | if err != nil { 59 | t.Fatalf("add page: %v", err) 60 | } 61 | if page.Created == 0 { 62 | t.Fatalf("expected created timestamp") 63 | } 64 | 65 | list, err := db.ListPages(ctx, true) 66 | if err != nil { 67 | t.Fatalf("list pages: %v", err) 68 | } 69 | if len(list) == 0 { 70 | t.Fatalf("expected pages > 0") 71 | } 72 | 73 | if err := db.UpdatePageContent(ctx, &models.Page{Core: models.Core{ID: page.ID}, Content: ptr("content")}); err != nil { 74 | t.Fatalf("update content: %v", err) 75 | } 76 | } 77 | 78 | func ptr[T any](v T) *T { return &v } 79 | -------------------------------------------------------------------------------- /pkg/archive/tar.go: -------------------------------------------------------------------------------- 1 | package archive 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "os" 9 | "time" 10 | ) 11 | 12 | // TarArchive is ... 13 | type TarArchive struct { 14 | dir string 15 | tarw *tar.Writer 16 | gzw *gzip.Writer 17 | file io.Closer 18 | } 19 | 20 | // NewTarArchive is ... 21 | func NewTarArchive(w io.WriteCloser) Archive { 22 | gzw := gzip.NewWriter(w) 23 | tarw := tar.NewWriter(gzw) 24 | return &TarArchive{"", tarw, gzw, w} 25 | } 26 | 27 | // Directory is ... 28 | func (a *TarArchive) Directory(name string) error { 29 | a.dir = name + "/" 30 | return a.tarw.WriteHeader(&tar.Header{ 31 | Name: a.dir, 32 | Mode: 0755, 33 | Typeflag: tar.TypeDir, 34 | ModTime: time.Now(), 35 | }) 36 | } 37 | 38 | // Header is ... 39 | func (a *TarArchive) Header(fi os.FileInfo) (io.Writer, error) { 40 | head, err := tar.FileInfoHeader(fi, "") 41 | if err != nil { 42 | return nil, fmt.Errorf("can't make tar header: %v", err) 43 | } 44 | head.Name = a.dir + head.Name 45 | if err := a.tarw.WriteHeader(head); err != nil { 46 | return nil, fmt.Errorf("can't add tar header: %v", err) 47 | } 48 | return a.tarw, nil 49 | } 50 | 51 | // Close is ... 52 | func (a *TarArchive) Close() error { 53 | if err := a.tarw.Close(); err != nil { 54 | return err 55 | } 56 | if err := a.gzw.Close(); err != nil { 57 | return err 58 | } 59 | return a.file.Close() 60 | } 61 | 62 | // Extract extracts the tar archive at src to dest. 63 | func ExtractTar(src, dest string) error { 64 | ar, err := os.Open(src) 65 | if err != nil { 66 | return err 67 | } 68 | defer func() { _ = ar.Close() }() 69 | 70 | gzr, err := gzip.NewReader(ar) 71 | if err != nil { 72 | return err 73 | } 74 | defer func() { _ = gzr.Close() }() 75 | 76 | tr := tar.NewReader(gzr) 77 | for { 78 | header, err := tr.Next() 79 | if err != nil { 80 | if err == io.EOF { 81 | return nil 82 | } 83 | return err 84 | } 85 | if header.Typeflag == tar.TypeReg { 86 | mode := header.FileInfo().Mode() 87 | err := extractFile(header.Name, mode, tr, dest) 88 | if err != nil { 89 | return fmt.Errorf("extract %s: %v", header.Name, err) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /web/admin/src/components/form/Upload.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 60 | 61 | 74 | -------------------------------------------------------------------------------- /k8s/litecart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: litecart-pvc 5 | namespace: shop 6 | spec: 7 | accessModes: 8 | - ReadWriteMany 9 | resources: 10 | requests: 11 | storage: 2Gi 12 | storageClassName: longhorn-sg 13 | 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: litecart 19 | namespace: shop 20 | spec: 21 | replicas: 1 22 | selector: 23 | matchLabels: 24 | app: litecart 25 | template: 26 | metadata: 27 | labels: 28 | app: litecart 29 | spec: 30 | nodeSelector: 31 | type: sg 32 | containers: 33 | - name: litecart 34 | image: shurco/litecart:latest 35 | ports: 36 | - containerPort: 8080 37 | volumeMounts: 38 | - name: litecart-storage 39 | mountPath: /lc_base 40 | subPath: lc_base 41 | - name: litecart-storage 42 | mountPath: /lc_digitals 43 | subPath: lc_digitals 44 | - name: litecart-storage 45 | mountPath: /lc_uploads 46 | subPath: lc_uploads 47 | - name: litecart-storage 48 | mountPath: /site 49 | subPath: site 50 | volumes: 51 | - name: litecart-storage 52 | persistentVolumeClaim: 53 | claimName: litecart-pvc 54 | 55 | --- 56 | apiVersion: networking.k8s.io/v1 57 | kind: Ingress 58 | metadata: 59 | name: litecart-ingress 60 | namespace: shop 61 | annotations: 62 | cert-manager.io/cluster-issuer: "letsencrypt-prod" 63 | nginx.ingress.kubernetes.io/ssl-redirect: "true" 64 | spec: 65 | ingressClassName: nginx 66 | tls: 67 | - hosts: 68 | - domain 69 | secretName: litecart-tls 70 | rules: 71 | - host: domain 72 | http: 73 | paths: 74 | - path: / 75 | pathType: Prefix 76 | backend: 77 | service: 78 | name: litecart-service 79 | port: 80 | number: 8080 81 | 82 | --- 83 | apiVersion: v1 84 | kind: Service 85 | metadata: 86 | name: litecart-service 87 | namespace: shop 88 | spec: 89 | selector: 90 | app: litecart 91 | ports: 92 | - protocol: TCP 93 | port: 8080 94 | targetPort: 8080 95 | type: NodePort -------------------------------------------------------------------------------- /internal/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | jwtMiddleware "github.com/gofiber/contrib/jwt" 11 | "github.com/gofiber/fiber/v2" 12 | "github.com/golang-jwt/jwt/v5" 13 | 14 | "github.com/shurco/litecart/internal/models" 15 | "github.com/shurco/litecart/internal/queries" 16 | "github.com/shurco/litecart/pkg/webutil" 17 | ) 18 | 19 | // JWTProtected is ... 20 | func JWTProtected() func(*fiber.Ctx) error { 21 | config := jwtMiddleware.Config{ 22 | KeyFunc: customKeyFunc(), 23 | ContextKey: "jwt", 24 | ErrorHandler: jwtError, 25 | TokenLookup: "header:Authorization,cookie:token", 26 | AuthScheme: "Bearer", 27 | } 28 | 29 | return jwtMiddleware.New(config) 30 | } 31 | 32 | func jwtError(c *fiber.Ctx, err error) error { 33 | path := strings.Split(c.Path(), "/")[1] 34 | if path == "api" { 35 | if err != nil { 36 | // Map common JWT errors to appropriate status codes 37 | if strings.Contains(err.Error(), "Missing") || strings.Contains(err.Error(), "malformed") { 38 | return webutil.Response(c, http.StatusBadRequest, "bad request", "missing or malformed token") 39 | } 40 | return webutil.Response(c, http.StatusUnauthorized, "unauthorized", "invalid or expired token") 41 | } 42 | } 43 | 44 | return c.Redirect("/_/signin") 45 | } 46 | 47 | func customKeyFunc() jwt.Keyfunc { 48 | return func(t *jwt.Token) (interface{}, error) { 49 | // Set a timeout of 5 secs to prevent indefinite blocking 50 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 51 | defer cancel() 52 | 53 | db := queries.DB() 54 | settingJWT, err := queries.GetSettingByGroup[models.JWT](ctx, db) 55 | // Handles database errors when retrieving the JWT secret 56 | if err != nil { 57 | if ctx.Err() == context.DeadlineExceeded { 58 | // Database time out 59 | return nil, fmt.Errorf("database took too long to respond") 60 | } 61 | // Database error 62 | return nil, fmt.Errorf("database error: %w", err) 63 | } 64 | //Add secret validation 65 | if settingJWT.Secret == "" { 66 | return nil, fmt.Errorf("JWT secret is empty or not configured") 67 | } 68 | 69 | return []byte(settingJWT.Secret), nil 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /web/admin/src/pages/Carts.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 69 | -------------------------------------------------------------------------------- /scripts/webscripts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ROOT_PATH="$(git rev-parse --show-toplevel)" 4 | source ${ROOT_PATH}/scripts/_helper 5 | 6 | NODE_ENV=".prod" 7 | [[ $1 == "dev" ]] && NODE_ENV="" 8 | 9 | print_header "Install/Update vue" 10 | VUECORE_LATEST=$(get_latest_release "vuejs/core") 11 | wget "https://unpkg.com/vue@${VUECORE_LATEST:1}/dist/vue.global${NODE_ENV}.js" -4 -q -O ${ROOT_PATH}/web/site/public/assets/js/vue.js 12 | if [ -s "${ROOT_PATH}/web/site/public/assets/js/vue.js" ]; then 13 | print_answer "SUCCESS" green 14 | else 15 | print_answer "ERROR" red 16 | rm ${ROOT_PATH}/web/site/public/assets/js/vue.js 17 | fi 18 | 19 | # print_header "Install/Update nprogress" 20 | # NPROGRESS_LATEST=$(get_latest_release "rstacruz/nprogress") 21 | # wget "https://unpkg.com/nprogress@${NPROGRESS_LATEST:1}/nprogress.js" -4 -q -O ${ROOT_PATH}/web/site/public/assets/js/nprogress.js 22 | # wget "https://unpkg.com/nprogress@${NPROGRESS_LATEST:1}/nprogress.css" -4 -q -O ${ROOT_PATH}/web/site/public/assets/css/nprogress.css 23 | # if [ -s "${ROOT_PATH}/web/site/public/assets/js/nprogress.js" ] && [ -s "${ROOT_PATH}/web/site/public/assets/css/nprogress.css" ]; then 24 | # print_answer "SUCCESS" green 25 | # else 26 | # print_answer "ERROR" red 27 | # rm ${ROOT_PATH}/web/site/public/assets/js/nprogress.js 28 | # rm ${ROOT_PATH}/web/site/public/assets/css/nprogress.css 29 | # fi 30 | 31 | # print_header "Install/Update vue-demi" 32 | # VUEDEMI_LATEST=$(get_latest_release "vueuse/vue-demi") 33 | # wget "https://unpkg.com/vue-demi@${VUEDEMI_LATEST:1}/lib/index.iife.js" -4 -q -O ${ROOT_PATH}/web/site/public/assets/js/vue-demi.js 34 | # if [ -s "${ROOT_PATH}/web/site/public/assets/js/vue-demi.js" ]; then 35 | # print_answer "SUCCESS" green 36 | # else 37 | # print_answer "ERROR" red 38 | # rm ${ROOT_PATH}/web/site/public/assets/js/vue-demi.js 39 | # fi 40 | 41 | # print_header "Install/Update pinia" 42 | # PINIA_LATEST=$(get_latest_release "vuejs/pinia") 43 | # wget "https://unpkg.com/pinia@${PINIA_LATEST:6}/dist/pinia.iife${NODE_ENV}.js" -4 -q -O ${ROOT_PATH}/web/site/public/assets/js/pinia.js 44 | # if [ -s "${ROOT_PATH}/web/site/public/assets/js/pinia.js" ]; then 45 | # print_answer "SUCCESS" green 46 | # else 47 | # print_answer "ERROR" red 48 | # rm ${ROOT_PATH}/web/site/public/assets/js/pinia.js 49 | # fi 50 | -------------------------------------------------------------------------------- /web/admin/src/components/product/Seo.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 76 | -------------------------------------------------------------------------------- /internal/handlers/private/auth_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gofiber/fiber/v2" 13 | 14 | "github.com/shurco/litecart/internal/models" 15 | "github.com/shurco/litecart/internal/queries" 16 | "github.com/shurco/litecart/internal/testutil" 17 | "github.com/shurco/litecart/migrations" 18 | ) 19 | 20 | func setupApp(t *testing.T) (*fiber.App, func()) { 21 | t.Helper() 22 | cleanup := testutil.WithCmdTestDir(t) 23 | 24 | if err := queries.New(migrations.Embed()); err != nil { 25 | t.Fatal(err) 26 | } 27 | app := fiber.New() 28 | return app, func() { cleanup(); _ = os.Unsetenv("_") } 29 | } 30 | 31 | func TestAuth_SignInOut(t *testing.T) { 32 | app, cleanup := setupApp(t) 33 | defer cleanup() 34 | 35 | // prepare settings (install + jwt) 36 | db := queries.DB() 37 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 38 | defer cancel() 39 | 40 | // install minimal creds 41 | inst := &models.Install{Email: "admin@example.com", Password: "secret", Domain: "example.com"} 42 | if err := db.Install(ctx, inst); err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | // jwt expiry shorter 47 | if err := db.UpdateSettingByGroup(ctx, &models.JWT{Secret: "secretjwt", ExpireHours: 1}); err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | app.Post("/api/sign/in", SignIn) 52 | app.Post("/api/sign/out", SignOut) 53 | 54 | // sign in 55 | body := `{"email":"admin@example.com","password":"secret"}` 56 | req := httptest.NewRequest(http.MethodPost, "/api/sign/in", strings.NewReader(body)) 57 | req.Header.Set("Content-Type", "application/json") 58 | resp, err := app.Test(req) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | if resp.StatusCode != http.StatusOK { 63 | t.Fatalf("signin status %d", resp.StatusCode) 64 | } 65 | 66 | // extract cookie 67 | cookie := resp.Header.Get("Set-Cookie") 68 | if cookie == "" { 69 | t.Fatalf("expected token cookie") 70 | } 71 | 72 | // sign out 73 | req2 := httptest.NewRequest(http.MethodPost, "/api/sign/out", nil) 74 | req2.Header.Set("Cookie", cookie) 75 | resp2, err := app.Test(req2) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if resp2.StatusCode != http.StatusNoContent { 80 | t.Fatalf("signout status %d", resp2.StatusCode) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/mailer/mailer.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "text/template" 7 | "time" 8 | 9 | mailer "github.com/xhit/go-simple-mail/v2" 10 | 11 | "github.com/shurco/litecart/internal/models" 12 | ) 13 | 14 | var EncryptionTypes = map[string]mailer.Encryption{ 15 | "None": mailer.EncryptionNone, 16 | "SSL/TLS": mailer.EncryptionSSL, 17 | "STARTTLS": mailer.EncryptionTLS, 18 | } 19 | 20 | // SendMail is ... 21 | func SendMail(smtp *models.Mail, mail *models.MessageMail) error { 22 | // Validate SMTP settings before attempting connection 23 | if smtp.SMTP.Host == "" || smtp.SMTP.Port <= 0 || smtp.SMTP.Username == "" || smtp.SMTP.Password == "" { 24 | return fmt.Errorf("invalid SMTP settings: host, port, username, and password are required") 25 | } 26 | 27 | server := mailer.NewSMTPClient() 28 | server.Host = smtp.SMTP.Host 29 | server.Port = smtp.SMTP.Port 30 | server.Username = smtp.SMTP.Username 31 | server.Password = smtp.SMTP.Password 32 | server.Encryption = EncryptionTypes[smtp.SMTP.Encryption] 33 | 34 | server.KeepAlive = false 35 | server.ConnectTimeout = 10 * time.Second 36 | server.SendTimeout = 10 * time.Second 37 | 38 | smtpClient, err := server.Connect() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | from := fmt.Sprintf("%s <%s>", smtp.SenderName, smtp.SenderEmail) 44 | email := mailer.NewMSG() 45 | email.SetFrom(from). 46 | AddTo(mail.To). 47 | SetSubject(mail.Letter.Subject) 48 | 49 | bodyText, err := textTemplate(mail.Letter.Text, mail.Data) 50 | if err != nil { 51 | return err 52 | } 53 | email.SetBodyData(mailer.TextPlain, bodyText) 54 | // email.AddAlternativeData(mail.TextPlain, "Hello Gophers!") 55 | 56 | if len(mail.Files) > 0 { 57 | for _, file := range mail.Files { 58 | email.Attach(&mailer.File{ 59 | FilePath: fmt.Sprintf("./lc_digitals/%s.%s", file.Name, file.Ext), 60 | Name: file.OrigName, 61 | }) 62 | } 63 | } 64 | 65 | if err := email.Send(smtpClient); err != nil { 66 | return err 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func textTemplate(tmp string, data any) ([]byte, error) { 73 | tmpl, err := template.New("").Parse(tmp) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | var body bytes.Buffer 79 | if err := tmpl.Execute(&body, data); err != nil { 80 | return nil, err 81 | } 82 | 83 | return body.Bytes(), nil 84 | } 85 | -------------------------------------------------------------------------------- /scripts/_helper: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The following line uses the `uname` command to get the machine's architecture and assigns it to the variable `ARCH`. 4 | ARCH=$(uname -m) 5 | case $ARCH in 6 | i386) ARCH="386" ;; 7 | i686) ARCH="386" ;; 8 | x86_64) ARCH="amd64" ;; 9 | esac 10 | 11 | # The following line uses the `uname` command to get the name of the operating system and assigns it to the variable `OS`. 12 | # return Linux, Darwin, Windows 13 | OS=$(uname -s) 14 | 15 | # Init color 16 | COLOR_GREY=$(tput setaf 0) 17 | COLOR_RED=$(tput setaf 1) 18 | COLOR_GREEN=$(tput setaf 2) 19 | COLOR_YELLOW=$(tput setaf 3) 20 | COLOR_RESET=$(tput sgr0) 21 | 22 | GOPATH=$(go env GOPATH) 23 | GOBIN=$(go env GOPATH)/bin 24 | 25 | DATE=$(date '+%Y-%m-%d-%H:%M:%S') 26 | GIT_COMMIT=$(git rev-parse --short HEAD) 27 | GIT_DIRTY=$(test -n "$(git status --porcelain)" && echo "+CHANGES" || true) 28 | GIT_TAG=$(git describe --tags --abbrev=0 2>/dev/null) 29 | VERSION=${GIT_TAG#*v} 30 | 31 | maybe_sudo() { 32 | if [ "$(id -u)" -ne 0 ]; then 33 | sudo "$@" 34 | else 35 | "$@" 36 | fi 37 | } 38 | 39 | # This function checks if the architecture of the system running the script is supported. 40 | support_arch() { 41 | if [ "$ARCH" != "amd64" ]; then 42 | print_answer "ERROR" red 43 | echo "unsupported architecture: $ARCH" 44 | exit 1 45 | fi 46 | } 47 | 48 | generate_password() { 49 | tr -cd 'a-zA-Z0-9!#$%&()*+?@[]^_' /dev/null 2>&1 63 | } 64 | 65 | print_answer() { 66 | local COLOR="$COLOR_RESET" 67 | for flag in "$@"; do 68 | case $flag in 69 | grey) COLOR=$COLOR_GREY ;; 70 | green) COLOR=$COLOR_GREEN ;; 71 | yellow) COLOR=$COLOR_YELLOW ;; 72 | red) COLOR=$COLOR_RED ;; 73 | esac 74 | done 75 | echo "${COLOR}$1${COLOR_RESET}" >&2 76 | } 77 | 78 | print_header() { 79 | printf "%.45s " "$* ........................................" 80 | } 81 | 82 | print_help() { 83 | printf "%.20s " "${COLOR_GREEN}$1${COLOR_RESET} " 84 | echo "$2" >&2 85 | } 86 | -------------------------------------------------------------------------------- /.vscode/tailwind.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.1, 3 | "atDirectives": [ 4 | { 5 | "name": "@tailwind", 6 | "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", 7 | "references": [ 8 | { 9 | "name": "Tailwind Documentation", 10 | "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "@apply", 16 | "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", 17 | "references": [ 18 | { 19 | "name": "Tailwind Documentation", 20 | "url": "https://tailwindcss.com/docs/functions-and-directives#apply" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "@responsive", 26 | "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", 27 | "references": [ 28 | { 29 | "name": "Tailwind Documentation", 30 | "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "@screen", 36 | "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", 37 | "references": [ 38 | { 39 | "name": "Tailwind Documentation", 40 | "url": "https://tailwindcss.com/docs/functions-and-directives#screen" 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "@variants", 46 | "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", 47 | "references": [ 48 | { 49 | "name": "Tailwind Documentation", 50 | "url": "https://tailwindcss.com/docs/functions-and-directives#variants" 51 | } 52 | ] 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /internal/routes/api_private_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | 6 | handlers "github.com/shurco/litecart/internal/handlers/private" 7 | "github.com/shurco/litecart/internal/middleware" 8 | ) 9 | 10 | // ApiPrivateRoutes is ... 11 | func ApiPrivateRoutes(c *fiber.App) { 12 | c.Post("/api/install", handlers.Install) 13 | 14 | c.Get("/api/_/version", middleware.JWTProtected(), handlers.Version) 15 | 16 | sign := c.Group("/api/sign") 17 | sign.Post("/in", handlers.SignIn) 18 | sign.Post("/out", middleware.JWTProtected(), handlers.SignOut) 19 | 20 | settings := c.Group("/api/_/settings", middleware.JWTProtected()) 21 | settings.Get("/:setting_key", handlers.GetSetting) 22 | settings.Patch("/:setting_key", handlers.UpdateSetting) 23 | 24 | test := c.Group("/api/_/test", middleware.JWTProtected()) 25 | test.Get("/letter/:letter_name", handlers.TestLetter) 26 | 27 | pages := c.Group("/api/_/pages", middleware.JWTProtected()) 28 | pages.Get("/", handlers.Pages) 29 | pages.Post("/", handlers.AddPage) 30 | pages.Patch("/:page_id", handlers.UpdatePage) 31 | pages.Delete("/:page_id", handlers.DeletePage) 32 | pages.Patch("/:page_id/content", handlers.UpdatePageContent) 33 | pages.Patch("/:page_id/active", handlers.UpdatePageActive) 34 | 35 | product := c.Group("/api/_/products", middleware.JWTProtected()) 36 | product.Get("/", handlers.Products) 37 | product.Post("/", handlers.AddProduct) 38 | product.Get("/:product_id", handlers.Product) 39 | product.Patch("/:product_id", handlers.UpdateProduct) 40 | product.Delete("/:product_id", handlers.DeleteProduct) 41 | product.Patch("/:product_id/active", handlers.UpdateProductActive) 42 | 43 | product.Get("/:product_id/digital", handlers.ProductDigital) 44 | product.Post("/:product_id/digital", handlers.AddProductDigital) 45 | product.Patch("/:product_id/digital/:digital_id", handlers.UpdateProductDigital) 46 | product.Delete("/:product_id/digital/:digital_id", handlers.DeleteProductDigital) 47 | 48 | product.Get("/:product_id/image", handlers.ProductImages) 49 | product.Post("/:product_id/image", handlers.AddProductImage) 50 | product.Delete("/:product_id/image/:image_id", handlers.DeleteProductImage) 51 | 52 | // carts 53 | carts := c.Group("/api/_/carts", middleware.JWTProtected()) 54 | carts.Get("/", handlers.Carts) 55 | carts.Post("/:cart_id/mail", handlers.CartSendMail) 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shurco/litecart 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/disintegration/imaging v1.6.2 7 | github.com/go-ozzo/ozzo-validation/v4 v4.3.0 8 | github.com/gofiber/contrib/fiberzerolog v1.0.3 9 | github.com/gofiber/contrib/jwt v1.1.2 10 | github.com/gofiber/fiber/v2 v2.52.9 11 | github.com/gofiber/template/html/v2 v2.1.3 12 | github.com/gofiber/utils v1.1.0 13 | github.com/golang-jwt/jwt/v5 v5.3.0 14 | github.com/google/uuid v1.6.0 15 | github.com/pressly/goose/v3 v3.26.0 16 | github.com/rs/zerolog v1.34.0 17 | github.com/spf13/cobra v1.10.1 18 | github.com/stretchr/testify v1.11.1 19 | github.com/xhit/go-simple-mail/v2 v2.16.0 20 | golang.org/x/crypto v0.43.0 21 | modernc.org/sqlite v1.39.1 22 | ) 23 | 24 | require ( 25 | github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect 26 | github.com/andybalholm/brotli v1.2.0 // indirect 27 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 28 | github.com/clipperhouse/uax29/v2 v2.2.0 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/dustin/go-humanize v1.0.1 // indirect 31 | github.com/go-test/deep v1.1.1 // indirect 32 | github.com/gofiber/template v1.8.3 // indirect 33 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 34 | github.com/klauspost/compress v1.18.1 // indirect 35 | github.com/mattn/go-colorable v0.1.14 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/mattn/go-runewidth v0.0.19 // indirect 38 | github.com/mfridman/interpolate v0.0.2 // indirect 39 | github.com/ncruces/go-strftime v1.0.0 // indirect 40 | github.com/pkg/errors v0.9.1 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 43 | github.com/rivo/uniseg v0.4.7 // indirect 44 | github.com/sethvargo/go-retry v0.3.0 // indirect 45 | github.com/spf13/pflag v1.0.10 // indirect 46 | github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect 47 | github.com/valyala/bytebufferpool v1.0.0 // indirect 48 | github.com/valyala/fasthttp v1.67.0 // indirect 49 | go.uber.org/multierr v1.11.0 // indirect 50 | golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect 51 | golang.org/x/image v0.32.0 // indirect 52 | golang.org/x/net v0.46.0 // indirect 53 | golang.org/x/sync v0.17.0 // indirect 54 | golang.org/x/sys v0.37.0 // indirect 55 | golang.org/x/text v0.30.0 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | modernc.org/libc v1.66.10 // indirect 58 | modernc.org/mathutil v1.7.1 // indirect 59 | modernc.org/memory v1.11.0 // indirect 60 | ) 61 | --------------------------------------------------------------------------------