├── .gitignore ├── README.md ├── backend ├── .env ├── .gitignore ├── Dockerfile ├── README.md ├── app │ ├── router.go │ └── server.go ├── backups │ └── balance_test_queries.txt ├── db │ ├── migrations │ │ ├── 20230116143036_init.sql │ │ ├── 20230117124309_transactions_table.sql │ │ └── 20230119152024_balances_table.sql │ ├── schema.sql │ ├── sql │ │ ├── balance.sql │ │ ├── functions.sql │ │ ├── transaction.sql │ │ └── user.sql │ ├── sqlc.yaml │ └── sqlc │ │ ├── balance.sql.go │ │ ├── db.go │ │ ├── models.go │ │ ├── transaction.sql.go │ │ └── user.sql.go ├── go.mod ├── go.sum ├── lib │ ├── auth │ │ ├── jwt_convert.go │ │ ├── jwt_create.go │ │ └── jwt_validate.go │ ├── middleware │ │ └── middleware.go │ ├── paginate │ │ └── main.go │ ├── request │ │ └── main.go │ ├── response │ │ └── response.go │ ├── utils │ │ └── utils.go │ └── validate │ │ └── validate.go ├── main.go ├── modules │ ├── auth_module │ │ ├── body.gen.go │ │ └── controllers.go │ ├── transaction_module │ │ ├── body.gen.go │ │ ├── controllers.go │ │ ├── get.go │ │ └── post.go │ └── user_module │ │ ├── body.gen.go │ │ ├── controllers.go │ │ ├── custm_response.go │ │ ├── delete.go │ │ ├── get.go │ │ └── post.go ├── public │ ├── gomarvin.gen.ts │ └── placeholder.ts └── settings │ ├── database │ └── database.go │ ├── env.go │ ├── jwt-time.go │ └── settings.go ├── dump.sql ├── frontend ├── .gitignore ├── .vscode │ └── extensions.json ├── README.md ├── backups │ └── Authenticate_previous.txt ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public │ └── vite.svg ├── src │ ├── App.vue │ ├── assets │ │ ├── css │ │ │ ├── base │ │ │ │ ├── _custom.css │ │ │ │ ├── html.css │ │ │ │ ├── media.css │ │ │ │ └── root.css │ │ │ └── index.css │ │ ├── ts │ │ │ ├── API.ts │ │ │ ├── auth.ts │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ └── localstorage.ts │ │ └── vue.svg │ ├── components │ │ ├── global │ │ │ ├── ApiValidationFailedErrors.vue │ │ │ ├── Container.vue │ │ │ ├── DebugGrid.vue │ │ │ ├── Header.vue │ │ │ ├── InputComponent.vue │ │ │ └── LoadingSpinner.vue │ │ └── pages │ │ │ ├── Authenticate │ │ │ ├── LoginForm.vue │ │ │ └── SignupForm.vue │ │ │ └── Home │ │ │ ├── TransactionsTable.vue │ │ │ └── UserDetails.vue │ ├── layout │ │ └── MainLayout.vue │ ├── main.ts │ ├── pages │ │ ├── Authenticate.vue │ │ └── Home.vue │ ├── router.ts │ ├── style.css │ └── vite-env.d.ts ├── tailwind.config.cjs ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── w.code-workspace ├── gomarvin.json ├── seeder.ts └── w.code-workspace /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Full-Stack app for creating transactions 2 | 3 | https://user-images.githubusercontent.com/82293948/213549059-0cdc7ddc-fabe-4d7f-92c3-1844c08ccbb5.mp4 4 | 5 | - Backend 6 | - Golang, Chi Framework 7 | - JWT for authentication 8 | - Rate limiting for registration endpoint 9 | - Payload validation using validate package 10 | - Cursor Pagination for Users and Transactions endpoints (using `uuids` and `created_at` values) 11 | - SQLC for getting the data, dbmate for managing migrations. 12 | - If you want to preview the backend endpoints, just copy `gomarvin.json` content in the [editor](https://gomarvin.pages.dev/) (Settings -> Import Tab) 13 | - Frontend 14 | - Vue 3 + Vite + Tailwind. 15 | - Registration and Login views 16 | - Field validation errors are returned from the database 17 | - Custom Login error messages if the user does not exist or the password is incorrect 18 | - Home view is guarded by authentication check. If the user has an invalid token, that route is not accessible. 19 | - Other 20 | - Deno and Faker used for seeding the database (using the generated gomarvin client 21 | - Backend code baseline and frontend fetch functions generated with gomarvin. 22 | 23 | ### DISCLAIMER 24 | 25 | A lot of parts are rough around the edges and can be improved to avoid code duplication. 26 | 27 | - JWT Auth flow in the frontend is lacking 28 | - `access_token` expiration is 15mins, no flow for re-authentication 29 | - DB tables can be improved 30 | - There are endpoints which don't execute any queries. 31 | - Frontend is as minimal as possible 32 | - No loading states while fetching the data 33 | 34 | ### Setup 35 | 36 | - Use the db dump to create the db schemas and rows. 37 | - Edit `/backend/.env` if needed 38 | 39 | ```bash 40 | # run backend 41 | cd backend 42 | go mod tidy 43 | go mod download 44 | go run main.go 45 | 46 | # run frontend 47 | cd frontend 48 | npm i 49 | npm run dev 50 | ``` 51 | 52 | #### Other 53 | 54 | ```bash 55 | # seeder (using deno with faker) 56 | deno run --allow-net ./seeder.ts 57 | 58 | # dump database info for test_db 59 | sudo pg_dump -U postgres test_db > ./dump.sql 60 | ``` 61 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | # db variables 2 | DB_HOST=localhost 3 | DB_USER=postgres 4 | DB_PASS=postgres 5 | DB_NAME=test_db 6 | DB_PORT=5432 7 | DB_SSL=disable 8 | DB_TZ=Europe/Helsinki 9 | 10 | # DATABASE_URL="://:@127.0.0.1:5432/?sslmode=disable" 11 | DATABASE_URL="postgres://postgres:postgres@127.0.0.1:5432/test_db?sslmode=disable" 12 | 13 | # go variables 14 | GO_BACKEND_PORT=4444 15 | HOST_URL=http://localhost:4444 16 | API_PATH=/api/v1 17 | FRONTEND_URL=http://localhost:3000 18 | DEBUG_MODE=true 19 | 20 | # JWT Settings 21 | JWT_KEY=6c2a18d1c7bc975467b99fadb60ad3e73532e62cac98d1a475f2d9810c210772 -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | #.env 3 | .DS_Store 4 | controllers.gen.go 5 | *.gen.txt -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from golang base image 2 | FROM golang:1.19-alpine as builder 3 | 4 | # Install git. Git is required for fetching the dependencies. 5 | RUN apk update && apk add --no-cache git 6 | 7 | # Set the current working directory inside the container 8 | WORKDIR /go_server 9 | 10 | # Copy go mod and sum files 11 | COPY go.mod go.sum ./ 12 | 13 | # Download all dependencies. Dependencies will be cached if the go.mod and the go.sum files are not changed 14 | RUN go mod download 15 | 16 | # Copy the source from the current directory to the working Directory inside the container 17 | COPY . . 18 | 19 | # Build the Go app 20 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . 21 | 22 | # Start a new stage from scratch 23 | FROM alpine:latest 24 | RUN apk --no-cache add ca-certificates 25 | 26 | WORKDIR /opt/gofiber_server 27 | 28 | # Copy the Pre-built binary file from the previous stage + .env file 29 | COPY --from=builder /go_server/main . 30 | COPY --from=builder /go_server/.env . -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # backend 2 | 3 | #### Init project structure 4 | 5 | ```bash 6 | ├── main.go # holds the server 7 | ├── app # holds the default config for server + all routes 8 | │── public # optionally serve some of the generated files for easier accesibility for frontend people 9 | ├── settings # holds util functions for loading .env vars 10 | │ └── database # holds a function that creates db connection with .env vars 11 | └── lib 12 | ├── response # predefined response function and messages 13 | └── validate # holds functions for validating payload structs 14 | └── utils # holds placeholder function used inside the controllers 15 | ``` 16 | 17 | #### Update on change 18 | 19 | ```bash 20 | # https://github.com/cespare/reflex 21 | reflex -r '\.go' -s -- sh -c "go run main.go" 22 | ``` 23 | 24 | #### Docker commands 25 | 26 | ```bash 27 | # NOTE. Dockerfile can be improved 28 | # build the image 29 | docker build -t backend . 30 | 31 | # run and publish with the name of backend 32 | docker run --publish 4444:4444 --name backend backend 33 | 34 | # stop 35 | docker stop backend 36 | 37 | # remove 38 | docker image rm backend 39 | ``` 40 | 41 | #### Renaming generated sql files 42 | 43 | ```bash 44 | # run from the root of the project 45 | for i in ./db/sql/*.sql.gen.txt; do mv "$i" "${i/.sql.gen.txt}.sql"; done 46 | ``` -------------------------------------------------------------------------------- /backend/app/router.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | res "backend/lib/response" 5 | "backend/settings" 6 | 7 | "net/http" 8 | 9 | "github.com/go-chi/chi/v5" 10 | 11 | "backend/modules/auth_module" 12 | "backend/modules/transaction_module" 13 | "backend/modules/user_module" 14 | ) 15 | 16 | func Router(app *chi.Mux) { 17 | 18 | // home_view 19 | app.Get("/", func(w http.ResponseWriter, r *http.Request) { 20 | res.Response(w, 200, nil, "Hello There!") 21 | }) 22 | 23 | api := chi.NewRouter() 24 | app.Mount(settings.API_PATH, api) 25 | 26 | user_module.Router(api) 27 | transaction_module.Router(api) 28 | auth_module.Router(api) 29 | } 30 | -------------------------------------------------------------------------------- /backend/app/server.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/go-chi/chi/v5/middleware" 6 | "github.com/go-chi/cors" 7 | ) 8 | 9 | func Start() *chi.Mux { 10 | 11 | app := chi.NewRouter() 12 | 13 | app.Use( 14 | middleware.Logger, 15 | middleware.Recoverer, 16 | middleware.AllowContentType("application/json"), 17 | middleware.ContentCharset("UTF-8", "Latin-1", ""), 18 | cors.Handler(cors.Options{ 19 | AllowedOrigins: []string{"https://*", "http://*"}, 20 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, 21 | AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, 22 | AllowCredentials: false, 23 | MaxAge: 300, 24 | }), 25 | ) 26 | 27 | Router(app) // use endpoints 28 | return app 29 | } 30 | -------------------------------------------------------------------------------- /backend/backups/balance_test_queries.txt: -------------------------------------------------------------------------------- 1 | 2 | WITH trans_sum AS ( 3 | SELECT 4 | sender_id AS user_id, 5 | SUM(CASE WHEN sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' THEN -amount ELSE amount END) AS trans_sum 6 | FROM transactions 7 | GROUP BY sender_id 8 | 9 | ) 10 | 11 | 12 | WITH trans_sum AS ( 13 | SELECT 14 | sender_id AS user_id, 15 | SUM(CASE WHEN sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' THEN -amount ELSE amount END) AS trans_sum 16 | FROM transactions 17 | GROUP BY sender_id 18 | ) SELECT COUNT(*) FROM transactions; 19 | 20 | 21 | 22 | SELECT COUNT(*) 23 | FROM transactions; 24 | 25 | 26 | SELECT SUM(R.amount), SUM(S.amount) FROM 27 | (SELECT SUM(amount) from transactions WHERE receiver_id= 'cc720122-04df-47c3-98ab-854bdedb9f8c') AS R 28 | 29 | (SELECT SUM(amount) from transactions WHERE sender?id= 'cc720122-04df-47c3-98ab-854bdedb9f8c') AS S; 30 | 31 | SELECT SUM(R.amount), SUM(S.amount) FROM 32 | (SELECT SUM(amount) from transactions WHERE receiver_id= 'cc720122-04df-47c3-98ab-854bdedb9f8c') R 33 | (SELECT SUM(amount) from transactions WHERE sender_id= 'cc720122-04df-47c3-98ab-854bdedb9f8c') S; 34 | 35 | 36 | 37 | WITH receieved_payments AS ( 38 | SELECT SUM(t.amount) 39 | FROM transactions t 40 | WHERE t.receiver_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' 41 | ), sent_payments AS ( 42 | SELECT SUM(t.amount) 43 | FROM transactions t 44 | WHERE t.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' 45 | ) SELECT receieved_payments - sent_payments; 46 | 47 | 48 | 49 | 50 | 51 | WITH sent_transactions_sum AS ( 52 | SELECT SUM(t.amount) 53 | FROM transactions t 54 | WHERE t.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c'; 55 | ) 56 | 57 | -------------------------------------------------------------------------------- /backend/db/migrations/20230116143036_init.sql: -------------------------------------------------------------------------------- 1 | -- migrate:up 2 | CREATE OR REPLACE FUNCTION trigger_set_timestamp() 3 | RETURNS TRIGGER AS $$ 4 | BEGIN 5 | NEW.updated_at = NOW(); 6 | RETURN NEW; 7 | END; 8 | $$ LANGUAGE plpgsql; 9 | 10 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 11 | 12 | 13 | -- user table 14 | CREATE TABLE IF NOT EXISTS users ( 15 | -- init 16 | user_id uuid DEFAULT uuid_generate_v4 () PRIMARY KEY, 17 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 18 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 19 | 20 | -- new columns below 21 | is_admin boolean NOT NULL DEFAULT false, 22 | username VARCHAR(300) NOT NULL UNIQUE, 23 | email VARCHAR(300) NOT NULL UNIQUE, 24 | password VARCHAR(700) NOT NULL 25 | ); 26 | 27 | -- when the row is updated, update the "updated_at" timestamp 28 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON users 29 | FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); 30 | 31 | 32 | 33 | 34 | 35 | -- migrate:down 36 | 37 | DROP TABLE IF EXISTS users; 38 | -------------------------------------------------------------------------------- /backend/db/migrations/20230117124309_transactions_table.sql: -------------------------------------------------------------------------------- 1 | -- migrate:up 2 | 3 | CREATE TABLE IF NOT EXISTS transactions ( 4 | -- init 5 | transaction_id uuid DEFAULT uuid_generate_v4 () PRIMARY KEY, 6 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 7 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 8 | 9 | -- new fields 10 | sender_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, 11 | receiver_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, 12 | amount integer NOT NULL 13 | ); 14 | 15 | -- when the row is updated, update the "updated_at" timestamp 16 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON transactions 17 | FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); 18 | 19 | 20 | -- migrate:down 21 | 22 | DROP TABLE IF EXISTS transactions; -------------------------------------------------------------------------------- /backend/db/migrations/20230119152024_balances_table.sql: -------------------------------------------------------------------------------- 1 | -- migrate:up 2 | CREATE TABLE IF NOT EXISTS balances ( 3 | -- init 4 | balance_id uuid DEFAULT uuid_generate_v4 () PRIMARY KEY, 5 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 6 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 7 | 8 | -- new columns below 9 | user_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, 10 | balance integer NOT NULL 11 | ); 12 | 13 | -- when the row is updated, update the "updated_at" timestamp 14 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON balances 15 | FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); 16 | 17 | -- migrate:down 18 | 19 | DROP TABLE IF EXISTS balances; -------------------------------------------------------------------------------- /backend/db/schema.sql: -------------------------------------------------------------------------------- 1 | SET statement_timeout = 0; 2 | SET lock_timeout = 0; 3 | SET idle_in_transaction_session_timeout = 0; 4 | SET client_encoding = 'UTF8'; 5 | SET standard_conforming_strings = on; 6 | SELECT pg_catalog.set_config('search_path', '', false); 7 | SET check_function_bodies = false; 8 | SET xmloption = content; 9 | SET client_min_messages = warning; 10 | SET row_security = off; 11 | 12 | -- 13 | -- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - 14 | -- 15 | 16 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; 17 | 18 | 19 | -- 20 | -- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: - 21 | -- 22 | 23 | COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; 24 | 25 | 26 | -- 27 | -- Name: trigger_set_timestamp(); Type: FUNCTION; Schema: public; Owner: - 28 | -- 29 | 30 | CREATE FUNCTION public.trigger_set_timestamp() RETURNS trigger 31 | LANGUAGE plpgsql 32 | AS $$ 33 | BEGIN 34 | NEW.updated_at = NOW(); 35 | RETURN NEW; 36 | END; 37 | $$; 38 | 39 | 40 | SET default_tablespace = ''; 41 | 42 | SET default_table_access_method = heap; 43 | 44 | -- 45 | -- Name: balances; Type: TABLE; Schema: public; Owner: - 46 | -- 47 | 48 | CREATE TABLE public.balances ( 49 | balance_id uuid DEFAULT public.uuid_generate_v4() NOT NULL, 50 | created_at timestamp without time zone DEFAULT now() NOT NULL, 51 | updated_at timestamp without time zone DEFAULT now() NOT NULL, 52 | user_id uuid NOT NULL, 53 | balance integer NOT NULL 54 | ); 55 | 56 | 57 | -- 58 | -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - 59 | -- 60 | 61 | CREATE TABLE public.schema_migrations ( 62 | version character varying(255) NOT NULL 63 | ); 64 | 65 | 66 | -- 67 | -- Name: transactions; Type: TABLE; Schema: public; Owner: - 68 | -- 69 | 70 | CREATE TABLE public.transactions ( 71 | transaction_id uuid DEFAULT public.uuid_generate_v4() NOT NULL, 72 | created_at timestamp without time zone DEFAULT now() NOT NULL, 73 | updated_at timestamp without time zone DEFAULT now() NOT NULL, 74 | sender_id uuid NOT NULL, 75 | receiver_id uuid NOT NULL, 76 | amount integer NOT NULL 77 | ); 78 | 79 | 80 | -- 81 | -- Name: users; Type: TABLE; Schema: public; Owner: - 82 | -- 83 | 84 | CREATE TABLE public.users ( 85 | user_id uuid DEFAULT public.uuid_generate_v4() NOT NULL, 86 | created_at timestamp without time zone DEFAULT now() NOT NULL, 87 | updated_at timestamp without time zone DEFAULT now() NOT NULL, 88 | is_admin boolean DEFAULT false NOT NULL, 89 | username character varying(300) NOT NULL, 90 | email character varying(300) NOT NULL, 91 | password character varying(700) NOT NULL 92 | ); 93 | 94 | 95 | -- 96 | -- Name: balances balances_pkey; Type: CONSTRAINT; Schema: public; Owner: - 97 | -- 98 | 99 | ALTER TABLE ONLY public.balances 100 | ADD CONSTRAINT balances_pkey PRIMARY KEY (balance_id); 101 | 102 | 103 | -- 104 | -- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - 105 | -- 106 | 107 | ALTER TABLE ONLY public.schema_migrations 108 | ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); 109 | 110 | 111 | -- 112 | -- Name: transactions transactions_pkey; Type: CONSTRAINT; Schema: public; Owner: - 113 | -- 114 | 115 | ALTER TABLE ONLY public.transactions 116 | ADD CONSTRAINT transactions_pkey PRIMARY KEY (transaction_id); 117 | 118 | 119 | -- 120 | -- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: - 121 | -- 122 | 123 | ALTER TABLE ONLY public.users 124 | ADD CONSTRAINT users_email_key UNIQUE (email); 125 | 126 | 127 | -- 128 | -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - 129 | -- 130 | 131 | ALTER TABLE ONLY public.users 132 | ADD CONSTRAINT users_pkey PRIMARY KEY (user_id); 133 | 134 | 135 | -- 136 | -- Name: users users_username_key; Type: CONSTRAINT; Schema: public; Owner: - 137 | -- 138 | 139 | ALTER TABLE ONLY public.users 140 | ADD CONSTRAINT users_username_key UNIQUE (username); 141 | 142 | 143 | -- 144 | -- Name: balances set_timestamp; Type: TRIGGER; Schema: public; Owner: - 145 | -- 146 | 147 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON public.balances FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamp(); 148 | 149 | 150 | -- 151 | -- Name: transactions set_timestamp; Type: TRIGGER; Schema: public; Owner: - 152 | -- 153 | 154 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON public.transactions FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamp(); 155 | 156 | 157 | -- 158 | -- Name: users set_timestamp; Type: TRIGGER; Schema: public; Owner: - 159 | -- 160 | 161 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamp(); 162 | 163 | 164 | -- 165 | -- Name: balances balances_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 166 | -- 167 | 168 | ALTER TABLE ONLY public.balances 169 | ADD CONSTRAINT balances_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(user_id) ON DELETE CASCADE; 170 | 171 | 172 | -- 173 | -- Name: transactions transactions_receiver_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 174 | -- 175 | 176 | ALTER TABLE ONLY public.transactions 177 | ADD CONSTRAINT transactions_receiver_id_fkey FOREIGN KEY (receiver_id) REFERENCES public.users(user_id) ON DELETE CASCADE; 178 | 179 | 180 | -- 181 | -- Name: transactions transactions_sender_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 182 | -- 183 | 184 | ALTER TABLE ONLY public.transactions 185 | ADD CONSTRAINT transactions_sender_id_fkey FOREIGN KEY (sender_id) REFERENCES public.users(user_id) ON DELETE CASCADE; 186 | 187 | 188 | -- 189 | -- PostgreSQL database dump complete 190 | -- 191 | 192 | 193 | -- 194 | -- Dbmate schema migrations 195 | -- 196 | 197 | INSERT INTO public.schema_migrations (version) VALUES 198 | ('20230101184700'), 199 | ('20230116143036'), 200 | ('20230117124309'), 201 | ('20230119152024'); 202 | -------------------------------------------------------------------------------- /backend/db/sql/balance.sql: -------------------------------------------------------------------------------- 1 | -- Code generated by gomarvin, v0.6.x 2 | 3 | CREATE TABLE IF NOT EXISTS balances ( 4 | -- init 5 | balance_id uuid DEFAULT uuid_generate_v4 () PRIMARY KEY, 6 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 7 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 8 | 9 | -- new columns below 10 | user_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, 11 | balance integer NOT NULL 12 | ); 13 | 14 | 15 | -- when the row is updated, update the "updated_at" timestamp 16 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON balances 17 | FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); 18 | 19 | 20 | -- name: Balance_GetWhereUserIdEquals :one 21 | SELECT b.balance 22 | FROM balances b 23 | JOIN transactions t ON b.user_id = t.sender_id OR b.user_id = t.receiver_id 24 | WHERE b.user_id = $1 25 | GROUP BY b.balance; 26 | 27 | 28 | -- cc720122-04df-47c3-98ab-854bdedb9f8c 29 | SELECT SUM(t.amount) 30 | FROM transactions t 31 | WHERE t.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' 32 | OR t.receiver_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' 33 | ; 34 | 35 | -- cc720122-04df-47c3-98ab-854bdedb9f8c 36 | SELECT SUM(t.amount) 37 | FROM transactions t 38 | WHERE t.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c'; 39 | 40 | 41 | -- name: Balance_UserReceivedTotalAmount :one 42 | SELECT SUM(t.amount) 43 | FROM transactions t 44 | WHERE t.receiver_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c'; 45 | 46 | -- name: Balance_UserSentTotalAmount :one 47 | SELECT SUM(t.amount) 48 | FROM transactions t 49 | WHERE t.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c'; 50 | 51 | 52 | 53 | -- name: Balance_GetUserBalanceByUserID :one 54 | SELECT SUM(CASE WHEN transactions.sender_id = @user_id::uuid THEN -transactions.amount ELSE transactions.amount END) as balance 55 | FROM transactions 56 | WHERE transactions.sender_id = @user_id::uuid OR transactions.receiver_id = @user_id::uuid; 57 | 58 | 59 | ---------------- Placeholder queries 60 | -- Return 2 rows of the results 61 | SELECT SUM(t.amount) 62 | FROM transactions t 63 | WHERE t.receiver_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' 64 | UNION 65 | SELECT SUM(t.amount) 66 | FROM transactions t 67 | WHERE t.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c'; 68 | 69 | -- Return balance for placeholder user 70 | SELECT SUM(CASE WHEN transactions.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' THEN -transactions.amount ELSE transactions.amount END) as balance 71 | FROM transactions 72 | WHERE transactions.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' OR transactions.receiver_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c'; -------------------------------------------------------------------------------- /backend/db/sql/functions.sql: -------------------------------------------------------------------------------- 1 | -- https://x-team.com/blog/automatic-timestamps-with-postgresql/ 2 | -- create a function that will update the "updated_at" timestamp when the row is changed 3 | CREATE OR REPLACE FUNCTION trigger_set_timestamp() 4 | RETURNS TRIGGER AS $$ 5 | BEGIN 6 | NEW.updated_at = NOW(); 7 | RETURN NEW; 8 | END; 9 | $$ LANGUAGE plpgsql; 10 | 11 | 12 | -- https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-uuid/ 13 | -- used for creating uuids for id columns 14 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -------------------------------------------------------------------------------- /backend/db/sql/transaction.sql: -------------------------------------------------------------------------------- 1 | -- Code generated by gomarvin, v0.6.x 2 | 3 | CREATE TABLE IF NOT EXISTS transactions ( 4 | -- init 5 | transaction_id uuid DEFAULT uuid_generate_v4 () PRIMARY KEY, 6 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 7 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 8 | 9 | -- new fields 10 | sender_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, 11 | receiver_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, 12 | amount integer NOT NULL 13 | ); 14 | 15 | 16 | -- when the row is updated, update the "updated_at" timestamp 17 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON transactions 18 | FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); 19 | 20 | 21 | -- name: Transaction_CountAll :one 22 | SELECT COUNT(*) 23 | FROM transactions; 24 | 25 | 26 | -- name: Transaction_GetAll :many 27 | SELECT transaction_id, created_at, sender_id, receiver_id 28 | FROM transactions 29 | ORDER BY created_at DESC; 30 | 31 | 32 | -- name: Transaction_GetAllWithPaginationFirstPage :many 33 | SELECT transaction_id, created_at, sender_id, receiver_id 34 | FROM transactions 35 | ORDER BY created_at DESC 36 | LIMIT $1; 37 | 38 | 39 | -- name: Transaction_GetAllWithPaginationNextPage :many 40 | SELECT transaction_id, created_at, sender_id, receiver_id 41 | FROM transactions 42 | WHERE ( created_at <= @created_at::TIMESTAMP OR 43 | ( created_at = @created_at::TIMESTAMP AND transaction_id < @transaction_id::uuid ) ) 44 | ORDER BY created_at DESC 45 | LIMIT @_limit::int; 46 | 47 | 48 | -- name: Transaction_GetAllWhereSenderId :many 49 | SELECT transaction_id, created_at, sender_id, receiver_id, amount 50 | FROM transactions 51 | WHERE sender_id = $1 52 | ORDER BY created_at DESC; 53 | 54 | 55 | -- name: Transaction_GetAllWhereReceiverId :many 56 | SELECT transaction_id, created_at, sender_id, receiver_id, amount 57 | FROM transactions 58 | WHERE receiver_id = $1 59 | ORDER BY created_at DESC; 60 | 61 | 62 | -- name: Transaction_GetAllBetweenDates :many 63 | SELECT transaction_id, created_at, sender_id, receiver_id, amount 64 | FROM transactions 65 | WHERE created_at BETWEEN SYMMETRIC $1 AND $2 66 | ORDER BY created_at DESC; 67 | 68 | 69 | -- name: Transaction_GetWhereIdEquals :one 70 | SELECT transaction_id, created_at, sender_id, receiver_id, amount 71 | FROM transactions 72 | WHERE transaction_id = $1 73 | LIMIT 1; 74 | 75 | 76 | -- name: Transaction_Create :one 77 | INSERT INTO transactions ( sender_id, receiver_id, amount ) 78 | VALUES ( $1, $2, $3 ) 79 | RETURNING *; 80 | 81 | 82 | -- name: Transaction_DeleteWhereIdEquals :one 83 | DELETE FROM transactions 84 | WHERE transaction_id = $1 85 | RETURNING *; 86 | 87 | 88 | -- name: Transaction_TransactionBotSendsBonusToUser :one 89 | INSERT INTO TRANSACTIONS ( sender_id, receiver_id, amount ) 90 | VALUES ( '899a61bf-d4e4-48d1-9274-467c50166252', $1, 1000 ) 91 | RETURNING *; 92 | 93 | 94 | -- name: Transaction_FindLastTransactionBotBonusPaymentForUser :one 95 | SELECT created_at, sender_id, receiver_id, amount 96 | FROM transactions 97 | WHERE sender_id = '899a61bf-d4e4-48d1-9274-467c50166252' AND receiver_id = $1 98 | ORDER BY created_at DESC 99 | LIMIT 1; -------------------------------------------------------------------------------- /backend/db/sql/user.sql: -------------------------------------------------------------------------------- 1 | -- Code generated by gomarvin, v0.6.x 2 | 3 | CREATE TABLE IF NOT EXISTS users ( 4 | -- init 5 | user_id uuid DEFAULT uuid_generate_v4 () PRIMARY KEY, 6 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 7 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 8 | 9 | -- new columns below 10 | is_admin boolean NOT NULL DEFAULT false, 11 | username VARCHAR(300) NOT NULL UNIQUE, 12 | email VARCHAR(300) NOT NULL UNIQUE, 13 | password VARCHAR(700) NOT NULL 14 | ); 15 | 16 | 17 | -- when the row is updated, update the "updated_at" timestamp 18 | CREATE TRIGGER set_timestamp BEFORE UPDATE ON users 19 | FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); 20 | 21 | 22 | -- name: User_CountAll :one 23 | SELECT COUNT(*) 24 | FROM users; 25 | 26 | 27 | -- name: User_GetAll :many 28 | SELECT user_id, created_at, username 29 | FROM users 30 | ORDER BY created_at DESC; 31 | 32 | 33 | -- name: User_GetAllWithPaginationFirstPage :many 34 | SELECT user_id, created_at, username 35 | FROM users 36 | ORDER BY created_at DESC 37 | LIMIT $1; 38 | 39 | 40 | -- name: User_GetAllWithPaginationNextPage :many 41 | SELECT user_id, created_at, username 42 | FROM users 43 | WHERE 44 | ( 45 | created_at <= @created_at::TIMESTAMP 46 | OR 47 | ( created_at = @created_at::TIMESTAMP AND user_id < @user_id::uuid ) 48 | ) 49 | ORDER BY created_at DESC 50 | LIMIT @_limit::int; 51 | 52 | 53 | -- name: User_GetAllWhereCreatedAt :many 54 | SELECT user_id, created_at, username 55 | FROM users 56 | WHERE created_at = $1 57 | ORDER BY created_at DESC; 58 | 59 | 60 | -- name: User_GetAllBetweenDates :many 61 | SELECT user_id, created_at, username 62 | FROM users 63 | WHERE created_at BETWEEN SYMMETRIC $1 AND $2 64 | ORDER BY created_at DESC; 65 | 66 | 67 | -- name: User_GetWhereUsernameIncludes :many 68 | SELECT user_id, created_at, username 69 | FROM users 70 | WHERE username ILIKE '%' || ( $1 ) || '%' 71 | ORDER BY created_at DESC; 72 | 73 | 74 | -- name: User_GetWhereIdEquals :one 75 | SELECT user_id, created_at, username 76 | FROM users 77 | WHERE user_id = $1 78 | LIMIT 1; 79 | 80 | -- name: User_GetWhereUsernameEquals :one 81 | SELECT user_id, created_at, username 82 | FROM users 83 | WHERE username = $1 84 | LIMIT 1; 85 | 86 | -- name: User_Create :one 87 | INSERT INTO users ( username, email, password ) 88 | VALUES ( $1, $2, $3 ) 89 | RETURNING *; 90 | 91 | -- name: User_LoginWithUsername :one 92 | SELECT * 93 | FROM users 94 | WHERE username = $1 95 | LIMIT 1; 96 | 97 | -- name: User_UpdateUsernameWhereIdEquals :one 98 | UPDATE users 99 | SET username = $1 100 | WHERE user_id = $2 101 | RETURNING *; 102 | 103 | -- name: User_UpdateUserToAdminWhereIdEquals :one 104 | UPDATE users 105 | SET is_admin = TRUE 106 | WHERE user_id = $1 107 | RETURNING *; 108 | 109 | -- name: User_UpdateUsernameWhereUsernameEquals :one 110 | UPDATE users 111 | SET username = $1 112 | WHERE username = $2 113 | RETURNING *; 114 | 115 | -- name: User_DeleteWhereIdEquals :one 116 | DELETE FROM users 117 | WHERE user_id = $1 118 | RETURNING *; 119 | 120 | 121 | -- name: User_GetSentTransactionsWhereUserIdEqualsFirstPage :many 122 | SELECT 123 | users.user_id, users.username, 124 | transactions.transaction_id, transactions.amount, transactions.created_at 125 | FROM 126 | users 127 | INNER JOIN transactions 128 | ON @user_id::uuid = transactions.sender_id 129 | ORDER BY transactions.created_at DESC 130 | LIMIT @_limit::int; 131 | 132 | 133 | -- name: User_GetSentTransactionsWhereUserIdEquals :many 134 | SELECT 135 | users.user_id, users.username, 136 | transactions.transaction_id, transactions.amount, transactions.created_at 137 | FROM 138 | users 139 | INNER JOIN transactions 140 | ON @user_id::uuid = transactions.sender_id 141 | WHERE 142 | ( transactions.created_at <= @created_at::TIMESTAMP OR 143 | ( transactions.created_at = @created_at::TIMESTAMP AND users.user_id < @user_id::uuid )) 144 | ORDER BY transactions.created_at DESC 145 | LIMIT @_limit::int; 146 | -------------------------------------------------------------------------------- /backend/db/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | packages: 3 | - path: "sqlc" # name of the dir / package that holds the generated code 4 | schema: "./sql/" # path to the the dir that holds all of the tables 5 | queries: "./sql/" # path to the the dir that holds all of the queries 6 | engine: "postgresql" # db type that will be used 7 | emit_json_tags: true 8 | 9 | 10 | # Notes 11 | # - Link to SQLC Documentation -> https://docs.sqlc.dev/en/stable/ 12 | # - If running on windows and need postgres support, install wsl and dowload sqlc binary to generate code 13 | # - The generated tables come with 3 predefined columns that would be used by most of the tables 14 | # - Additionally, there is a change_me column used for testing purposes. 15 | -------------------------------------------------------------------------------- /backend/db/sqlc/balance.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.15.0 4 | // source: balance.sql 5 | 6 | package sqlc 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/google/uuid" 12 | ) 13 | 14 | const balance_GetUserBalanceByUserID = `-- name: Balance_GetUserBalanceByUserID :one 15 | SELECT SUM(CASE WHEN transactions.sender_id = $1::uuid THEN -transactions.amount ELSE transactions.amount END) as balance 16 | FROM transactions 17 | WHERE transactions.sender_id = $1::uuid OR transactions.receiver_id = $1::uuid 18 | ` 19 | 20 | func (q *Queries) Balance_GetUserBalanceByUserID(ctx context.Context, userID uuid.UUID) (int64, error) { 21 | row := q.db.QueryRowContext(ctx, balance_GetUserBalanceByUserID, userID) 22 | var balance int64 23 | err := row.Scan(&balance) 24 | return balance, err 25 | } 26 | 27 | const balance_GetWhereUserIdEquals = `-- name: Balance_GetWhereUserIdEquals :one 28 | SELECT b.balance 29 | FROM balances b 30 | JOIN transactions t ON b.user_id = t.sender_id OR b.user_id = t.receiver_id 31 | WHERE b.user_id = $1 32 | GROUP BY b.balance 33 | ` 34 | 35 | func (q *Queries) Balance_GetWhereUserIdEquals(ctx context.Context, userID uuid.UUID) (int32, error) { 36 | row := q.db.QueryRowContext(ctx, balance_GetWhereUserIdEquals, userID) 37 | var balance int32 38 | err := row.Scan(&balance) 39 | return balance, err 40 | } 41 | 42 | const balance_UserReceivedTotalAmount = `-- name: Balance_UserReceivedTotalAmount :one 43 | SELECT SUM(t.amount) 44 | FROM transactions t 45 | WHERE t.receiver_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' 46 | ` 47 | 48 | func (q *Queries) Balance_UserReceivedTotalAmount(ctx context.Context) (int64, error) { 49 | row := q.db.QueryRowContext(ctx, balance_UserReceivedTotalAmount) 50 | var sum int64 51 | err := row.Scan(&sum) 52 | return sum, err 53 | } 54 | 55 | const balance_UserSentTotalAmount = `-- name: Balance_UserSentTotalAmount :one 56 | SELECT SUM(t.amount) 57 | FROM transactions t 58 | WHERE t.sender_id = 'cc720122-04df-47c3-98ab-854bdedb9f8c' 59 | ` 60 | 61 | func (q *Queries) Balance_UserSentTotalAmount(ctx context.Context) (int64, error) { 62 | row := q.db.QueryRowContext(ctx, balance_UserSentTotalAmount) 63 | var sum int64 64 | err := row.Scan(&sum) 65 | return sum, err 66 | } 67 | -------------------------------------------------------------------------------- /backend/db/sqlc/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.15.0 4 | 5 | package sqlc 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/db/sqlc/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.15.0 4 | 5 | package sqlc 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type Balance struct { 14 | BalanceID uuid.UUID `json:"balance_id"` 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | UserID uuid.UUID `json:"user_id"` 18 | Balance int32 `json:"balance"` 19 | } 20 | 21 | type Transaction struct { 22 | TransactionID uuid.UUID `json:"transaction_id"` 23 | CreatedAt time.Time `json:"created_at"` 24 | UpdatedAt time.Time `json:"updated_at"` 25 | SenderID uuid.UUID `json:"sender_id"` 26 | ReceiverID uuid.UUID `json:"receiver_id"` 27 | Amount int32 `json:"amount"` 28 | } 29 | 30 | type User struct { 31 | UserID uuid.UUID `json:"user_id"` 32 | CreatedAt time.Time `json:"created_at"` 33 | UpdatedAt time.Time `json:"updated_at"` 34 | IsAdmin bool `json:"is_admin"` 35 | Username string `json:"username"` 36 | Email string `json:"email"` 37 | Password string `json:"password"` 38 | } 39 | -------------------------------------------------------------------------------- /backend/db/sqlc/transaction.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.15.0 4 | // source: transaction.sql 5 | 6 | package sqlc 7 | 8 | import ( 9 | "context" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | ) 14 | 15 | const transaction_CountAll = `-- name: Transaction_CountAll :one 16 | SELECT COUNT(*) 17 | FROM transactions 18 | ` 19 | 20 | func (q *Queries) Transaction_CountAll(ctx context.Context) (int64, error) { 21 | row := q.db.QueryRowContext(ctx, transaction_CountAll) 22 | var count int64 23 | err := row.Scan(&count) 24 | return count, err 25 | } 26 | 27 | const transaction_Create = `-- name: Transaction_Create :one 28 | INSERT INTO transactions ( sender_id, receiver_id, amount ) 29 | VALUES ( $1, $2, $3 ) 30 | RETURNING transaction_id, created_at, updated_at, sender_id, receiver_id, amount 31 | ` 32 | 33 | type Transaction_CreateParams struct { 34 | SenderID uuid.UUID `json:"sender_id"` 35 | ReceiverID uuid.UUID `json:"receiver_id"` 36 | Amount int32 `json:"amount"` 37 | } 38 | 39 | func (q *Queries) Transaction_Create(ctx context.Context, arg Transaction_CreateParams) (Transaction, error) { 40 | row := q.db.QueryRowContext(ctx, transaction_Create, arg.SenderID, arg.ReceiverID, arg.Amount) 41 | var i Transaction 42 | err := row.Scan( 43 | &i.TransactionID, 44 | &i.CreatedAt, 45 | &i.UpdatedAt, 46 | &i.SenderID, 47 | &i.ReceiverID, 48 | &i.Amount, 49 | ) 50 | return i, err 51 | } 52 | 53 | const transaction_DeleteWhereIdEquals = `-- name: Transaction_DeleteWhereIdEquals :one 54 | DELETE FROM transactions 55 | WHERE transaction_id = $1 56 | RETURNING transaction_id, created_at, updated_at, sender_id, receiver_id, amount 57 | ` 58 | 59 | func (q *Queries) Transaction_DeleteWhereIdEquals(ctx context.Context, transactionID uuid.UUID) (Transaction, error) { 60 | row := q.db.QueryRowContext(ctx, transaction_DeleteWhereIdEquals, transactionID) 61 | var i Transaction 62 | err := row.Scan( 63 | &i.TransactionID, 64 | &i.CreatedAt, 65 | &i.UpdatedAt, 66 | &i.SenderID, 67 | &i.ReceiverID, 68 | &i.Amount, 69 | ) 70 | return i, err 71 | } 72 | 73 | const transaction_FindLastTransactionBotBonusPaymentForUser = `-- name: Transaction_FindLastTransactionBotBonusPaymentForUser :one 74 | SELECT created_at, sender_id, receiver_id, amount 75 | FROM transactions 76 | WHERE sender_id = '899a61bf-d4e4-48d1-9274-467c50166252' AND receiver_id = $1 77 | ORDER BY created_at DESC 78 | LIMIT 1 79 | ` 80 | 81 | type Transaction_FindLastTransactionBotBonusPaymentForUserRow struct { 82 | CreatedAt time.Time `json:"created_at"` 83 | SenderID uuid.UUID `json:"sender_id"` 84 | ReceiverID uuid.UUID `json:"receiver_id"` 85 | Amount int32 `json:"amount"` 86 | } 87 | 88 | func (q *Queries) Transaction_FindLastTransactionBotBonusPaymentForUser(ctx context.Context, receiverID uuid.UUID) (Transaction_FindLastTransactionBotBonusPaymentForUserRow, error) { 89 | row := q.db.QueryRowContext(ctx, transaction_FindLastTransactionBotBonusPaymentForUser, receiverID) 90 | var i Transaction_FindLastTransactionBotBonusPaymentForUserRow 91 | err := row.Scan( 92 | &i.CreatedAt, 93 | &i.SenderID, 94 | &i.ReceiverID, 95 | &i.Amount, 96 | ) 97 | return i, err 98 | } 99 | 100 | const transaction_GetAll = `-- name: Transaction_GetAll :many 101 | SELECT transaction_id, created_at, sender_id, receiver_id 102 | FROM transactions 103 | ORDER BY created_at DESC 104 | ` 105 | 106 | type Transaction_GetAllRow struct { 107 | TransactionID uuid.UUID `json:"transaction_id"` 108 | CreatedAt time.Time `json:"created_at"` 109 | SenderID uuid.UUID `json:"sender_id"` 110 | ReceiverID uuid.UUID `json:"receiver_id"` 111 | } 112 | 113 | func (q *Queries) Transaction_GetAll(ctx context.Context) ([]Transaction_GetAllRow, error) { 114 | rows, err := q.db.QueryContext(ctx, transaction_GetAll) 115 | if err != nil { 116 | return nil, err 117 | } 118 | defer rows.Close() 119 | var items []Transaction_GetAllRow 120 | for rows.Next() { 121 | var i Transaction_GetAllRow 122 | if err := rows.Scan( 123 | &i.TransactionID, 124 | &i.CreatedAt, 125 | &i.SenderID, 126 | &i.ReceiverID, 127 | ); err != nil { 128 | return nil, err 129 | } 130 | items = append(items, i) 131 | } 132 | if err := rows.Close(); err != nil { 133 | return nil, err 134 | } 135 | if err := rows.Err(); err != nil { 136 | return nil, err 137 | } 138 | return items, nil 139 | } 140 | 141 | const transaction_GetAllBetweenDates = `-- name: Transaction_GetAllBetweenDates :many 142 | SELECT transaction_id, created_at, sender_id, receiver_id, amount 143 | FROM transactions 144 | WHERE created_at BETWEEN SYMMETRIC $1 AND $2 145 | ORDER BY created_at DESC 146 | ` 147 | 148 | type Transaction_GetAllBetweenDatesParams struct { 149 | CreatedAt time.Time `json:"created_at"` 150 | CreatedAt_2 time.Time `json:"created_at_2"` 151 | } 152 | 153 | type Transaction_GetAllBetweenDatesRow struct { 154 | TransactionID uuid.UUID `json:"transaction_id"` 155 | CreatedAt time.Time `json:"created_at"` 156 | SenderID uuid.UUID `json:"sender_id"` 157 | ReceiverID uuid.UUID `json:"receiver_id"` 158 | Amount int32 `json:"amount"` 159 | } 160 | 161 | func (q *Queries) Transaction_GetAllBetweenDates(ctx context.Context, arg Transaction_GetAllBetweenDatesParams) ([]Transaction_GetAllBetweenDatesRow, error) { 162 | rows, err := q.db.QueryContext(ctx, transaction_GetAllBetweenDates, arg.CreatedAt, arg.CreatedAt_2) 163 | if err != nil { 164 | return nil, err 165 | } 166 | defer rows.Close() 167 | var items []Transaction_GetAllBetweenDatesRow 168 | for rows.Next() { 169 | var i Transaction_GetAllBetweenDatesRow 170 | if err := rows.Scan( 171 | &i.TransactionID, 172 | &i.CreatedAt, 173 | &i.SenderID, 174 | &i.ReceiverID, 175 | &i.Amount, 176 | ); err != nil { 177 | return nil, err 178 | } 179 | items = append(items, i) 180 | } 181 | if err := rows.Close(); err != nil { 182 | return nil, err 183 | } 184 | if err := rows.Err(); err != nil { 185 | return nil, err 186 | } 187 | return items, nil 188 | } 189 | 190 | const transaction_GetAllWhereReceiverId = `-- name: Transaction_GetAllWhereReceiverId :many 191 | SELECT transaction_id, created_at, sender_id, receiver_id, amount 192 | FROM transactions 193 | WHERE receiver_id = $1 194 | ORDER BY created_at DESC 195 | ` 196 | 197 | type Transaction_GetAllWhereReceiverIdRow struct { 198 | TransactionID uuid.UUID `json:"transaction_id"` 199 | CreatedAt time.Time `json:"created_at"` 200 | SenderID uuid.UUID `json:"sender_id"` 201 | ReceiverID uuid.UUID `json:"receiver_id"` 202 | Amount int32 `json:"amount"` 203 | } 204 | 205 | func (q *Queries) Transaction_GetAllWhereReceiverId(ctx context.Context, receiverID uuid.UUID) ([]Transaction_GetAllWhereReceiverIdRow, error) { 206 | rows, err := q.db.QueryContext(ctx, transaction_GetAllWhereReceiverId, receiverID) 207 | if err != nil { 208 | return nil, err 209 | } 210 | defer rows.Close() 211 | var items []Transaction_GetAllWhereReceiverIdRow 212 | for rows.Next() { 213 | var i Transaction_GetAllWhereReceiverIdRow 214 | if err := rows.Scan( 215 | &i.TransactionID, 216 | &i.CreatedAt, 217 | &i.SenderID, 218 | &i.ReceiverID, 219 | &i.Amount, 220 | ); err != nil { 221 | return nil, err 222 | } 223 | items = append(items, i) 224 | } 225 | if err := rows.Close(); err != nil { 226 | return nil, err 227 | } 228 | if err := rows.Err(); err != nil { 229 | return nil, err 230 | } 231 | return items, nil 232 | } 233 | 234 | const transaction_GetAllWhereSenderId = `-- name: Transaction_GetAllWhereSenderId :many 235 | SELECT transaction_id, created_at, sender_id, receiver_id, amount 236 | FROM transactions 237 | WHERE sender_id = $1 238 | ORDER BY created_at DESC 239 | ` 240 | 241 | type Transaction_GetAllWhereSenderIdRow struct { 242 | TransactionID uuid.UUID `json:"transaction_id"` 243 | CreatedAt time.Time `json:"created_at"` 244 | SenderID uuid.UUID `json:"sender_id"` 245 | ReceiverID uuid.UUID `json:"receiver_id"` 246 | Amount int32 `json:"amount"` 247 | } 248 | 249 | func (q *Queries) Transaction_GetAllWhereSenderId(ctx context.Context, senderID uuid.UUID) ([]Transaction_GetAllWhereSenderIdRow, error) { 250 | rows, err := q.db.QueryContext(ctx, transaction_GetAllWhereSenderId, senderID) 251 | if err != nil { 252 | return nil, err 253 | } 254 | defer rows.Close() 255 | var items []Transaction_GetAllWhereSenderIdRow 256 | for rows.Next() { 257 | var i Transaction_GetAllWhereSenderIdRow 258 | if err := rows.Scan( 259 | &i.TransactionID, 260 | &i.CreatedAt, 261 | &i.SenderID, 262 | &i.ReceiverID, 263 | &i.Amount, 264 | ); err != nil { 265 | return nil, err 266 | } 267 | items = append(items, i) 268 | } 269 | if err := rows.Close(); err != nil { 270 | return nil, err 271 | } 272 | if err := rows.Err(); err != nil { 273 | return nil, err 274 | } 275 | return items, nil 276 | } 277 | 278 | const transaction_GetAllWithPaginationFirstPage = `-- name: Transaction_GetAllWithPaginationFirstPage :many 279 | SELECT transaction_id, created_at, sender_id, receiver_id 280 | FROM transactions 281 | ORDER BY created_at DESC 282 | LIMIT $1 283 | ` 284 | 285 | type Transaction_GetAllWithPaginationFirstPageRow struct { 286 | TransactionID uuid.UUID `json:"transaction_id"` 287 | CreatedAt time.Time `json:"created_at"` 288 | SenderID uuid.UUID `json:"sender_id"` 289 | ReceiverID uuid.UUID `json:"receiver_id"` 290 | } 291 | 292 | func (q *Queries) Transaction_GetAllWithPaginationFirstPage(ctx context.Context, limit int32) ([]Transaction_GetAllWithPaginationFirstPageRow, error) { 293 | rows, err := q.db.QueryContext(ctx, transaction_GetAllWithPaginationFirstPage, limit) 294 | if err != nil { 295 | return nil, err 296 | } 297 | defer rows.Close() 298 | var items []Transaction_GetAllWithPaginationFirstPageRow 299 | for rows.Next() { 300 | var i Transaction_GetAllWithPaginationFirstPageRow 301 | if err := rows.Scan( 302 | &i.TransactionID, 303 | &i.CreatedAt, 304 | &i.SenderID, 305 | &i.ReceiverID, 306 | ); err != nil { 307 | return nil, err 308 | } 309 | items = append(items, i) 310 | } 311 | if err := rows.Close(); err != nil { 312 | return nil, err 313 | } 314 | if err := rows.Err(); err != nil { 315 | return nil, err 316 | } 317 | return items, nil 318 | } 319 | 320 | const transaction_GetAllWithPaginationNextPage = `-- name: Transaction_GetAllWithPaginationNextPage :many 321 | SELECT transaction_id, created_at, sender_id, receiver_id 322 | FROM transactions 323 | WHERE ( created_at <= $1::TIMESTAMP OR 324 | ( created_at = $1::TIMESTAMP AND transaction_id < $2::uuid ) ) 325 | ORDER BY created_at DESC 326 | LIMIT $3::int 327 | ` 328 | 329 | type Transaction_GetAllWithPaginationNextPageParams struct { 330 | CreatedAt time.Time `json:"created_at"` 331 | TransactionID uuid.UUID `json:"transaction_id"` 332 | Limit int32 `json:"_limit"` 333 | } 334 | 335 | type Transaction_GetAllWithPaginationNextPageRow struct { 336 | TransactionID uuid.UUID `json:"transaction_id"` 337 | CreatedAt time.Time `json:"created_at"` 338 | SenderID uuid.UUID `json:"sender_id"` 339 | ReceiverID uuid.UUID `json:"receiver_id"` 340 | } 341 | 342 | func (q *Queries) Transaction_GetAllWithPaginationNextPage(ctx context.Context, arg Transaction_GetAllWithPaginationNextPageParams) ([]Transaction_GetAllWithPaginationNextPageRow, error) { 343 | rows, err := q.db.QueryContext(ctx, transaction_GetAllWithPaginationNextPage, arg.CreatedAt, arg.TransactionID, arg.Limit) 344 | if err != nil { 345 | return nil, err 346 | } 347 | defer rows.Close() 348 | var items []Transaction_GetAllWithPaginationNextPageRow 349 | for rows.Next() { 350 | var i Transaction_GetAllWithPaginationNextPageRow 351 | if err := rows.Scan( 352 | &i.TransactionID, 353 | &i.CreatedAt, 354 | &i.SenderID, 355 | &i.ReceiverID, 356 | ); err != nil { 357 | return nil, err 358 | } 359 | items = append(items, i) 360 | } 361 | if err := rows.Close(); err != nil { 362 | return nil, err 363 | } 364 | if err := rows.Err(); err != nil { 365 | return nil, err 366 | } 367 | return items, nil 368 | } 369 | 370 | const transaction_GetWhereIdEquals = `-- name: Transaction_GetWhereIdEquals :one 371 | SELECT transaction_id, created_at, sender_id, receiver_id, amount 372 | FROM transactions 373 | WHERE transaction_id = $1 374 | LIMIT 1 375 | ` 376 | 377 | type Transaction_GetWhereIdEqualsRow struct { 378 | TransactionID uuid.UUID `json:"transaction_id"` 379 | CreatedAt time.Time `json:"created_at"` 380 | SenderID uuid.UUID `json:"sender_id"` 381 | ReceiverID uuid.UUID `json:"receiver_id"` 382 | Amount int32 `json:"amount"` 383 | } 384 | 385 | func (q *Queries) Transaction_GetWhereIdEquals(ctx context.Context, transactionID uuid.UUID) (Transaction_GetWhereIdEqualsRow, error) { 386 | row := q.db.QueryRowContext(ctx, transaction_GetWhereIdEquals, transactionID) 387 | var i Transaction_GetWhereIdEqualsRow 388 | err := row.Scan( 389 | &i.TransactionID, 390 | &i.CreatedAt, 391 | &i.SenderID, 392 | &i.ReceiverID, 393 | &i.Amount, 394 | ) 395 | return i, err 396 | } 397 | 398 | const transaction_TransactionBotSendsBonusToUser = `-- name: Transaction_TransactionBotSendsBonusToUser :one 399 | INSERT INTO TRANSACTIONS ( sender_id, receiver_id, amount ) 400 | VALUES ( '899a61bf-d4e4-48d1-9274-467c50166252', $1, 1000 ) 401 | RETURNING transaction_id, created_at, updated_at, sender_id, receiver_id, amount 402 | ` 403 | 404 | func (q *Queries) Transaction_TransactionBotSendsBonusToUser(ctx context.Context, receiverID uuid.UUID) (Transaction, error) { 405 | row := q.db.QueryRowContext(ctx, transaction_TransactionBotSendsBonusToUser, receiverID) 406 | var i Transaction 407 | err := row.Scan( 408 | &i.TransactionID, 409 | &i.CreatedAt, 410 | &i.UpdatedAt, 411 | &i.SenderID, 412 | &i.ReceiverID, 413 | &i.Amount, 414 | ) 415 | return i, err 416 | } 417 | -------------------------------------------------------------------------------- /backend/db/sqlc/user.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.15.0 4 | // source: user.sql 5 | 6 | package sqlc 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | "time" 12 | 13 | "github.com/google/uuid" 14 | ) 15 | 16 | const user_CountAll = `-- name: User_CountAll :one 17 | SELECT COUNT(*) 18 | FROM users 19 | ` 20 | 21 | func (q *Queries) User_CountAll(ctx context.Context) (int64, error) { 22 | row := q.db.QueryRowContext(ctx, user_CountAll) 23 | var count int64 24 | err := row.Scan(&count) 25 | return count, err 26 | } 27 | 28 | const user_Create = `-- name: User_Create :one 29 | INSERT INTO users ( username, email, password ) 30 | VALUES ( $1, $2, $3 ) 31 | RETURNING user_id, created_at, updated_at, is_admin, username, email, password 32 | ` 33 | 34 | type User_CreateParams struct { 35 | Username string `json:"username"` 36 | Email string `json:"email"` 37 | Password string `json:"password"` 38 | } 39 | 40 | func (q *Queries) User_Create(ctx context.Context, arg User_CreateParams) (User, error) { 41 | row := q.db.QueryRowContext(ctx, user_Create, arg.Username, arg.Email, arg.Password) 42 | var i User 43 | err := row.Scan( 44 | &i.UserID, 45 | &i.CreatedAt, 46 | &i.UpdatedAt, 47 | &i.IsAdmin, 48 | &i.Username, 49 | &i.Email, 50 | &i.Password, 51 | ) 52 | return i, err 53 | } 54 | 55 | const user_DeleteWhereIdEquals = `-- name: User_DeleteWhereIdEquals :one 56 | DELETE FROM users 57 | WHERE user_id = $1 58 | RETURNING user_id, created_at, updated_at, is_admin, username, email, password 59 | ` 60 | 61 | func (q *Queries) User_DeleteWhereIdEquals(ctx context.Context, userID uuid.UUID) (User, error) { 62 | row := q.db.QueryRowContext(ctx, user_DeleteWhereIdEquals, userID) 63 | var i User 64 | err := row.Scan( 65 | &i.UserID, 66 | &i.CreatedAt, 67 | &i.UpdatedAt, 68 | &i.IsAdmin, 69 | &i.Username, 70 | &i.Email, 71 | &i.Password, 72 | ) 73 | return i, err 74 | } 75 | 76 | const user_GetAll = `-- name: User_GetAll :many 77 | SELECT user_id, created_at, username 78 | FROM users 79 | ORDER BY created_at DESC 80 | ` 81 | 82 | type User_GetAllRow struct { 83 | UserID uuid.UUID `json:"user_id"` 84 | CreatedAt time.Time `json:"created_at"` 85 | Username string `json:"username"` 86 | } 87 | 88 | func (q *Queries) User_GetAll(ctx context.Context) ([]User_GetAllRow, error) { 89 | rows, err := q.db.QueryContext(ctx, user_GetAll) 90 | if err != nil { 91 | return nil, err 92 | } 93 | defer rows.Close() 94 | var items []User_GetAllRow 95 | for rows.Next() { 96 | var i User_GetAllRow 97 | if err := rows.Scan(&i.UserID, &i.CreatedAt, &i.Username); err != nil { 98 | return nil, err 99 | } 100 | items = append(items, i) 101 | } 102 | if err := rows.Close(); err != nil { 103 | return nil, err 104 | } 105 | if err := rows.Err(); err != nil { 106 | return nil, err 107 | } 108 | return items, nil 109 | } 110 | 111 | const user_GetAllBetweenDates = `-- name: User_GetAllBetweenDates :many 112 | SELECT user_id, created_at, username 113 | FROM users 114 | WHERE created_at BETWEEN SYMMETRIC $1 AND $2 115 | ORDER BY created_at DESC 116 | ` 117 | 118 | type User_GetAllBetweenDatesParams struct { 119 | CreatedAt time.Time `json:"created_at"` 120 | CreatedAt_2 time.Time `json:"created_at_2"` 121 | } 122 | 123 | type User_GetAllBetweenDatesRow struct { 124 | UserID uuid.UUID `json:"user_id"` 125 | CreatedAt time.Time `json:"created_at"` 126 | Username string `json:"username"` 127 | } 128 | 129 | func (q *Queries) User_GetAllBetweenDates(ctx context.Context, arg User_GetAllBetweenDatesParams) ([]User_GetAllBetweenDatesRow, error) { 130 | rows, err := q.db.QueryContext(ctx, user_GetAllBetweenDates, arg.CreatedAt, arg.CreatedAt_2) 131 | if err != nil { 132 | return nil, err 133 | } 134 | defer rows.Close() 135 | var items []User_GetAllBetweenDatesRow 136 | for rows.Next() { 137 | var i User_GetAllBetweenDatesRow 138 | if err := rows.Scan(&i.UserID, &i.CreatedAt, &i.Username); err != nil { 139 | return nil, err 140 | } 141 | items = append(items, i) 142 | } 143 | if err := rows.Close(); err != nil { 144 | return nil, err 145 | } 146 | if err := rows.Err(); err != nil { 147 | return nil, err 148 | } 149 | return items, nil 150 | } 151 | 152 | const user_GetAllWhereCreatedAt = `-- name: User_GetAllWhereCreatedAt :many 153 | SELECT user_id, created_at, username 154 | FROM users 155 | WHERE created_at = $1 156 | ORDER BY created_at DESC 157 | ` 158 | 159 | type User_GetAllWhereCreatedAtRow struct { 160 | UserID uuid.UUID `json:"user_id"` 161 | CreatedAt time.Time `json:"created_at"` 162 | Username string `json:"username"` 163 | } 164 | 165 | func (q *Queries) User_GetAllWhereCreatedAt(ctx context.Context, createdAt time.Time) ([]User_GetAllWhereCreatedAtRow, error) { 166 | rows, err := q.db.QueryContext(ctx, user_GetAllWhereCreatedAt, createdAt) 167 | if err != nil { 168 | return nil, err 169 | } 170 | defer rows.Close() 171 | var items []User_GetAllWhereCreatedAtRow 172 | for rows.Next() { 173 | var i User_GetAllWhereCreatedAtRow 174 | if err := rows.Scan(&i.UserID, &i.CreatedAt, &i.Username); err != nil { 175 | return nil, err 176 | } 177 | items = append(items, i) 178 | } 179 | if err := rows.Close(); err != nil { 180 | return nil, err 181 | } 182 | if err := rows.Err(); err != nil { 183 | return nil, err 184 | } 185 | return items, nil 186 | } 187 | 188 | const user_GetAllWithPaginationFirstPage = `-- name: User_GetAllWithPaginationFirstPage :many 189 | SELECT user_id, created_at, username 190 | FROM users 191 | ORDER BY created_at DESC 192 | LIMIT $1 193 | ` 194 | 195 | type User_GetAllWithPaginationFirstPageRow struct { 196 | UserID uuid.UUID `json:"user_id"` 197 | CreatedAt time.Time `json:"created_at"` 198 | Username string `json:"username"` 199 | } 200 | 201 | func (q *Queries) User_GetAllWithPaginationFirstPage(ctx context.Context, limit int32) ([]User_GetAllWithPaginationFirstPageRow, error) { 202 | rows, err := q.db.QueryContext(ctx, user_GetAllWithPaginationFirstPage, limit) 203 | if err != nil { 204 | return nil, err 205 | } 206 | defer rows.Close() 207 | var items []User_GetAllWithPaginationFirstPageRow 208 | for rows.Next() { 209 | var i User_GetAllWithPaginationFirstPageRow 210 | if err := rows.Scan(&i.UserID, &i.CreatedAt, &i.Username); err != nil { 211 | return nil, err 212 | } 213 | items = append(items, i) 214 | } 215 | if err := rows.Close(); err != nil { 216 | return nil, err 217 | } 218 | if err := rows.Err(); err != nil { 219 | return nil, err 220 | } 221 | return items, nil 222 | } 223 | 224 | const user_GetAllWithPaginationNextPage = `-- name: User_GetAllWithPaginationNextPage :many 225 | SELECT user_id, created_at, username 226 | FROM users 227 | WHERE 228 | ( 229 | created_at <= $1::TIMESTAMP 230 | OR 231 | ( created_at = $1::TIMESTAMP AND user_id < $2::uuid ) 232 | ) 233 | ORDER BY created_at DESC 234 | LIMIT $3::int 235 | ` 236 | 237 | type User_GetAllWithPaginationNextPageParams struct { 238 | CreatedAt time.Time `json:"created_at"` 239 | UserID uuid.UUID `json:"user_id"` 240 | Limit int32 `json:"_limit"` 241 | } 242 | 243 | type User_GetAllWithPaginationNextPageRow struct { 244 | UserID uuid.UUID `json:"user_id"` 245 | CreatedAt time.Time `json:"created_at"` 246 | Username string `json:"username"` 247 | } 248 | 249 | func (q *Queries) User_GetAllWithPaginationNextPage(ctx context.Context, arg User_GetAllWithPaginationNextPageParams) ([]User_GetAllWithPaginationNextPageRow, error) { 250 | rows, err := q.db.QueryContext(ctx, user_GetAllWithPaginationNextPage, arg.CreatedAt, arg.UserID, arg.Limit) 251 | if err != nil { 252 | return nil, err 253 | } 254 | defer rows.Close() 255 | var items []User_GetAllWithPaginationNextPageRow 256 | for rows.Next() { 257 | var i User_GetAllWithPaginationNextPageRow 258 | if err := rows.Scan(&i.UserID, &i.CreatedAt, &i.Username); err != nil { 259 | return nil, err 260 | } 261 | items = append(items, i) 262 | } 263 | if err := rows.Close(); err != nil { 264 | return nil, err 265 | } 266 | if err := rows.Err(); err != nil { 267 | return nil, err 268 | } 269 | return items, nil 270 | } 271 | 272 | const user_GetSentTransactionsWhereUserIdEquals = `-- name: User_GetSentTransactionsWhereUserIdEquals :many 273 | SELECT 274 | users.user_id, users.username, 275 | transactions.transaction_id, transactions.amount, transactions.created_at 276 | FROM 277 | users 278 | INNER JOIN transactions 279 | ON $1::uuid = transactions.sender_id 280 | WHERE 281 | ( transactions.created_at <= $2::TIMESTAMP OR 282 | ( transactions.created_at = $2::TIMESTAMP AND users.user_id < $1::uuid )) 283 | ORDER BY transactions.created_at DESC 284 | LIMIT $3::int 285 | ` 286 | 287 | type User_GetSentTransactionsWhereUserIdEqualsParams struct { 288 | UserID uuid.UUID `json:"user_id"` 289 | CreatedAt time.Time `json:"created_at"` 290 | Limit int32 `json:"_limit"` 291 | } 292 | 293 | type User_GetSentTransactionsWhereUserIdEqualsRow struct { 294 | UserID uuid.UUID `json:"user_id"` 295 | Username string `json:"username"` 296 | TransactionID uuid.UUID `json:"transaction_id"` 297 | Amount int32 `json:"amount"` 298 | CreatedAt time.Time `json:"created_at"` 299 | } 300 | 301 | func (q *Queries) User_GetSentTransactionsWhereUserIdEquals(ctx context.Context, arg User_GetSentTransactionsWhereUserIdEqualsParams) ([]User_GetSentTransactionsWhereUserIdEqualsRow, error) { 302 | rows, err := q.db.QueryContext(ctx, user_GetSentTransactionsWhereUserIdEquals, arg.UserID, arg.CreatedAt, arg.Limit) 303 | if err != nil { 304 | return nil, err 305 | } 306 | defer rows.Close() 307 | var items []User_GetSentTransactionsWhereUserIdEqualsRow 308 | for rows.Next() { 309 | var i User_GetSentTransactionsWhereUserIdEqualsRow 310 | if err := rows.Scan( 311 | &i.UserID, 312 | &i.Username, 313 | &i.TransactionID, 314 | &i.Amount, 315 | &i.CreatedAt, 316 | ); err != nil { 317 | return nil, err 318 | } 319 | items = append(items, i) 320 | } 321 | if err := rows.Close(); err != nil { 322 | return nil, err 323 | } 324 | if err := rows.Err(); err != nil { 325 | return nil, err 326 | } 327 | return items, nil 328 | } 329 | 330 | const user_GetSentTransactionsWhereUserIdEqualsFirstPage = `-- name: User_GetSentTransactionsWhereUserIdEqualsFirstPage :many 331 | SELECT 332 | users.user_id, users.username, 333 | transactions.transaction_id, transactions.amount, transactions.created_at 334 | FROM 335 | users 336 | INNER JOIN transactions 337 | ON $1::uuid = transactions.sender_id 338 | ORDER BY transactions.created_at DESC 339 | LIMIT $2::int 340 | ` 341 | 342 | type User_GetSentTransactionsWhereUserIdEqualsFirstPageParams struct { 343 | UserID uuid.UUID `json:"user_id"` 344 | Limit int32 `json:"_limit"` 345 | } 346 | 347 | type User_GetSentTransactionsWhereUserIdEqualsFirstPageRow struct { 348 | UserID uuid.UUID `json:"user_id"` 349 | Username string `json:"username"` 350 | TransactionID uuid.UUID `json:"transaction_id"` 351 | Amount int32 `json:"amount"` 352 | CreatedAt time.Time `json:"created_at"` 353 | } 354 | 355 | func (q *Queries) User_GetSentTransactionsWhereUserIdEqualsFirstPage(ctx context.Context, arg User_GetSentTransactionsWhereUserIdEqualsFirstPageParams) ([]User_GetSentTransactionsWhereUserIdEqualsFirstPageRow, error) { 356 | rows, err := q.db.QueryContext(ctx, user_GetSentTransactionsWhereUserIdEqualsFirstPage, arg.UserID, arg.Limit) 357 | if err != nil { 358 | return nil, err 359 | } 360 | defer rows.Close() 361 | var items []User_GetSentTransactionsWhereUserIdEqualsFirstPageRow 362 | for rows.Next() { 363 | var i User_GetSentTransactionsWhereUserIdEqualsFirstPageRow 364 | if err := rows.Scan( 365 | &i.UserID, 366 | &i.Username, 367 | &i.TransactionID, 368 | &i.Amount, 369 | &i.CreatedAt, 370 | ); err != nil { 371 | return nil, err 372 | } 373 | items = append(items, i) 374 | } 375 | if err := rows.Close(); err != nil { 376 | return nil, err 377 | } 378 | if err := rows.Err(); err != nil { 379 | return nil, err 380 | } 381 | return items, nil 382 | } 383 | 384 | const user_GetWhereIdEquals = `-- name: User_GetWhereIdEquals :one 385 | SELECT user_id, created_at, username 386 | FROM users 387 | WHERE user_id = $1 388 | LIMIT 1 389 | ` 390 | 391 | type User_GetWhereIdEqualsRow struct { 392 | UserID uuid.UUID `json:"user_id"` 393 | CreatedAt time.Time `json:"created_at"` 394 | Username string `json:"username"` 395 | } 396 | 397 | func (q *Queries) User_GetWhereIdEquals(ctx context.Context, userID uuid.UUID) (User_GetWhereIdEqualsRow, error) { 398 | row := q.db.QueryRowContext(ctx, user_GetWhereIdEquals, userID) 399 | var i User_GetWhereIdEqualsRow 400 | err := row.Scan(&i.UserID, &i.CreatedAt, &i.Username) 401 | return i, err 402 | } 403 | 404 | const user_GetWhereUsernameEquals = `-- name: User_GetWhereUsernameEquals :one 405 | SELECT user_id, created_at, username 406 | FROM users 407 | WHERE username = $1 408 | LIMIT 1 409 | ` 410 | 411 | type User_GetWhereUsernameEqualsRow struct { 412 | UserID uuid.UUID `json:"user_id"` 413 | CreatedAt time.Time `json:"created_at"` 414 | Username string `json:"username"` 415 | } 416 | 417 | func (q *Queries) User_GetWhereUsernameEquals(ctx context.Context, username string) (User_GetWhereUsernameEqualsRow, error) { 418 | row := q.db.QueryRowContext(ctx, user_GetWhereUsernameEquals, username) 419 | var i User_GetWhereUsernameEqualsRow 420 | err := row.Scan(&i.UserID, &i.CreatedAt, &i.Username) 421 | return i, err 422 | } 423 | 424 | const user_GetWhereUsernameIncludes = `-- name: User_GetWhereUsernameIncludes :many 425 | SELECT user_id, created_at, username 426 | FROM users 427 | WHERE username ILIKE '%' || ( $1 ) || '%' 428 | ORDER BY created_at DESC 429 | ` 430 | 431 | type User_GetWhereUsernameIncludesRow struct { 432 | UserID uuid.UUID `json:"user_id"` 433 | CreatedAt time.Time `json:"created_at"` 434 | Username string `json:"username"` 435 | } 436 | 437 | func (q *Queries) User_GetWhereUsernameIncludes(ctx context.Context, dollar_1 sql.NullString) ([]User_GetWhereUsernameIncludesRow, error) { 438 | rows, err := q.db.QueryContext(ctx, user_GetWhereUsernameIncludes, dollar_1) 439 | if err != nil { 440 | return nil, err 441 | } 442 | defer rows.Close() 443 | var items []User_GetWhereUsernameIncludesRow 444 | for rows.Next() { 445 | var i User_GetWhereUsernameIncludesRow 446 | if err := rows.Scan(&i.UserID, &i.CreatedAt, &i.Username); err != nil { 447 | return nil, err 448 | } 449 | items = append(items, i) 450 | } 451 | if err := rows.Close(); err != nil { 452 | return nil, err 453 | } 454 | if err := rows.Err(); err != nil { 455 | return nil, err 456 | } 457 | return items, nil 458 | } 459 | 460 | const user_LoginWithUsername = `-- name: User_LoginWithUsername :one 461 | SELECT user_id, created_at, updated_at, is_admin, username, email, password 462 | FROM users 463 | WHERE username = $1 464 | LIMIT 1 465 | ` 466 | 467 | func (q *Queries) User_LoginWithUsername(ctx context.Context, username string) (User, error) { 468 | row := q.db.QueryRowContext(ctx, user_LoginWithUsername, username) 469 | var i User 470 | err := row.Scan( 471 | &i.UserID, 472 | &i.CreatedAt, 473 | &i.UpdatedAt, 474 | &i.IsAdmin, 475 | &i.Username, 476 | &i.Email, 477 | &i.Password, 478 | ) 479 | return i, err 480 | } 481 | 482 | const user_UpdateUserToAdminWhereIdEquals = `-- name: User_UpdateUserToAdminWhereIdEquals :one 483 | UPDATE users 484 | SET is_admin = TRUE 485 | WHERE user_id = $1 486 | RETURNING user_id, created_at, updated_at, is_admin, username, email, password 487 | ` 488 | 489 | func (q *Queries) User_UpdateUserToAdminWhereIdEquals(ctx context.Context, userID uuid.UUID) (User, error) { 490 | row := q.db.QueryRowContext(ctx, user_UpdateUserToAdminWhereIdEquals, userID) 491 | var i User 492 | err := row.Scan( 493 | &i.UserID, 494 | &i.CreatedAt, 495 | &i.UpdatedAt, 496 | &i.IsAdmin, 497 | &i.Username, 498 | &i.Email, 499 | &i.Password, 500 | ) 501 | return i, err 502 | } 503 | 504 | const user_UpdateUsernameWhereIdEquals = `-- name: User_UpdateUsernameWhereIdEquals :one 505 | UPDATE users 506 | SET username = $1 507 | WHERE user_id = $2 508 | RETURNING user_id, created_at, updated_at, is_admin, username, email, password 509 | ` 510 | 511 | type User_UpdateUsernameWhereIdEqualsParams struct { 512 | Username string `json:"username"` 513 | UserID uuid.UUID `json:"user_id"` 514 | } 515 | 516 | func (q *Queries) User_UpdateUsernameWhereIdEquals(ctx context.Context, arg User_UpdateUsernameWhereIdEqualsParams) (User, error) { 517 | row := q.db.QueryRowContext(ctx, user_UpdateUsernameWhereIdEquals, arg.Username, arg.UserID) 518 | var i User 519 | err := row.Scan( 520 | &i.UserID, 521 | &i.CreatedAt, 522 | &i.UpdatedAt, 523 | &i.IsAdmin, 524 | &i.Username, 525 | &i.Email, 526 | &i.Password, 527 | ) 528 | return i, err 529 | } 530 | 531 | const user_UpdateUsernameWhereUsernameEquals = `-- name: User_UpdateUsernameWhereUsernameEquals :one 532 | UPDATE users 533 | SET username = $1 534 | WHERE username = $2 535 | RETURNING user_id, created_at, updated_at, is_admin, username, email, password 536 | ` 537 | 538 | type User_UpdateUsernameWhereUsernameEqualsParams struct { 539 | Username string `json:"username"` 540 | Username_2 string `json:"username_2"` 541 | } 542 | 543 | func (q *Queries) User_UpdateUsernameWhereUsernameEquals(ctx context.Context, arg User_UpdateUsernameWhereUsernameEqualsParams) (User, error) { 544 | row := q.db.QueryRowContext(ctx, user_UpdateUsernameWhereUsernameEquals, arg.Username, arg.Username_2) 545 | var i User 546 | err := row.Scan( 547 | &i.UserID, 548 | &i.CreatedAt, 549 | &i.UpdatedAt, 550 | &i.IsAdmin, 551 | &i.Username, 552 | &i.Email, 553 | &i.Password, 554 | ) 555 | return i, err 556 | } 557 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module backend 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.7 7 | github.com/go-chi/cors v1.2.1 8 | github.com/go-chi/httprate v0.7.1 9 | github.com/go-playground/validator/v10 v10.11.1 10 | github.com/golang-jwt/jwt/v4 v4.4.3 11 | github.com/google/uuid v1.3.0 12 | github.com/joho/godotenv v1.3.0 13 | github.com/lib/pq v1.10.2 14 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 15 | ) 16 | 17 | require ( 18 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 19 | github.com/go-playground/locales v0.14.0 // indirect 20 | github.com/go-playground/universal-translator v0.18.0 // indirect 21 | github.com/leodido/go-urn v1.2.1 // indirect 22 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 23 | golang.org/x/text v0.3.7 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= 8 | github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 9 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 10 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 11 | github.com/go-chi/httprate v0.7.1 h1:d5kXARdms2PREQfU4pHvq44S6hJ1hPu4OXLeBKmCKWs= 12 | github.com/go-chi/httprate v0.7.1/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= 13 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 14 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 15 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 16 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 17 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 18 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 19 | github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= 20 | github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 21 | github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= 22 | github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 23 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 24 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 26 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 27 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 28 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 29 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 33 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 34 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 35 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 36 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 37 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 41 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 42 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 43 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 45 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 46 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= 47 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 48 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 49 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 53 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 55 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 56 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 57 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 58 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 62 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 63 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 65 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | -------------------------------------------------------------------------------- /backend/lib/auth/jwt_convert.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "backend/settings" 5 | "fmt" 6 | 7 | "github.com/golang-jwt/jwt/v4" 8 | ) 9 | 10 | // If an error occurs while parsing the tokenString, return error 11 | func ConvertJwtStringToToken(tokenString string) (*jwt.Token, error) { 12 | 13 | var JWT_KEY = []byte(settings.JWT_KEY) 14 | 15 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 16 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 17 | return nil, fmt.Errorf("error occured while parsing the JWT token") 18 | } 19 | return JWT_KEY, nil 20 | }) 21 | 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return token, err 27 | } 28 | -------------------------------------------------------------------------------- /backend/lib/auth/jwt_create.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "backend/settings" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/golang-jwt/jwt/v4" 10 | ) 11 | 12 | // Define which fields should be stored in the claims. 13 | // THe json field will be used as the key name for the 14 | // claim, when decoded 15 | type TokenClaims struct { 16 | *jwt.RegisteredClaims 17 | IsAdmin bool `json:"is_admin"` 18 | Username string `json:"username"` 19 | UserID string `json:"user_id"` 20 | ExpiresAt int64 `json:"exp"` 21 | } 22 | 23 | // https://www.golinuxcloud.com/golang-jwt/ 24 | // Create a JWT string that stores the passed in fields. 25 | // If at some point the creation of the token fails, 26 | // return an empty string and an error 27 | func CreateJwtToken(user_claims TokenClaims, expiration_time time.Time) (string, error) { 28 | 29 | // token := jwt.New(jwt.SigningMethodES256) 30 | token := jwt.New(jwt.SigningMethodHS256) 31 | 32 | token.Claims = &TokenClaims{ 33 | IsAdmin: user_claims.IsAdmin, 34 | Username: user_claims.Username, 35 | UserID: user_claims.UserID, 36 | ExpiresAt: expiration_time.Unix()} 37 | 38 | tokenString, err := token.SignedString([]byte(settings.JWT_KEY)) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | return tokenString, err 44 | } 45 | 46 | // Create a JWT Access Token string, using the expiration time that is used in the settings 47 | func CreateJwtAccesToken(user_claims TokenClaims) (string, error) { 48 | token, err := CreateJwtToken(user_claims, settings.AccessTokenDuration()) 49 | if err != nil { 50 | return "", err 51 | } 52 | return token, err 53 | } 54 | 55 | // Create a JWT Refresh Token string, using the expiration time that is used in the settings 56 | func CreateJwtRefreshToken(user_claims TokenClaims) (string, error) { 57 | token, err := CreateJwtToken(user_claims, settings.RefreshTokenDuration()) 58 | if err != nil { 59 | return "", err 60 | } 61 | return token, err 62 | } 63 | 64 | // Return claims from the token, if no errors or token is valid 65 | func GetTokenClaims(tokenString string) (jwt.MapClaims, error) { 66 | token, err := ConvertJwtStringToToken(tokenString) 67 | if err != nil { 68 | fmt.Println("Could not convert token string to JWT!") 69 | return nil, err 70 | } 71 | 72 | claims, ok := token.Claims.(jwt.MapClaims) 73 | if !ok || !token.Valid { 74 | return nil, errors.New("jwt token is invalid") 75 | } 76 | 77 | return claims, err 78 | } 79 | -------------------------------------------------------------------------------- /backend/lib/auth/jwt_validate.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // Return true only when the input JWT string is valid. 4 | // Note that this returns false, if the token is valid, 5 | // but has expired. 6 | func JwtIsValid(tokenString string) bool { 7 | 8 | token, err := ConvertJwtStringToToken(tokenString) 9 | if err != nil { 10 | return false 11 | } 12 | if token.Valid { 13 | return true 14 | } 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /backend/lib/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "backend/lib/auth" 5 | res "backend/lib/response" 6 | "backend/settings" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // Get the request header 13 | // - If authorization header does not exists, return error response 14 | // - If authorization header exists, but is not valid, return error response 15 | // - If the auth token is not valid, return error response 16 | func IsAuthenticated(next http.Handler) http.Handler { 17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | 19 | auth_key := r.Header.Get(settings.AUTHENTICATION_HEADER_KEY) 20 | if auth_key == "" { 21 | res.Response(w, 401, nil, "No Authorization header provided!") 22 | return 23 | } 24 | 25 | auth_token := strings.Split(auth_key, "Bearer ")[1] 26 | if auth_token == "" { 27 | res.Response(w, 401, nil, "No Bearer token provided!") 28 | return 29 | } 30 | 31 | auth_token_is_valid := auth.JwtIsValid(auth_token) 32 | if !auth_token_is_valid { 33 | res.Response(w, 401, nil, "Invalid Bearer Token provided!") 34 | return 35 | } 36 | 37 | next.ServeHTTP(w, r) 38 | }) 39 | } 40 | 41 | // Get the request header 42 | // - If authorization header does not exists, return error response 43 | // - If authorization header exists, but is not valid, return error response 44 | // - If the auth token is not valid, return error response 45 | // 46 | // TODO : Find out if you can reuse the previous middleware, 47 | // so that the code would not be duplicated. 48 | func IsAdmin(next http.Handler) http.Handler { 49 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | 51 | auth_key := r.Header.Get(settings.AUTHENTICATION_HEADER_KEY) 52 | if auth_key == "" { 53 | res.Response(w, 401, nil, "No Authorization header provided!") 54 | return 55 | } 56 | 57 | auth_token := strings.Split(auth_key, "Bearer ")[1] 58 | if auth_token == "" { 59 | res.Response(w, 401, nil, "No Bearer token provided!") 60 | return 61 | } 62 | 63 | auth_token_is_valid := auth.JwtIsValid(auth_token) 64 | if !auth_token_is_valid { 65 | res.Response(w, 401, nil, "Invalid Bearer Token provided!") 66 | return 67 | } 68 | 69 | auth_token_claims, err := auth.GetTokenClaims(auth_token) 70 | if err != nil { 71 | res.Response(w, 401, nil, "Could not extract claims from the authorization token!") 72 | return 73 | } 74 | 75 | is_admin := auth_token_claims["is_admin"].(bool) 76 | if !is_admin { 77 | res.Response(w, 401, nil, "Could not extract claims from the authorization token!") 78 | return 79 | } 80 | fmt.Println("IS ADMIN :: ", is_admin) 81 | 82 | next.ServeHTTP(w, r) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /backend/lib/paginate/main.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "backend/settings" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Credits 14 | // https://medium.easyread.co/how-to-do-pagination-in-postgres-with-golang-in-4-common-ways-12365b9fb528 15 | func DecodeCursor(encodedCursor string) (timestamp time.Time, uuid string, err error) { 16 | byt, err := base64.StdEncoding.DecodeString(encodedCursor) 17 | if err != nil { 18 | return 19 | } 20 | 21 | arrStr := strings.Split(string(byt), ",") 22 | if len(arrStr) != 2 { 23 | err = errors.New("cursor is invalid") 24 | return 25 | } 26 | 27 | timestamp, err = time.Parse(time.RFC3339Nano, arrStr[0]) 28 | if err != nil { 29 | return 30 | } 31 | uuid = arrStr[1] 32 | return 33 | } 34 | 35 | func EncodeCursor(t time.Time, uuid string) string { 36 | key := fmt.Sprintf("%s,%s", t.Format(time.RFC3339Nano), uuid) 37 | return base64.StdEncoding.EncodeToString([]byte(key)) 38 | } 39 | 40 | /* 41 | Pagination param variables that can be used 42 | for querying the data 43 | - limit = How many records can be returned 44 | - key = the name of the key which will hold the cursor value 45 | - cursor = the value of the cursor key 46 | - url = Full API path of the controller (excluding query params) 47 | */ 48 | func PaginationParams(r *http.Request, url_pagination_key, endpoint_path string) (int32, string, string, string) { 49 | 50 | limit := int32(settings.PAGINATION_LIMIT) 51 | key := url_pagination_key 52 | cursor := r.URL.Query().Get(url_pagination_key) 53 | url := fmt.Sprintf("%s%s", settings.BASE_URL, endpoint_path) 54 | 55 | return limit, key, cursor, url 56 | } 57 | 58 | // Return a link that has an appended key-value pair 59 | func LinkWithQueryKey(endpoint_path, url_key_name, url_key_value string) string { 60 | return endpoint_path + "?" + url_key_name + "=" + url_key_value 61 | } 62 | -------------------------------------------------------------------------------- /backend/lib/request/main.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "backend/settings" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func GetAuthHeader(r *http.Request) string { 10 | auth_key := r.Header.Get(settings.AUTHENTICATION_HEADER_KEY) 11 | auth_token := strings.Split(auth_key, "Bearer ")[1] 12 | return auth_token 13 | } 14 | -------------------------------------------------------------------------------- /backend/lib/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "backend/settings" 8 | "fmt" 9 | ) 10 | 11 | const ( 12 | ParamIsNotIntMessage = "Only integers as URL params allowed!" 13 | FailedDbConnMessage = "Could not connect to the database!" 14 | FailedJsonValidation = "Invalid JSON sent!" 15 | FailedPayloadValidation = "Payload validation failed!" 16 | ) 17 | 18 | type Res struct { 19 | // Baseline Response 20 | Status int `json:"status"` 21 | Message string `json:"message"` 22 | Data interface{} `json:"data"` 23 | } 24 | 25 | type ResWithPagination struct { 26 | // Baseline Response + Pagination links 27 | Res 28 | Links interface{} `json:"links"` 29 | } 30 | 31 | type PaginationLinks struct { 32 | Prev string `json:"prev,omitempty"` 33 | Next string `json:"next,omitempty"` 34 | } 35 | 36 | type ResponseWithJwt struct { 37 | Data interface{} `json:"data"` 38 | JwtFields JwtFields `json:"token,omitempty"` 39 | } 40 | 41 | type JwtFields struct { 42 | AccessToken string `json:"access_token,omitempty"` 43 | RefreshToken string `json:"refresh_token,omitempty"` 44 | } 45 | 46 | // all of the correct status codes can be found here -> https://pkg.go.dev/net/http?utm_source=gopls#StatusOK 47 | func Response(w http.ResponseWriter, status int, data interface{}, message string) error { 48 | w.Header().Set("Content-Type", "application/json") 49 | w.WriteHeader(status) 50 | 51 | return json.NewEncoder(w).Encode(&Res{ 52 | Status: status, 53 | Message: message, 54 | Data: data, 55 | }) 56 | } 57 | 58 | func ResponseWithPagination(w http.ResponseWriter, status int, data interface{}, message string, links PaginationLinks) error { 59 | w.Header().Set("Content-Type", "application/json") 60 | w.WriteHeader(status) 61 | 62 | return json.NewEncoder(w).Encode(&ResWithPagination{ 63 | Res: Res{ 64 | Status: status, 65 | Message: message, 66 | Data: data, 67 | }, 68 | Links: PaginationLinks{ 69 | Prev: links.Prev, 70 | Next: links.Next, 71 | }, 72 | }) 73 | } 74 | 75 | // return the full error message only during debug 76 | func DbErrorMessage(err_msg string) string { 77 | if settings.DEBUG_MODE == "true" { 78 | return fmt.Sprintln(FailedDbConnMessage, err_msg) 79 | } 80 | return fmt.Sprintln(FailedDbConnMessage) 81 | } 82 | -------------------------------------------------------------------------------- /backend/lib/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | // Placeholder function to be replaced after codegen. 6 | // Usually replaced with the function that returns the 7 | // data from the database, if everything goes ok. 8 | func PlaceholderFunction() (struct{}, error) { 9 | var placeholder struct{} 10 | return placeholder, nil 11 | } 12 | 13 | func DifferenceIsLogerThanOneDay(start time.Time, end time.Time) bool { 14 | duration := end.Sub(start) 15 | return duration.Hours()/24 > 1 16 | } 17 | 18 | func AddOneDayToDate(t time.Time) time.Time { 19 | return t.AddDate(0, 0, 1) 20 | } 21 | 22 | func TimeUntillNextDay(day time.Time) time.Duration { 23 | return time.Since(AddOneDayToDate(day)) 24 | } 25 | 26 | func SecondsUntillNextDay(day time.Time) time.Duration { 27 | return -time.Duration(TimeUntillNextDay(day).Seconds()) 28 | } 29 | -------------------------------------------------------------------------------- /backend/lib/validate/validate.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | ) 6 | 7 | type ErrorResponse struct { 8 | FailedField string `json:"failed_field"` 9 | Message string `json:"message"` 10 | } 11 | 12 | // Return an understandabe / UI friendly message that potentially 13 | // can be displayed on the client side, when validation for 14 | // a field fails. Be free to add new tags! 15 | func messageForTag(fe validator.FieldError) string { 16 | 17 | field := fe.Field() 18 | param := fe.Param() 19 | tag := fe.Tag() 20 | 21 | switch tag { 22 | case "required": 23 | return field + " is required" 24 | case "email": 25 | return field + " must be a valid email address" 26 | case "min": 27 | return field + " should be at least " + param + " characters long" 28 | case "max": 29 | return field + " should be less than " + param + " characters long" 30 | } 31 | return field + " failed validation for " + tag + ", " + param 32 | } 33 | 34 | // Pass in the sent struct and check if the 35 | // fields pass validation 36 | func ValidateStruct(i interface{}) []*ErrorResponse { 37 | var errors []*ErrorResponse 38 | err := validator.New().Struct(i) 39 | 40 | if err != nil { 41 | for _, err := range err.(validator.ValidationErrors) { 42 | errors = append(errors, &ErrorResponse{ 43 | FailedField: err.Field(), 44 | Message: messageForTag(err), 45 | }) 46 | } 47 | } 48 | return errors 49 | } 50 | -------------------------------------------------------------------------------- /backend/main.go: -------------------------------------------------------------------------------- 1 | // Server generated by gomarvin, v0.6.x 2 | // Repo : https://github.com/tompston/gomarvin 3 | // Docs : https://gomarvin.pages.dev/docs 4 | 5 | package main 6 | 7 | import ( 8 | "backend/app" 9 | "backend/settings" 10 | "backend/settings/database" 11 | "fmt" 12 | "net/http" 13 | ) 14 | 15 | func main() { 16 | 17 | address := fmt.Sprintf("localhost:%s", settings.GO_BACKEND_PORT) 18 | 19 | database.Connect() // set database.DB var to hold a connection to the db 20 | 21 | server := app.Start() 22 | http.ListenAndServe(address, server) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /backend/modules/auth_module/body.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gomarvin, v0.6.x. DO NOT EDIT. 2 | // Rename the current file and remove upper comment to save changes! 3 | 4 | package auth_module 5 | 6 | type TokenIsValidBody struct { 7 | AccessToken string `json:"access_token" validate:"required"` 8 | } 9 | -------------------------------------------------------------------------------- /backend/modules/auth_module/controllers.go: -------------------------------------------------------------------------------- 1 | package auth_module 2 | 3 | import ( 4 | "backend/lib/auth" 5 | res "backend/lib/response" 6 | 7 | "net/http" 8 | 9 | "github.com/go-chi/chi/v5" 10 | 11 | "backend/lib/validate" 12 | "encoding/json" 13 | ) 14 | 15 | const ( 16 | TokenIsValidUrl = "/auth/token" 17 | ) 18 | 19 | func Router(api *chi.Mux) { 20 | api.Post(TokenIsValidUrl, TokenIsValid) 21 | } 22 | 23 | func TokenIsValid(w http.ResponseWriter, r *http.Request) { 24 | 25 | // validate the sent json object 26 | payload := new(TokenIsValidBody) 27 | 28 | if err := json.NewDecoder(r.Body).Decode(payload); err != nil { 29 | res.Response(w, 400, err.Error(), res.FailedJsonValidation) 30 | return 31 | } 32 | if err := validate.ValidateStruct(payload); err != nil { 33 | res.Response(w, 400, err, res.FailedPayloadValidation) 34 | return 35 | } 36 | 37 | token_is_valid := auth.JwtIsValid(payload.AccessToken) 38 | if !token_is_valid { 39 | res.Response(w, 401, nil, "Authorization token is invalid!") 40 | return 41 | } 42 | 43 | res.Response(w, 200, token_is_valid, "") 44 | } 45 | -------------------------------------------------------------------------------- /backend/modules/transaction_module/body.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gomarvin, v0.6.x. DO NOT EDIT. 2 | // Rename the current file and remove upper comment to save changes! 3 | 4 | package transaction_module 5 | 6 | type CreateTransactionBody struct { 7 | SenderId string `json:"sender_id" validate:"required,uuid4"` 8 | RecieverId string `json:"reciever_id" validate:"required,uuid4"` 9 | Amount int32 `json:"amount" validate:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /backend/modules/transaction_module/controllers.go: -------------------------------------------------------------------------------- 1 | package transaction_module 2 | 3 | import ( 4 | "backend/lib/middleware" 5 | 6 | "github.com/go-chi/chi/v5" 7 | ) 8 | 9 | const ( 10 | GetTransactionsForUserUrl = "/transaction" 11 | CreateTransactionUrl = "/transaction" 12 | GetTransactionByIDUrl = "/transaction/{transaction_id}" 13 | ) 14 | 15 | func Router(api *chi.Mux) { 16 | // If user is authenticated, allow access to these routes 17 | api.Group(func(r chi.Router) { 18 | r.Use(middleware.IsAuthenticated) 19 | r.Get(GetTransactionsForUserUrl, GetTransactionsForUser) 20 | r.Get(GetTransactionByIDUrl, GetTransactionByID) 21 | r.Post(CreateTransactionUrl, CreateTransaction) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /backend/modules/transaction_module/get.go: -------------------------------------------------------------------------------- 1 | package transaction_module 2 | 3 | import ( 4 | "backend/db/sqlc" 5 | "backend/lib/auth" 6 | "backend/lib/paginate" 7 | "backend/lib/request" 8 | res "backend/lib/response" 9 | "backend/lib/utils" 10 | "backend/settings/database" 11 | "context" 12 | "net/http" 13 | 14 | "github.com/go-chi/chi/v5" 15 | "github.com/google/uuid" 16 | ) 17 | 18 | // Return all of the sent transactions for the logged in user 19 | // using cursor paginations 20 | func GetTransactionsForUser(w http.ResponseWriter, r *http.Request) { 21 | 22 | auth_header := request.GetAuthHeader(r) 23 | auth_claim, err := auth.GetTokenClaims(auth_header) 24 | if err != nil { 25 | res.Response(w, 401, nil, "Authentication Failed!") 26 | } 27 | 28 | user_id := uuid.MustParse(auth_claim["user_id"].(string)) 29 | 30 | limit, url_pagination_key, cursor, endpoint_path := paginate.PaginationParams(r, "cursor", GetTransactionsForUserUrl) 31 | ctx := context.Background() 32 | 33 | // If no cursor provided, execute the first query 34 | if cursor == "" { 35 | data, err := sqlc.New(database.DB). 36 | User_GetSentTransactionsWhereUserIdEqualsFirstPage(ctx, 37 | sqlc.User_GetSentTransactionsWhereUserIdEqualsFirstPageParams{ 38 | UserID: user_id, Limit: limit + 1}) 39 | 40 | if err != nil { 41 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 42 | return 43 | } 44 | 45 | // If no transactions for the user are found, return nil data, 46 | // and an appropriate message. 47 | if data == nil { 48 | res.Response(w, 200, nil, "No transactions for user found!") 49 | return 50 | } 51 | 52 | next_cursor := paginate.EncodeCursor( 53 | data[limit].CreatedAt, 54 | data[limit].UserID.String()) 55 | 56 | res.ResponseWithPagination(w, 200, data[:limit], "", 57 | res.PaginationLinks{ 58 | Next: endpoint_path + "?" + url_pagination_key + "=" + next_cursor, 59 | }) 60 | return 61 | } 62 | 63 | // If cursor exists and is valid, execute the pagination query 64 | if cursor != "" { 65 | timestamp, _, err := paginate.DecodeCursor(cursor) 66 | if err != nil { 67 | res.Response(w, 400, nil, "Could not decode the cursor!") 68 | return 69 | } 70 | 71 | data, err := sqlc.New(database.DB). 72 | User_GetSentTransactionsWhereUserIdEquals( 73 | ctx, sqlc.User_GetSentTransactionsWhereUserIdEqualsParams{ 74 | UserID: user_id, 75 | CreatedAt: timestamp, 76 | Limit: limit + 1}) 77 | 78 | if err != nil { 79 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 80 | return 81 | } 82 | 83 | if len(data) < int(limit) { 84 | res.ResponseWithPagination(w, 200, data, "", res.PaginationLinks{Next: "null"}) 85 | return 86 | } 87 | 88 | next_cursor := paginate.EncodeCursor( 89 | data[limit].CreatedAt, 90 | data[limit].UserID.String()) 91 | 92 | res.ResponseWithPagination(w, 200, data[:limit], "", 93 | res.PaginationLinks{ 94 | Next: endpoint_path + "?" + url_pagination_key + "=" + next_cursor, 95 | }) 96 | return 97 | } 98 | } 99 | 100 | // Not implemented 101 | func GetTransactionByID(w http.ResponseWriter, r *http.Request) { 102 | 103 | id := chi.URLParam(r, "id") 104 | _ = id 105 | 106 | // replace the function to return the data from the db 107 | data, err := utils.PlaceholderFunction() 108 | if err != nil { 109 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 110 | return 111 | } 112 | 113 | res.Response(w, 200, data, "") 114 | } 115 | -------------------------------------------------------------------------------- /backend/modules/transaction_module/post.go: -------------------------------------------------------------------------------- 1 | package transaction_module 2 | 3 | import ( 4 | "backend/db/sqlc" 5 | res "backend/lib/response" 6 | "backend/lib/validate" 7 | "backend/settings/database" 8 | "context" 9 | "encoding/json" 10 | "net/http" 11 | 12 | "github.com/google/uuid" 13 | ) 14 | 15 | func CreateTransaction(w http.ResponseWriter, r *http.Request) { 16 | 17 | // validate the sent json object 18 | payload := new(CreateTransactionBody) 19 | if err := json.NewDecoder(r.Body).Decode(payload); err != nil { 20 | res.Response(w, 400, err.Error(), res.FailedJsonValidation) 21 | return 22 | } 23 | if err := validate.ValidateStruct(payload); err != nil { 24 | res.Response(w, 400, err, res.FailedPayloadValidation) 25 | return 26 | } 27 | 28 | // Store the transaction between two accounts in the db 29 | data, err := sqlc.New(database.DB).Transaction_Create( 30 | context.Background(), 31 | sqlc.Transaction_CreateParams{ 32 | SenderID: uuid.MustParse(payload.SenderId), 33 | ReceiverID: uuid.MustParse(payload.RecieverId), 34 | Amount: payload.Amount}) 35 | 36 | if err != nil { 37 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 38 | return 39 | } 40 | 41 | res.Response(w, 200, data, "") 42 | } 43 | -------------------------------------------------------------------------------- /backend/modules/user_module/body.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gomarvin, v0.6.x. DO NOT EDIT. 2 | // Rename the current file and remove upper comment to save changes! 3 | 4 | package user_module 5 | 6 | type RegisterUserBody struct { 7 | Username string `json:"username" validate:"required,min=5,max=250"` 8 | Password string `json:"password" validate:"required,min=10,max=250"` 9 | Email string `json:"email" validate:"required,email"` 10 | } 11 | 12 | type LoginUserBody struct { 13 | Username string `json:"username" validate:"required,min=5,max=250"` 14 | Password string `json:"password" validate:"required,min=10,max=250"` 15 | } 16 | -------------------------------------------------------------------------------- /backend/modules/user_module/controllers.go: -------------------------------------------------------------------------------- 1 | package user_module 2 | 3 | import ( 4 | "backend/lib/middleware" 5 | "time" 6 | 7 | "github.com/go-chi/chi/v5" 8 | "github.com/go-chi/httprate" 9 | ) 10 | 11 | const ( 12 | GetUsersUrl = "/user" 13 | GetUserByIDUrl = "/user/id/{id}" 14 | RegisterUserUrl = "/user/register" 15 | DeleteUserUrl = "/user" 16 | GetUserByUsernameUrl = "/user/username/{username}" 17 | LoginUserUrl = "/user/login" 18 | GetUserDetailsWithAuthUrl = "/user/details" 19 | ) 20 | 21 | func Router(api *chi.Mux) { 22 | api.Get(GetUsersUrl, GetUsers) 23 | api.Get(GetUserByIDUrl, GetUserByID) 24 | api.Delete(DeleteUserUrl, DeleteUser) 25 | api.Get(GetUserByUsernameUrl, GetUserByUsername) 26 | api.Post(LoginUserUrl, LoginUser) 27 | 28 | // Use authentication Guard for the route that returns non-public 29 | // user details 30 | api.Group(func(r chi.Router) { 31 | r.Use(middleware.IsAuthenticated) 32 | r.Get(GetUserDetailsWithAuthUrl, GetUserDetailsWithAuth) 33 | }) 34 | 35 | // Use rate limiting on registration route to prevent bot spamming 36 | // Example -> https://go-chi.io/#/pages/middleware?id=http-rate-limiting-middleware 37 | api.Group(func(r chi.Router) { 38 | r.Use(httprate.LimitByIP(20, 10*time.Minute)) 39 | r.Post(RegisterUserUrl, RegisterUser) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /backend/modules/user_module/custm_response.go: -------------------------------------------------------------------------------- 1 | package user_module 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | // Custom Response for endpoint 10 | type GetUserDetailsWithAuthResponse struct { 11 | UserID uuid.UUID `json:"user_id"` 12 | CreatedAt time.Time `json:"created_at"` 13 | Username string `json:"username"` 14 | Balance int64 `json:"balance"` 15 | TimeUntillBonus time.Duration `json:"time_untill_bonus"` 16 | } 17 | -------------------------------------------------------------------------------- /backend/modules/user_module/delete.go: -------------------------------------------------------------------------------- 1 | package user_module 2 | 3 | import ( 4 | res "backend/lib/response" 5 | "backend/lib/utils" 6 | "net/http" 7 | ) 8 | 9 | // Not implemented 10 | func DeleteUser(w http.ResponseWriter, r *http.Request) { 11 | 12 | // replace the function to return the data from the db 13 | data, err := utils.PlaceholderFunction() 14 | if err != nil { 15 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 16 | return 17 | } 18 | 19 | res.Response(w, 200, data, "") 20 | } 21 | -------------------------------------------------------------------------------- /backend/modules/user_module/get.go: -------------------------------------------------------------------------------- 1 | package user_module 2 | 3 | import ( 4 | "backend/db/sqlc" 5 | "backend/lib/auth" 6 | "backend/lib/paginate" 7 | "backend/lib/request" 8 | res "backend/lib/response" 9 | "backend/lib/utils" 10 | "backend/settings/database" 11 | "context" 12 | "fmt" 13 | "net/http" 14 | "time" 15 | 16 | "github.com/go-chi/chi/v5" 17 | "github.com/google/uuid" 18 | ) 19 | 20 | // Return all users using cursor pagination. 21 | // - If the query param is an empty string, execute 22 | // the first pagination func which does not need any state params. 23 | // - If the query param exists and is valid, exec the query that 24 | // does cursor pagination. 25 | func GetUsers(w http.ResponseWriter, r *http.Request) { 26 | 27 | limit, url_pagination_key, cursor, endpoint_path := paginate. 28 | PaginationParams(r, "cursor", GetUsersUrl) 29 | 30 | ctx := context.Background() 31 | 32 | // response if there is no cursor query param 33 | if cursor == "" { 34 | data, err := sqlc.New(database.DB).User_GetAllWithPaginationFirstPage(ctx, limit+1) 35 | if err != nil { 36 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 37 | return 38 | } 39 | 40 | next_cursor := paginate.EncodeCursor( 41 | data[limit].CreatedAt, 42 | data[limit].UserID.String()) 43 | 44 | res.ResponseWithPagination(w, 200, data[:limit], "", 45 | res.PaginationLinks{Next: endpoint_path + "?" + url_pagination_key + "=" + next_cursor}) 46 | return 47 | } 48 | 49 | // response if cursor query param exists and is valid 50 | if cursor != "" { 51 | timestamp, id, err := paginate.DecodeCursor(cursor) 52 | if err != nil { 53 | res.Response(w, 400, nil, "Could not decode the cursor!") 54 | return 55 | } 56 | 57 | data, err := sqlc.New(database.DB). 58 | User_GetAllWithPaginationNextPage(ctx, 59 | sqlc.User_GetAllWithPaginationNextPageParams{ 60 | UserID: uuid.MustParse(id), 61 | CreatedAt: timestamp, 62 | Limit: limit + 1}) 63 | 64 | if err != nil { 65 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 66 | return 67 | } 68 | 69 | // If lenght of the returned array does not have the last 70 | // row, you can't create the next cursor 71 | if len(data) < int(limit) { 72 | res.ResponseWithPagination(w, 200, data, "", res.PaginationLinks{Next: "null"}) 73 | return 74 | } 75 | 76 | next_cursor := paginate.EncodeCursor( 77 | data[limit].CreatedAt, 78 | data[limit].UserID.String()) 79 | 80 | res.ResponseWithPagination(w, 200, data[:limit], "", res.PaginationLinks{ 81 | Next: endpoint_path + "?" + url_pagination_key + "=" + next_cursor}) 82 | return 83 | } 84 | } 85 | 86 | // If the user_id exists, return response with public user details 87 | func GetUserByID(w http.ResponseWriter, r *http.Request) { 88 | 89 | id := chi.URLParam(r, "id") 90 | 91 | user_id, err := uuid.Parse(id) 92 | if err != nil { 93 | res.Response(w, 400, nil, "Could not parse the user id!") 94 | return 95 | } 96 | 97 | data, err := sqlc.New(database.DB).User_GetWhereIdEquals( 98 | context.Background(), user_id) 99 | 100 | if err != nil { 101 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 102 | return 103 | } 104 | 105 | res.Response(w, 200, data, "User Found!") 106 | } 107 | 108 | // If the username exists, return response with public user details 109 | func GetUserByUsername(w http.ResponseWriter, r *http.Request) { 110 | 111 | username := chi.URLParam(r, "username") 112 | 113 | data, err := sqlc.New(database.DB). 114 | User_GetWhereUsernameEquals(context.Background(), username) 115 | 116 | if err != nil { 117 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 118 | return 119 | } 120 | 121 | // if the returned data from the db is empty, the user 122 | // with the specified username does not exist 123 | if (data == sqlc.User_GetWhereUsernameEqualsRow{}) { 124 | res.Response(w, 400, nil, "User does not exist!") 125 | return 126 | } 127 | 128 | res.Response(w, 200, data, "") 129 | } 130 | 131 | func GetUserDetailsWithAuth(w http.ResponseWriter, r *http.Request) { 132 | 133 | auth_header := request.GetAuthHeader(r) 134 | auth_claim, err := auth.GetTokenClaims(auth_header) 135 | if err != nil { 136 | res.Response(w, 401, nil, "Authentication Failed! Invalid token claim extraction.") 137 | return 138 | } 139 | 140 | user_id := uuid.MustParse(auth_claim["user_id"].(string)) 141 | db := sqlc.New(database.DB) 142 | ctx := context.Background() 143 | 144 | data, err := db.User_GetWhereIdEquals(ctx, user_id) 145 | if err != nil { 146 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 147 | return 148 | } 149 | 150 | // If no transactions are found for the user, the returned balance is 0 151 | balance, err := db.Balance_GetUserBalanceByUserID(ctx, user_id) 152 | if err != nil { 153 | fmt.Println("Error occured during calculating the balance ", res.DbErrorMessage(err.Error())) 154 | } 155 | 156 | // Find the last transaction that was sent from the BonusBot 157 | last_bonus_from_transaction_bot, err := db. 158 | Transaction_FindLastTransactionBotBonusPaymentForUser(ctx, data.UserID) 159 | 160 | // If a user registered before the BonusBot was implemented, no transactions 161 | // between the users will be found, so Create the initial payment, so that 162 | // next sql queries would find a row between the bot and the user. 163 | if (last_bonus_from_transaction_bot == 164 | sqlc.Transaction_FindLastTransactionBotBonusPaymentForUserRow{}) { 165 | bonus_payment, err := db.Transaction_TransactionBotSendsBonusToUser(ctx, data.UserID) 166 | if err != nil { 167 | fmt.Println("Could not send the bonus transaction to user with the id of ", bonus_payment.ReceiverID) 168 | } 169 | } 170 | 171 | if err != nil { 172 | fmt.Println(res.DbErrorMessage(err.Error())) 173 | } 174 | 175 | last_bonus_exceeds_one_day := utils.DifferenceIsLogerThanOneDay( 176 | time.Now(), last_bonus_from_transaction_bot.CreatedAt) 177 | 178 | if last_bonus_exceeds_one_day { 179 | bonus_payment, err := db.Transaction_TransactionBotSendsBonusToUser(ctx, data.UserID) 180 | if err != nil { 181 | fmt.Println("Could not send the bonus transaction to user with the id of ", bonus_payment.ReceiverID) 182 | } 183 | res.Response(w, 200, 184 | GetUserDetailsWithAuthResponse{ 185 | UserID: data.UserID, 186 | CreatedAt: data.CreatedAt, 187 | Username: data.Username, 188 | Balance: balance, 189 | TimeUntillBonus: utils.SecondsUntillNextDay(bonus_payment.CreatedAt)}, "") 190 | return 191 | } 192 | 193 | res.Response(w, 200, 194 | GetUserDetailsWithAuthResponse{ 195 | UserID: data.UserID, 196 | CreatedAt: data.CreatedAt, 197 | Username: data.Username, 198 | Balance: balance, 199 | TimeUntillBonus: utils.SecondsUntillNextDay(last_bonus_from_transaction_bot.CreatedAt)}, "") 200 | } 201 | -------------------------------------------------------------------------------- /backend/modules/user_module/post.go: -------------------------------------------------------------------------------- 1 | package user_module 2 | 3 | import ( 4 | "backend/db/sqlc" 5 | "backend/lib/auth" 6 | res "backend/lib/response" 7 | "backend/lib/validate" 8 | "backend/settings/database" 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "net/http" 13 | 14 | "golang.org/x/crypto/bcrypt" 15 | ) 16 | 17 | func RegisterUser(w http.ResponseWriter, r *http.Request) { 18 | 19 | // validate the sent json object 20 | payload := new(RegisterUserBody) 21 | if err := json.NewDecoder(r.Body).Decode(payload); err != nil { 22 | res.Response(w, 400, err.Error(), res.FailedJsonValidation) 23 | return 24 | } 25 | if err := validate.ValidateStruct(payload); err != nil { 26 | res.Response(w, 400, err, res.FailedPayloadValidation) 27 | return 28 | } 29 | 30 | hashed_password, err := bcrypt.GenerateFromPassword( 31 | []byte(payload.Password), bcrypt.DefaultCost) 32 | 33 | if err != nil { 34 | res.Response(w, 400, nil, "Could not hash the password!") 35 | return 36 | } 37 | 38 | // replace the function to return the data from the db 39 | data, err := sqlc.New(database.DB).User_Create( 40 | context.Background(), 41 | sqlc.User_CreateParams{ 42 | Username: payload.Username, 43 | Password: string(hashed_password), 44 | Email: payload.Email}) 45 | 46 | if err != nil { 47 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 48 | return 49 | } 50 | 51 | token_claims := auth.TokenClaims{ 52 | IsAdmin: data.IsAdmin, 53 | Username: data.Username, 54 | UserID: data.UserID.String()} 55 | 56 | access_token, err := auth.CreateJwtAccesToken(token_claims) 57 | if err != nil || access_token == "" { 58 | res.Response(w, 400, nil, "Could not create a valid JWT Access Token!") 59 | return 60 | } 61 | 62 | refresh_token, err := auth.CreateJwtRefreshToken(token_claims) 63 | if err != nil || refresh_token == "" { 64 | res.Response(w, 400, nil, "Could not create a valid JWT Refresh Token!") 65 | return 66 | } 67 | 68 | data_with_token := &res.ResponseWithJwt{ 69 | Data: data, 70 | JwtFields: res.JwtFields{AccessToken: access_token, RefreshToken: refresh_token}} 71 | 72 | // Upon registration, send a transaction from the transaction bot to the newly created user 73 | bonus_payment, err := sqlc.New(database.DB). 74 | Transaction_TransactionBotSendsBonusToUser(context.Background(), data.UserID) 75 | if err != nil { 76 | fmt.Println("Could not send the bonus transaction to user with the id of ", bonus_payment.ReceiverID) 77 | } 78 | 79 | res.Response(w, 200, data_with_token, "User Created and Logged in!") 80 | } 81 | 82 | // Return access and refresh JWT tokens in the response, if 83 | // the sent credentials are correct 84 | func LoginUser(w http.ResponseWriter, r *http.Request) { 85 | 86 | // validate the sent json object 87 | payload := new(LoginUserBody) 88 | if err := json.NewDecoder(r.Body).Decode(payload); err != nil { 89 | res.Response(w, 400, err.Error(), res.FailedJsonValidation) 90 | return 91 | } 92 | if err := validate.ValidateStruct(payload); err != nil { 93 | res.Response(w, 400, err, res.FailedPayloadValidation) 94 | return 95 | } 96 | 97 | data, err := sqlc.New(database.DB).User_LoginWithUsername(context.Background(), payload.Username) 98 | // If no user is returned 99 | if (data == sqlc.User{}) { 100 | res.Response(w, 400, nil, "User Not Found!") 101 | return 102 | } 103 | 104 | if err != nil { 105 | res.Response(w, 400, nil, res.DbErrorMessage(err.Error())) 106 | return 107 | } 108 | 109 | // If no user is returned from the db, return error 110 | if (data == sqlc.User{}) { 111 | res.Response(w, 400, nil, "User does not exist!") 112 | return 113 | } 114 | 115 | // If the user exists and the payload password does 116 | // not match the decoded hashed password in the db, 117 | // return an error message and StatusUnauthorized code 118 | if err := bcrypt.CompareHashAndPassword( 119 | []byte(data.Password), 120 | []byte(payload.Password)); err != nil { 121 | res.Response(w, 401, nil, "Incorrect password provided for the User!") 122 | return 123 | } 124 | 125 | token_claims := auth.TokenClaims{ 126 | IsAdmin: data.IsAdmin, 127 | Username: data.Username, 128 | UserID: data.UserID.String()} 129 | 130 | access_token, err := auth.CreateJwtAccesToken(token_claims) 131 | if err != nil || access_token == "" { 132 | res.Response(w, 400, nil, "Could not create a valid JWT Access Token!") 133 | return 134 | } 135 | 136 | refresh_token, err := auth.CreateJwtRefreshToken(token_claims) 137 | if err != nil || refresh_token == "" { 138 | res.Response(w, 400, nil, "Could not create a valid JWT Refresh Token!") 139 | return 140 | } 141 | 142 | data_with_token := &res.ResponseWithJwt{ 143 | Data: data, 144 | JwtFields: res.JwtFields{AccessToken: access_token, RefreshToken: refresh_token}} 145 | 146 | res.Response(w, 200, data_with_token, "User Logged in!") 147 | } 148 | -------------------------------------------------------------------------------- /backend/public/gomarvin.gen.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file API client generated by gomarvin, DO NOT EDIT! 3 | * 4 | * Rename the current file if you want to 5 | * edit it and save changes. 6 | * 7 | * Repo : https://github.com/tompston/gomarvin 8 | * Docs : https://gomarvin.pages.dev/docs 9 | * Editor : https://gomarvin.pages.dev 10 | * project_name : "backend", 11 | * config_version : "0.1" 12 | * gomarvin_version : "v0.6.x" 13 | * 14 | */ 15 | 16 | /** The API client used by the fetch function */ 17 | export interface Client { 18 | host_url: string 19 | api_prefix: string 20 | headers: HeadersInit 21 | } 22 | 23 | /** Default api params */ 24 | export const defaultClient : Client = { 25 | host_url: "http://localhost:4444", 26 | api_prefix: "/api/v1", 27 | headers: { 28 | "Content-type": "application/json;charset=UTF-8", 29 | }, 30 | } 31 | 32 | 33 | /** 34 | * optional interface used in the fetch request with optional parameters 35 | * @param {RequestInit} [options] 36 | * If default fetch options need to be edited, provide a custom options object 37 | * @param {string} [append_url] 38 | * extend the url with custom params (like "?name=jim") 39 | * @example 40 | * // Append an optional url string 41 | * async function FetchUserById() { 42 | * const res = await F.GetUserById(client, 10, { append_url: "?name=jim" }); 43 | * console.log(res); 44 | * } 45 | * // Use a different RequestInit object in the fetch request 46 | * async function FetchUserById() { 47 | * const res = await F.GetUserById(client, 10, { options: { method: "POST" } }); 48 | * console.log(res); 49 | * } 50 | */ 51 | export interface OptionalParams { 52 | options?: RequestInit; 53 | append_url?: string; 54 | } 55 | 56 | 57 | /** Endpoints for the User module */ 58 | export const UserEndpoints = { 59 | GetUsers, 60 | GetUserByID, 61 | RegisterUser, 62 | DeleteUser, 63 | GetUserByUsername, 64 | LoginUser, 65 | GetUserDetailsWithAuth, 66 | } 67 | 68 | /** Endpoints for the Transaction module */ 69 | export const TransactionEndpoints = { 70 | GetTransactionsForUser, 71 | CreateTransaction, 72 | GetTransactionByID, 73 | } 74 | 75 | /** Endpoints for the Balance module */ 76 | export const BalanceEndpoints = { 77 | GetBalanceForUserWithAuth, 78 | } 79 | 80 | /** Endpoints for the Auth module */ 81 | export const AuthEndpoints = { 82 | TokenIsValid, 83 | } 84 | 85 | /** 86 | * ### Body for the RegisterUser endpoint 87 | * @interface RegisterUserBody 88 | * 89 | * @field username `required,min=5,max=250` 90 | * @field password `required,min=10,max=250` 91 | * @field email `required,email` 92 | */ 93 | export interface RegisterUserBody { 94 | username: string 95 | password: string 96 | email: string 97 | } 98 | 99 | /** 100 | * ### Body for the LoginUser endpoint 101 | * @interface LoginUserBody 102 | * 103 | * @field username `required,min=5,max=250` 104 | * @field password `required,min=10,max=250` 105 | */ 106 | export interface LoginUserBody { 107 | username: string 108 | password: string 109 | } 110 | 111 | /** 112 | * ### Body for the CreateTransaction endpoint 113 | * @interface CreateTransactionBody 114 | * 115 | * @field sender_id `required,uuid4` 116 | * @field reciever_id `required,uuid4` 117 | * @field amount `required` 118 | */ 119 | export interface CreateTransactionBody { 120 | sender_id: string 121 | reciever_id: string 122 | amount: number 123 | } 124 | 125 | /** 126 | * ### Body for the TokenIsValid endpoint 127 | * @interface TokenIsValidBody 128 | * 129 | * @field access_token `required` 130 | */ 131 | export interface TokenIsValidBody { 132 | access_token: string 133 | } 134 | 135 | 136 | /** 137 | * ### GET URL/user 138 | * Fetch GetUsers endpoint and return the promise of the response 139 | * @param {Client} client init settings for the api client (url and headers) 140 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 141 | * @returns {Promise} Promise of the fetch request 142 | */ 143 | export async function GetUsers(client : Client, opt?: OptionalParams): Promise { 144 | const appended_url = opt?.append_url ? opt?.append_url : ""; 145 | const url = `${client.host_url}${client.api_prefix}/user${appended_url}`; 146 | if (opt?.options) { 147 | return await fetch(url, opt?.options); 148 | } 149 | return await fetch(url, { 150 | method: "GET", 151 | headers: client.headers, 152 | }); 153 | } 154 | 155 | /** 156 | * ### GET URL/user/id/[id:string] 157 | * Fetch GetUserByID endpoint and return the promise of the response 158 | * @param {Client} client init settings for the api client (url and headers) 159 | * @param {string} id url param for the endpoint 160 | 161 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 162 | * @returns {Promise} Promise of the fetch request 163 | */ 164 | export async function GetUserByID(client : Client,id: string, opt?: OptionalParams): Promise { 165 | const appended_url = opt?.append_url ? opt?.append_url : ""; 166 | const url = `${client.host_url}${client.api_prefix}/user/id/${id}${appended_url}`; 167 | if (opt?.options) { 168 | return await fetch(url, opt?.options); 169 | } 170 | return await fetch(url, { 171 | method: "GET", 172 | headers: client.headers, 173 | }); 174 | } 175 | 176 | /** 177 | * ### POST URL/user/register 178 | * Fetch RegisterUser endpoint and return the promise of the response 179 | * @param {Client} client init settings for the api client (url and headers) 180 | * @param {RegisterUserBody} body see RegisterUserBody interface for validation fields 181 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 182 | * @returns {Promise} Promise of the fetch request 183 | */ 184 | export async function RegisterUser(client : Client,body: RegisterUserBody, opt?: OptionalParams ): Promise { 185 | const appended_url = opt?.append_url ? opt?.append_url : ""; 186 | const url = `${client.host_url}${client.api_prefix}/user/register${appended_url}`; 187 | if (opt?.options) { 188 | return await fetch(url, opt?.options); 189 | } 190 | return await fetch(url, { 191 | method: "POST", 192 | headers: client.headers, 193 | body: JSON.stringify(body)}); 194 | } 195 | 196 | /** 197 | * ### DELETE URL/user 198 | * Fetch DeleteUser endpoint and return the promise of the response 199 | * @param {Client} client init settings for the api client (url and headers) 200 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 201 | * @returns {Promise} Promise of the fetch request 202 | */ 203 | export async function DeleteUser(client : Client, opt?: OptionalParams): Promise { 204 | const appended_url = opt?.append_url ? opt?.append_url : ""; 205 | const url = `${client.host_url}${client.api_prefix}/user${appended_url}`; 206 | if (opt?.options) { 207 | return await fetch(url, opt?.options); 208 | } 209 | return await fetch(url, { 210 | method: "DELETE", 211 | headers: client.headers, 212 | }); 213 | } 214 | 215 | /** 216 | * ### GET URL/user/username/[username:string] 217 | * Fetch GetUserByUsername endpoint and return the promise of the response 218 | * @param {Client} client init settings for the api client (url and headers) 219 | * @param {string} username url param for the endpoint 220 | 221 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 222 | * @returns {Promise} Promise of the fetch request 223 | */ 224 | export async function GetUserByUsername(client : Client,username: string, opt?: OptionalParams): Promise { 225 | const appended_url = opt?.append_url ? opt?.append_url : ""; 226 | const url = `${client.host_url}${client.api_prefix}/user/username/${username}${appended_url}`; 227 | if (opt?.options) { 228 | return await fetch(url, opt?.options); 229 | } 230 | return await fetch(url, { 231 | method: "GET", 232 | headers: client.headers, 233 | }); 234 | } 235 | 236 | /** 237 | * ### POST URL/user/login 238 | * Fetch LoginUser endpoint and return the promise of the response 239 | * @param {Client} client init settings for the api client (url and headers) 240 | * @param {LoginUserBody} body see LoginUserBody interface for validation fields 241 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 242 | * @returns {Promise} Promise of the fetch request 243 | */ 244 | export async function LoginUser(client : Client,body: LoginUserBody, opt?: OptionalParams ): Promise { 245 | const appended_url = opt?.append_url ? opt?.append_url : ""; 246 | const url = `${client.host_url}${client.api_prefix}/user/login${appended_url}`; 247 | if (opt?.options) { 248 | return await fetch(url, opt?.options); 249 | } 250 | return await fetch(url, { 251 | method: "POST", 252 | headers: client.headers, 253 | body: JSON.stringify(body)}); 254 | } 255 | 256 | /** 257 | * ### GET URL/user/details 258 | * Fetch GetUserDetailsWithAuth endpoint and return the promise of the response 259 | * @param {Client} client init settings for the api client (url and headers) 260 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 261 | * @returns {Promise} Promise of the fetch request 262 | */ 263 | export async function GetUserDetailsWithAuth(client : Client, opt?: OptionalParams): Promise { 264 | const appended_url = opt?.append_url ? opt?.append_url : ""; 265 | const url = `${client.host_url}${client.api_prefix}/user/details${appended_url}`; 266 | if (opt?.options) { 267 | return await fetch(url, opt?.options); 268 | } 269 | return await fetch(url, { 270 | method: "GET", 271 | headers: client.headers, 272 | }); 273 | } 274 | 275 | 276 | /** 277 | * ### GET URL/transaction 278 | * Fetch GetTransactionsForUser endpoint and return the promise of the response 279 | * @param {Client} client init settings for the api client (url and headers) 280 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 281 | * @returns {Promise} Promise of the fetch request 282 | */ 283 | export async function GetTransactionsForUser(client : Client, opt?: OptionalParams): Promise { 284 | const appended_url = opt?.append_url ? opt?.append_url : ""; 285 | const url = `${client.host_url}${client.api_prefix}/transaction${appended_url}`; 286 | if (opt?.options) { 287 | return await fetch(url, opt?.options); 288 | } 289 | return await fetch(url, { 290 | method: "GET", 291 | headers: client.headers, 292 | }); 293 | } 294 | 295 | /** 296 | * ### POST URL/transaction 297 | * Fetch CreateTransaction endpoint and return the promise of the response 298 | * @param {Client} client init settings for the api client (url and headers) 299 | * @param {CreateTransactionBody} body see CreateTransactionBody interface for validation fields 300 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 301 | * @returns {Promise} Promise of the fetch request 302 | */ 303 | export async function CreateTransaction(client : Client,body: CreateTransactionBody, opt?: OptionalParams ): Promise { 304 | const appended_url = opt?.append_url ? opt?.append_url : ""; 305 | const url = `${client.host_url}${client.api_prefix}/transaction${appended_url}`; 306 | if (opt?.options) { 307 | return await fetch(url, opt?.options); 308 | } 309 | return await fetch(url, { 310 | method: "POST", 311 | headers: client.headers, 312 | body: JSON.stringify(body)}); 313 | } 314 | 315 | /** 316 | * ### GET URL/transaction/[transaction_id:string] 317 | * Fetch GetTransactionByID endpoint and return the promise of the response 318 | * @param {Client} client init settings for the api client (url and headers) 319 | * @param {string} transaction_id url param for the endpoint 320 | 321 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 322 | * @returns {Promise} Promise of the fetch request 323 | */ 324 | export async function GetTransactionByID(client : Client,transaction_id: string, opt?: OptionalParams): Promise { 325 | const appended_url = opt?.append_url ? opt?.append_url : ""; 326 | const url = `${client.host_url}${client.api_prefix}/transaction/${transaction_id}${appended_url}`; 327 | if (opt?.options) { 328 | return await fetch(url, opt?.options); 329 | } 330 | return await fetch(url, { 331 | method: "GET", 332 | headers: client.headers, 333 | }); 334 | } 335 | 336 | 337 | /** 338 | * ### GET URL/balance 339 | * Fetch GetBalanceForUserWithAuth endpoint and return the promise of the response 340 | * @param {Client} client init settings for the api client (url and headers) 341 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 342 | * @returns {Promise} Promise of the fetch request 343 | */ 344 | export async function GetBalanceForUserWithAuth(client : Client, opt?: OptionalParams): Promise { 345 | const appended_url = opt?.append_url ? opt?.append_url : ""; 346 | const url = `${client.host_url}${client.api_prefix}/balance${appended_url}`; 347 | if (opt?.options) { 348 | return await fetch(url, opt?.options); 349 | } 350 | return await fetch(url, { 351 | method: "GET", 352 | headers: client.headers, 353 | }); 354 | } 355 | 356 | 357 | /** 358 | * ### POST URL/auth/token 359 | * Fetch TokenIsValid endpoint and return the promise of the response 360 | * @param {Client} client init settings for the api client (url and headers) 361 | * @param {TokenIsValidBody} body see TokenIsValidBody interface for validation fields 362 | * @param {OptionalParams} [opt] optional params you can add to the request ( appended_url and custom options ) 363 | * @returns {Promise} Promise of the fetch request 364 | */ 365 | export async function TokenIsValid(client : Client,body: TokenIsValidBody, opt?: OptionalParams ): Promise { 366 | const appended_url = opt?.append_url ? opt?.append_url : ""; 367 | const url = `${client.host_url}${client.api_prefix}/auth/token${appended_url}`; 368 | if (opt?.options) { 369 | return await fetch(url, opt?.options); 370 | } 371 | return await fetch(url, { 372 | method: "POST", 373 | headers: client.headers, 374 | body: JSON.stringify(body)}); 375 | } 376 | 377 | -------------------------------------------------------------------------------- /backend/public/placeholder.ts: -------------------------------------------------------------------------------- 1 | export interface PlaceholderUser { 2 | email: string 3 | username: string 4 | password: string 5 | } 6 | 7 | export const placeholderUser: PlaceholderUser = { 8 | email: "hello@gmail.com", 9 | username: "placeholder-user", 10 | password: "very-strong-password", 11 | } 12 | 13 | export const placeholderUserSecond: PlaceholderUser = { 14 | email: "placeholder-user-2@gmail.com", 15 | username: "placeholder-user-2", 16 | password: "very-strong-password-2", 17 | } -------------------------------------------------------------------------------- /backend/settings/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "backend/settings" 7 | "fmt" 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | var DB *sql.DB 12 | 13 | // create the dsn string from variables that are specified in the .env file 14 | func DsnString() string { 15 | 16 | dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s", 17 | settings.DB_HOST, 18 | settings.DB_USER, 19 | settings.DB_PASS, 20 | settings.DB_NAME, 21 | settings.DB_PORT, 22 | settings.DB_SSL, 23 | settings.DB_TZ) 24 | return dsn 25 | } 26 | 27 | // call a db connection without using the global db variable 28 | func GetDbConn() (*sql.DB, error) { 29 | 30 | db, err := sql.Open("postgres", DsnString()) 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | return db, err 36 | 37 | } 38 | 39 | func Connect() { 40 | 41 | db, err := sql.Open("postgres", DsnString()) 42 | 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | // assign *sql.DB to the global variable 48 | DB = db 49 | 50 | } 51 | -------------------------------------------------------------------------------- /backend/settings/env.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/joho/godotenv" 8 | ) 9 | 10 | // load all of the env variables from .env 11 | func LoadEnvFile() { 12 | envErr := godotenv.Load(".env") 13 | if envErr != nil { 14 | fmt.Println("Error loading .env file") 15 | } 16 | } 17 | 18 | // load a specified .env variable 19 | func Config(key string) string { 20 | err := godotenv.Load(".env") 21 | if err != nil { 22 | fmt.Print("Error loading .env file") 23 | } 24 | return os.Getenv(key) 25 | } 26 | -------------------------------------------------------------------------------- /backend/settings/jwt-time.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Pass in the number of minutes, to convert it to the corresponding time.Time value 8 | func JwtExpirationTime(jwt_token_duration int) time.Time { 9 | duration := time.Duration(jwt_token_duration) 10 | expiration_time := time.Now().Add(duration * time.Minute) 11 | return expiration_time 12 | } 13 | 14 | // Return the access token duration that is used in the jwt auth string 15 | func AccessTokenDuration() time.Time { 16 | return JwtExpirationTime(JWT_ACCESS_TOKEN_DURATION) 17 | } 18 | 19 | // Return the refresh token duration that is used in the jwt auth string 20 | func RefreshTokenDuration() time.Time { 21 | return JwtExpirationTime(JWT_REFRESH_TOKEN_DURATION) 22 | } 23 | -------------------------------------------------------------------------------- /backend/settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // .env variables. Name of the variable is mapped to the env var name. 8 | var ( 9 | // Go .env vars 10 | GO_BACKEND_PORT = Config("GO_BACKEND_PORT") 11 | DEBUG_MODE = Config("DEBUG_MODE") 12 | HOST_URL = Config("HOST_URL") 13 | API_PATH = Config("API_PATH") 14 | BASE_URL = fmt.Sprintf("%s%s", HOST_URL, API_PATH) // host + api prefix path 15 | // DB .env vars 16 | DB_PASS = Config("DB_PASS") 17 | DB_HOST = Config("DB_HOST") 18 | DB_USER = Config("DB_USER") 19 | DB_NAME = Config("DB_NAME") 20 | DB_PORT = Config("DB_PORT") 21 | DB_SSL = Config("DB_SSL") 22 | DB_TZ = Config("DB_TZ") 23 | 24 | // JWT Variables 25 | JWT_KEY = Config("JWT_KEY") 26 | ) 27 | 28 | const ( 29 | PAGINATION_LIMIT = 20 // specify how many rows can be returned in a paginated response 30 | JWT_ACCESS_COOKIE_NAME = "access_cookie" 31 | JWT_REFRESH_COOKIE_NAME = "refresh_cookie" 32 | JWT_ACCESS_TOKEN_DURATION = 10 // time in minutes 33 | JWT_REFRESH_TOKEN_DURATION = 15 // time in minutes 34 | // 35 | AUTHENTICATION_HEADER_KEY = "Authorization" 36 | ) 37 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Tailwind 2 | 3 | ```js 4 | 7 | 8 | 9 | 10 | 11 | // center content 12 |
13 |
content
14 |
15 | ``` -------------------------------------------------------------------------------- /frontend/backups/Authenticate_previous.txt: -------------------------------------------------------------------------------- 1 | 37 | 38 | 71 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | User Auth Example App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.2.45", 13 | "vue-router": "^4.1.6" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "^4.0.0", 17 | "autoprefixer": "^10.4.13", 18 | "postcss": "^8.4.21", 19 | "tailwindcss": "^3.2.4", 20 | "typescript": "^4.9.3", 21 | "vite": "^4.0.0", 22 | "vue-tsc": "^1.0.11" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/css/base/_custom.css: -------------------------------------------------------------------------------- 1 | /* place for custom / new css variables */ 2 | :root {} 3 | 4 | [data-theme="dark"], 5 | [data-theme="dark"] :root {} 6 | 7 | 8 | /* Remove the link colors for all a tags */ 9 | a { 10 | color: inherit !important; 11 | text-decoration: none; 12 | } 13 | 14 | .input__1{ 15 | border: var(--border-1-3); 16 | border-radius: var(--border-rad-4); 17 | padding: 3px 12px 3px 12px; 18 | font-size: var(--fs-8); 19 | background-color: var(--bg-light-select-1); 20 | transition: all 0.14s ease-in; 21 | width: 100%; 22 | } 23 | 24 | .input__1:focus{ 25 | border: 1px solid rgb(76, 94, 255); 26 | } 27 | 28 | .input__1-label { 29 | font-size: 11px; 30 | font-weight: 700; 31 | opacity: 0.5; 32 | } 33 | 34 | .button__1 { 35 | padding: 6px 16px; 36 | border-radius: var(--border-rad-4); 37 | background-color: black; 38 | color: white; 39 | font-size: var(--fs-10); 40 | font-weight: 700; 41 | transition: all 0.14s ease-in; 42 | } 43 | .button__1:hover{ 44 | background-color: gray; 45 | } -------------------------------------------------------------------------------- /frontend/src/assets/css/base/html.css: -------------------------------------------------------------------------------- 1 | html { 2 | scroll-behavior: smooth; 3 | height: 100%; 4 | -webkit-text-size-adjust: 100%; 5 | /* make the scrollbar not take up the total 6 | space of the page, so that there is no shift 7 | once it appears / dissapears */ 8 | overflow-y: scroll; 9 | cursor: default; 10 | } 11 | 12 | body { 13 | padding: 0; 14 | margin: 0; 15 | height: 100%; 16 | min-height: 100%; 17 | width: 100%; 18 | touch-action: manipulation; 19 | text-rendering: optimizeLegibility; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | font-family: var(--global-font); 23 | background: var(--global-bg-col); 24 | color: var(--global-txt-col); 25 | } 26 | 27 | ::selection { 28 | color: var(--selection-txt-col); 29 | background-color: var(--selection-bg-col); 30 | } 31 | 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6 { 38 | font-weight: unset; 39 | } 40 | 41 | ul, 42 | ol { 43 | margin: 0px; 44 | padding-left: 33px; 45 | } 46 | 47 | h1, 48 | h2, 49 | h3, 50 | h4, 51 | h5, 52 | h6, 53 | hr, 54 | p, 55 | pre { 56 | margin: 0; 57 | } 58 | 59 | button { 60 | background: none; 61 | cursor: pointer; 62 | color: inherit; 63 | font: inherit; 64 | padding: 0; 65 | } 66 | 67 | button:disabled, 68 | button[disabled] { 69 | opacity: 0.3; 70 | } 71 | 72 | code, 73 | .code { 74 | border-radius: var(--code-border-rad); 75 | font-family: var(--font-mono); 76 | padding: var(--code-padding); 77 | background: var(--code-bg); 78 | font-size: var(--code-fs); 79 | color: var(--code-txt); 80 | } 81 | 82 | .code { 83 | max-width: 100%; 84 | -moz-box-sizing: border-box; 85 | -webkit-box-sizing: border-box; 86 | box-sizing: border-box; 87 | } 88 | 89 | input, 90 | textarea { 91 | padding: 0; 92 | margin: 0; 93 | } 94 | 95 | textarea { 96 | resize: vertical; 97 | } 98 | 99 | /* removes the border on focus for inputs */ 100 | input:focus, 101 | textarea:focus { 102 | outline: none; 103 | } 104 | 105 | input::placeholder, 106 | textarea::placeholder { 107 | opacity: 1; 108 | font-size: var(--placeholder-fs); 109 | color: var(--placeholder-txt); 110 | } 111 | 112 | a, 113 | a:hover, 114 | a:focus, 115 | a:active { 116 | color: inherit; 117 | } 118 | 119 | /* disable text select utility class + applied to buttons, images and inputs */ 120 | .disable-text-select, 121 | button, 122 | img, 123 | input::placeholder, 124 | textarea::placeholder { 125 | -moz-user-select: none; 126 | -webkit-user-select: none; 127 | -webkit-touch-callout: none; 128 | user-select: none; 129 | } 130 | 131 | ol li { 132 | margin-left: 20px; 133 | list-style: decimal; 134 | } 135 | 136 | /* Link tag colors */ 137 | a { 138 | font-weight: var(--link-fw); 139 | color: var(--link-txt); 140 | } 141 | 142 | a:hover { 143 | color: var(--link-txt-hover); 144 | text-decoration: underline; 145 | } 146 | 147 | a:active{ 148 | color: var(--link-txt-active); 149 | } 150 | 151 | a:visited{ 152 | color: var(--link-txt-visited); 153 | } 154 | -------------------------------------------------------------------------------- /frontend/src/assets/css/base/media.css: -------------------------------------------------------------------------------- 1 | .max-width-1, 2 | .max-width-2, 3 | .max-width-3, 4 | .max-width-3, 5 | .max-width-4, 6 | .max-width-5, 7 | .max-width-6 { 8 | padding: 0px 30px; 9 | } 10 | 11 | /* xl */ 12 | @media (max-width: 1600px) { 13 | .hide-on-xl { 14 | display: none; 15 | } 16 | .show-on-xl { 17 | display: block !important; 18 | } 19 | } 20 | 21 | /* lg */ 22 | @media (max-width: 1064px) { 23 | .max-width-1, 24 | .max-width-2, 25 | .max-width-3 { 26 | padding: 0px 20px; 27 | } 28 | 29 | .hide-on-lg { 30 | display: none; 31 | } 32 | .show-on-lg { 33 | display: block !important; 34 | } 35 | 36 | :root { 37 | --fs-1: 54px; 38 | --fs-2: 39px; 39 | --fs-3: 34px; 40 | --fs-4: 27px; 41 | --fs-5: 22px; 42 | --fs-6: 20px; 43 | --fs-7: 18px; 44 | --fs-8: 15px; 45 | --fs-9: 14px; 46 | } 47 | } 48 | 49 | /* md */ 50 | @media (max-width: 920px) { 51 | .hide-on-md { 52 | display: none; 53 | } 54 | .show-on-md { 55 | display: block !important; 56 | } 57 | } 58 | 59 | /* sm */ 60 | @media (max-width: 750px) { 61 | .hide-on-sm { 62 | display: none; 63 | } 64 | .show-on-sm { 65 | display: block !important; 66 | } 67 | } 68 | 69 | /* xs */ 70 | @media (max-width: 550px) { 71 | :root { 72 | --fs-1: 35px; 73 | --fs-2: 29px; 74 | --fs-3: 26px; 75 | --fs-4: 23px; 76 | --fs-5: 21px; 77 | --fs-6: 19px; 78 | --fs-7: 18px; 79 | --fs-8: 15px; 80 | --fs-9: 14px; 81 | } 82 | 83 | .hide-on-xs { 84 | display: none; 85 | } 86 | .show-on-xs { 87 | display: block !important; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /frontend/src/assets/css/base/root.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* max-width for the main content */ 3 | --max-width-1: 1325px; 4 | --max-width-2: 1205px; 5 | --max-width-3: 1015px; 6 | --max-width-4: 940px; 7 | --max-width-5: 780px; 8 | --max-width-6: 540px; 9 | 10 | /* font-family, used when custom fonts imported */ 11 | --font-fam-1: sans-serif; 12 | --font-fam-2: sans-serif; 13 | --font-fam-3: sans-serif; 14 | --font-fam-4: sans-serif; 15 | --font-fam-5: sans-serif; 16 | --font-fam-6: sans-serif; 17 | 18 | --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; 19 | 20 | /* define the color palette ( mapped to main-col-x and text-col-x ) */ 21 | --main-col-1: #000000; 22 | --main-col-2: #ffffff; 23 | --main-col-3: #ffffff; 24 | --main-col-4: #ffffff; 25 | --main-col-5: #ffffff; 26 | --main-col-6: #ffffff; 27 | --main-col-7: #ffffff; 28 | --main-col-8: #ffffff; 29 | --main-col-9: #ffffff; 30 | 31 | /* font-size */ 32 | --fs-1: 78px; 33 | --fs-2: 64px; 34 | --fs-3: 44px; 35 | --fs-4: 30px; 36 | --fs-5: 25px; 37 | --fs-6: 20px; 38 | --fs-7: 18px; 39 | --fs-8: 15px; 40 | --fs-9: 14px; 41 | --fs-10: 12px; 42 | 43 | /* border-radius */ 44 | --border-rad-1: 12px; 45 | --border-rad-2: 10px; 46 | --border-rad-3: 7px; 47 | --border-rad-4: 5px; 48 | --border-rad-5: 3px; 49 | --border-rad-6: 0px; 50 | 51 | /* hr color */ 52 | --hr-1-col: rgba(29, 29, 29, 0.4); 53 | --hr-2-col: rgba(29, 29, 29, 0.2); 54 | --hr-3-col: rgba(29, 29, 29, 0.1); 55 | 56 | /* borders */ 57 | --border-1-1: 1px solid rgba(48, 48, 48, 0.3); 58 | --border-1-2: 1px solid rgba(48, 48, 48, 0.15); 59 | --border-1-3: 1px solid rgba(48, 48, 48, 0.08); 60 | --border-2-1: 2px solid rgba(48, 48, 48, 0.3); 61 | --border-2-2: 2px solid rgba(48, 48, 48, 0.15); 62 | --border-2-3: 2px solid rgba(48, 48, 48, 0.04); 63 | 64 | /* box-shadow */ 65 | --shadow-1: rgba(0, 0, 0, 0.3) 0px 3px 7px; 66 | --shadow-2: rgba(0, 0, 0, 0.2) 0px 3px 5px; 67 | --shadow-3: rgba(0, 0, 0, 0.24) 0px 3px 8px; 68 | --shadow-4: rgba(0, 0, 0, 0.2) 2px 7px 15px -3px; 69 | --shadow-5: rgba(149, 157, 165, 0.3) 0px 2px 10px; 70 | --shadow-6: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px; 71 | 72 | /* colors associated with states */ 73 | --danger-1-bg: rgb(225, 86, 86); 74 | --danger-1-txt: rgb(255, 255, 255); 75 | --success-1-bg: rgb(114, 213, 48); 76 | --success-1-txt: rgb(255, 255, 255); 77 | 78 | /* global properties attached to body */ 79 | --global-bg-col: white; 80 | --global-txt-col: rgb(46, 46, 46); 81 | --global-font: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 82 | Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", 83 | sans-serif; 84 | 85 | /* cursor selection colors */ 86 | --selection-bg-col: rgba(126, 167, 243, 0.65); 87 | --selection-txt-col: rgb(255, 255, 255); 88 | /* light bg colors */ 89 | --bg-light-select-1: rgb(251, 251, 251); 90 | --bg-light-select-2: rgb(245, 245, 245); 91 | --bg-light-select-3: rgb(240, 240, 240); 92 | 93 | /* code element properties */ 94 | --code-bg: rgb(243, 243, 243); 95 | --code-txt: rgb(48, 48, 48); 96 | --code-padding: 3px 7px; 97 | --code-border-rad: 6px; 98 | --code-fs: 13px; 99 | 100 | /* input placeholder properties */ 101 | --placeholder-fs: 12px; 102 | --placeholder-txt: #9299a4; 103 | 104 | /* svg */ 105 | --svg-dark-1: rgb(30, 30, 30); 106 | 107 | /* a href tags */ 108 | --link-fw: 500; 109 | --link-txt: rgb(74, 144, 248); 110 | --link-txt-hover: rgb(74, 144, 248); 111 | --link-txt-active: rgb(74, 144, 248); 112 | --link-txt-visited: rgb(74, 144, 248); 113 | 114 | } 115 | 116 | [data-theme="dark"], 117 | [data-theme="dark"] :root { 118 | --global-bg-col: rgb(23, 25, 26); 119 | --global-txt-col: rgb(232, 230, 227); 120 | 121 | /* */ 122 | --border-1-1: 1px solid rgba(255, 255, 255, 0.3); 123 | --border-1-2: 1px solid rgba(255, 255, 255, 0.15); 124 | --border-1-3: 1px solid rgba(255, 255, 255, 0.08); 125 | --border-2-1: 2px solid rgba(255, 255, 255, 0.3); 126 | --border-2-2: 2px solid rgba(255, 255, 255, 0.15); 127 | --border-2-3: 2px solid rgba(255, 255, 255, 0.076); 128 | 129 | /* */ 130 | --code-bg: #0e0e0e; 131 | --code-txt: #e9e9e9; 132 | 133 | /* */ 134 | --bg-light-select-1: rgb(21, 21, 21); 135 | --bg-light-select-2: rgb(30, 30, 30); 136 | --bg-light-select-3: rgb(38, 38, 38); 137 | 138 | /* */ 139 | --hr-1-col: rgba(228, 228, 228, 0.4); 140 | --hr-2-col: rgba(226, 226, 226, 0.2); 141 | --hr-3-col: rgba(226, 226, 226, 0.1); 142 | } 143 | 144 | /* 145 | 146 | -- Centering content + full width backgrounds in html 147 | 148 |
149 |
content
150 |
151 | 152 | */ 153 | /* max-width */ 154 | .max-width-1 { 155 | width: var(--max-width-1); 156 | } 157 | 158 | .max-width-2 { 159 | width: var(--max-width-2); 160 | } 161 | 162 | .max-width-3 { 163 | width: var(--max-width-3); 164 | } 165 | 166 | .max-width-4 { 167 | width: var(--max-width-4); 168 | } 169 | 170 | .max-width-5 { 171 | width: var(--max-width-5); 172 | } 173 | 174 | .max-width-6 { 175 | width: var(--max-width-6); 176 | } 177 | 178 | /* background-color */ 179 | .main-col-1 { 180 | background-color: var(--main-col-1); 181 | } 182 | 183 | .main-col-2 { 184 | background-color: var(--main-col-2); 185 | } 186 | 187 | .main-col-3 { 188 | background-color: var(--main-col-3); 189 | } 190 | 191 | .main-col-4 { 192 | background-color: var(--main-col-4); 193 | } 194 | 195 | .main-col-5 { 196 | background-color: var(--main-col-5); 197 | } 198 | 199 | .main-col-6 { 200 | background-color: var(--main-col-6); 201 | } 202 | 203 | .main-col-7 { 204 | background-color: var(--main-col-7); 205 | } 206 | 207 | .main-col-8 { 208 | background-color: var(--main-col-8); 209 | } 210 | 211 | .main-col-9 { 212 | background-color: var(--main-col-9); 213 | } 214 | 215 | /* bord-rad */ 216 | .border-rad-1 { 217 | border-radius: var(--border-rad-1); 218 | } 219 | 220 | .border-rad-2 { 221 | border-radius: var(--border-rad-2); 222 | } 223 | 224 | .border-rad-3 { 225 | border-radius: var(--border-rad-3); 226 | } 227 | 228 | .border-rad-4 { 229 | border-radius: var(--border-rad-4); 230 | } 231 | 232 | .border-rad-5 { 233 | border-radius: var(--border-rad-5); 234 | } 235 | 236 | .border-rad-6 { 237 | border-radius: var(--border-rad-6); 238 | } 239 | 240 | /* font-family */ 241 | .font-fam-1 { 242 | font-family: var(--font-fam-1); 243 | } 244 | 245 | .font-fam-2 { 246 | font-family: var(--font-fam-2); 247 | } 248 | 249 | .font-fam-3 { 250 | font-family: var(--font-fam-3); 251 | } 252 | 253 | .font-fam-4 { 254 | font-family: var(--font-fam-4); 255 | } 256 | 257 | .font-fam-5 { 258 | font-family: var(--font-fam-5); 259 | } 260 | 261 | .font-fam-6 { 262 | font-family: var(--font-fam-6); 263 | } 264 | 265 | /* text-color mapped to main-col-x */ 266 | .text-col-1 { 267 | color: var(--main-col-1); 268 | } 269 | 270 | .text-col-2 { 271 | color: var(--main-col-2); 272 | } 273 | 274 | .text-col-3 { 275 | color: var(--main-col-3); 276 | } 277 | 278 | .text-col-4 { 279 | color: var(--main-col-4); 280 | } 281 | 282 | .text-col-5 { 283 | color: var(--main-col-5); 284 | } 285 | 286 | .text-col-6 { 287 | color: var(--main-col-6); 288 | } 289 | 290 | .text-col-7 { 291 | color: var(--main-col-7); 292 | } 293 | 294 | .text-col-8 { 295 | color: var(--main-col-8); 296 | } 297 | 298 | .text-col-9 { 299 | color: var(--main-col-9); 300 | } 301 | 302 | /* font-size */ 303 | .fs-1, 304 | h1 { 305 | font-size: var(--fs-1) !important; 306 | } 307 | 308 | .fs-2, 309 | h2 { 310 | font-size: var(--fs-2) !important; 311 | } 312 | 313 | .fs-3, 314 | h3 { 315 | font-size: var(--fs-3) !important; 316 | } 317 | 318 | .fs-4, 319 | h4 { 320 | font-size: var(--fs-4) !important; 321 | } 322 | 323 | .fs-5, 324 | h5 { 325 | font-size: var(--fs-5) !important; 326 | } 327 | 328 | .fs-6, 329 | h6 { 330 | font-size: var(--fs-6) !important; 331 | } 332 | 333 | .fs-7 { 334 | font-size: var(--fs-7) !important; 335 | } 336 | 337 | .fs-8 { 338 | font-size: var(--fs-8) !important; 339 | } 340 | 341 | .fs-9 { 342 | font-size: var(--fs-9) !important; 343 | } 344 | 345 | .fs-10 { 346 | font-size: var(--fs-10) !important; 347 | } 348 | 349 | /* shadow-x */ 350 | .shadow-1 { 351 | box-shadow: var(--shadow-1); 352 | } 353 | 354 | .shadow-2 { 355 | box-shadow: var(--shadow-2); 356 | } 357 | 358 | .shadow-3 { 359 | box-shadow: var(--shadow-3); 360 | } 361 | 362 | .shadow-4 { 363 | box-shadow: var(--shadow-4); 364 | } 365 | 366 | .shadow-5 { 367 | box-shadow: var(--shadow-5); 368 | } 369 | 370 | .shadow-6 { 371 | box-shadow: var(--shadow-6); 372 | } 373 | 374 | /* text */ 375 | .text-1 { 376 | font-size: var(--fs-6); 377 | } 378 | 379 | .text-2 { 380 | font-size: var(--fs-7); 381 | } 382 | 383 | .text-3 { 384 | font-size: var(--fs-8); 385 | } 386 | 387 | .text-4 { 388 | font-size: var(--fs-9); 389 | } 390 | 391 | /* hr with a height of 1px, and decreasing color intensity */ 392 | .hr-1-1 { 393 | border: 0; 394 | height: 1px; 395 | background: var(--hr-1-col); 396 | } 397 | 398 | .hr-1-2 { 399 | border: 0; 400 | height: 1px; 401 | background: var(--hr-2-col); 402 | } 403 | 404 | .hr-1-3 { 405 | border: 0; 406 | height: 1px; 407 | background: var(--hr-3-col); 408 | } 409 | 410 | /* hr with a height of 2px, and decreasing color intensity */ 411 | .hr-2-1 { 412 | border: 0; 413 | height: 2px; 414 | background: var(--hr-1-col); 415 | } 416 | 417 | .hr-2-2 { 418 | border: 0; 419 | height: 2px; 420 | background: var(--hr-2-col); 421 | } 422 | 423 | .hr-2-3 { 424 | border: 0; 425 | height: 2px; 426 | background: var(--hr-3-col); 427 | } 428 | 429 | /* border */ 430 | .border-1-1 { 431 | border: var(--border-1-1); 432 | } 433 | 434 | .border-1-2 { 435 | border: var(--border-1-2); 436 | } 437 | 438 | .border-1-3 { 439 | border: var(--border-1-3); 440 | } 441 | 442 | .border-2-1 { 443 | border: var(--border-2-1); 444 | } 445 | 446 | .border-2-2 { 447 | border: var(--border-2-2); 448 | } 449 | 450 | .border-2-3 { 451 | border: var(--border-2-3); 452 | } 453 | 454 | /* border-side-1px */ 455 | .border-b-1-1 { 456 | border-bottom: var(--border-1-1); 457 | } 458 | 459 | .border-b-1-2 { 460 | border-bottom: var(--border-1-2); 461 | } 462 | 463 | .border-b-1-3 { 464 | border-bottom: var(--border-1-3); 465 | } 466 | 467 | .border-t-1-1 { 468 | border-top: var(--border-1-1); 469 | } 470 | 471 | .border-t-1-2 { 472 | border-top: var(--border-1-2); 473 | } 474 | 475 | .border-t-1-3 { 476 | border-top: var(--border-1-3); 477 | } 478 | 479 | .border-l-1-1 { 480 | border-left: var(--border-1-1); 481 | } 482 | 483 | .border-l-1-2 { 484 | border-left: var(--border-1-2); 485 | } 486 | 487 | .border-l-1-3 { 488 | border-left: var(--border-1-3); 489 | } 490 | 491 | .border-r-1-1 { 492 | border-right: var(--border-1-1); 493 | } 494 | 495 | .border-r-1-2 { 496 | border-right: var(--border-1-2); 497 | } 498 | 499 | .border-r-1-3 { 500 | border-right: var(--border-1-3); 501 | } 502 | 503 | /* border-side-2px */ 504 | .border-b-2-1 { 505 | border-bottom: var(--border-2-1); 506 | } 507 | 508 | .border-b-2-2 { 509 | border-bottom: var(--border-2-2); 510 | } 511 | 512 | .border-b-2-3 { 513 | border-bottom: var(--border-2-3); 514 | } 515 | 516 | .border-t-2-1 { 517 | border-top: var(--border-2-1); 518 | } 519 | 520 | .border-t-2-2 { 521 | border-top: var(--border-2-2); 522 | } 523 | 524 | .border-t-2-3 { 525 | border-top: var(--border-2-3); 526 | } 527 | 528 | .border-l-2-1 { 529 | border-left: var(--border-2-1); 530 | } 531 | 532 | .border-l-2-2 { 533 | border-left: var(--border-2-2); 534 | } 535 | 536 | .border-l-2-3 { 537 | border-left: var(--border-2-3); 538 | } 539 | 540 | .border-r-2-1 { 541 | border-right: var(--border-2-1); 542 | } 543 | 544 | .border-r-2-2 { 545 | border-right: var(--border-2-2); 546 | } 547 | 548 | .border-r-2-3 { 549 | border-right: var(--border-2-3); 550 | } 551 | 552 | /* state colors */ 553 | .danger-1-bg { 554 | background-color: var(--danger-1-bg); 555 | } 556 | 557 | .danger-1-txt { 558 | color: var(--danger-1-txt); 559 | } 560 | 561 | .success-1-bg { 562 | background-color: var(--success-1-bg); 563 | } 564 | 565 | .success-1-txt { 566 | color: var(--success-1-txt); 567 | } 568 | 569 | /* */ 570 | .bg-light-select-1 { 571 | background-color: var(--bg-light-select-1); 572 | } 573 | 574 | .bg-light-select-2 { 575 | background-color: var(--bg-light-select-2); 576 | } 577 | 578 | .bg-light-select-3 { 579 | background-color: var(--bg-light-select-3); 580 | } 581 | 582 | .font-mono { 583 | font-family: var(--font-mono); 584 | } 585 | 586 | /* */ 587 | .flex-center { 588 | display: flex; 589 | align-items: center; 590 | justify-content: center; 591 | } 592 | 593 | /* fw-x */ 594 | .fw-100 { 595 | font-weight: 100; 596 | } 597 | 598 | .fw-200 { 599 | font-weight: 200; 600 | } 601 | 602 | .fw-300 { 603 | font-weight: 300; 604 | } 605 | 606 | .fw-400 { 607 | font-weight: 400; 608 | } 609 | 610 | .fw-500 { 611 | font-weight: 500; 612 | } 613 | 614 | .fw-600 { 615 | font-weight: 600; 616 | } 617 | 618 | .fw-700 { 619 | font-weight: 700; 620 | } 621 | 622 | .fw-800 { 623 | font-weight: 800; 624 | } 625 | 626 | .fw-900 { 627 | font-weight: 900; 628 | } 629 | 630 | /* other helper classes */ 631 | .hidden { 632 | display: none; 633 | } 634 | 635 | .round { 636 | border-radius: 999px; 637 | } 638 | 639 | .pointer { 640 | cursor: pointer; 641 | } 642 | 643 | .align-text-vertically { 644 | display: flex; 645 | align-items: center; 646 | } 647 | 648 | .remove-decorations { 649 | text-decoration: none; 650 | } 651 | 652 | .hover-pointer:hover { 653 | cursor: pointer; 654 | } 655 | 656 | .hover-underline:hover { 657 | text-decoration: underline; 658 | } -------------------------------------------------------------------------------- /frontend/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | @import "./base/root.css"; 2 | @import "./base/html.css"; 3 | @import "./base/_custom.css"; 4 | @import "./base/media.css"; 5 | 6 | @tailwind base; 7 | @tailwind components; 8 | @tailwind utilities; -------------------------------------------------------------------------------- /frontend/src/assets/ts/API.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * data.data interface that is returned when 3 | * payload validation fails 4 | */ 5 | export interface FieldValidationErrors { 6 | errors: FieldValidationError[] 7 | } 8 | 9 | export interface FieldValidationError { 10 | failed_field: string 11 | message: string 12 | } 13 | 14 | export function isValidationErrors(value: any): value is FieldValidationErrors { 15 | return value.hasOwnProperty('errors'); 16 | } 17 | 18 | /** 19 | * Filter an array of objects to return the message 20 | * based on the failed_field key 21 | */ 22 | export function errorMessageForFieldName(fields: any[], field_key: string) { 23 | const filteredArray = fields.filter(item => item.failed_field === field_key); 24 | const string = filteredArray.map(item => item.message).join(', '); 25 | return string 26 | } -------------------------------------------------------------------------------- /frontend/src/assets/ts/auth.ts: -------------------------------------------------------------------------------- 1 | import { StorageKey } from "./localstorage" 2 | import * as F from "../../../../backend/public/gomarvin.gen" 3 | 4 | /** 5 | * Object that holds all of the util functions 6 | * that are used for authentication. 7 | */ 8 | export const Auth = { 9 | /** 10 | * Name of the localstorage key for the value that 11 | * holds the auth token 12 | */ 13 | ACCESS_TOKEN_KEY: (): string => "access_token" 14 | } 15 | 16 | 17 | /** 18 | * Flow that validates if the user is authenticated 19 | * - if localstorage auth token exists and is valid, return true 20 | * - Else return false 21 | */ 22 | export async function isAuthenticated(): Promise { 23 | let key = StorageKey.Get(Auth.ACCESS_TOKEN_KEY()) 24 | if (key == null) return false 25 | 26 | const res = await F.AuthEndpoints.TokenIsValid(F.defaultClient, { access_token: key }) 27 | 28 | if (res.status === 401) return false 29 | 30 | if (res.status === 200) { 31 | const data = await res.json() 32 | // console.log(data) 33 | if (data.data === true) return true 34 | } 35 | 36 | return false 37 | } 38 | 39 | export function GetAuthToken(): string { 40 | const key = StorageKey.Get(Auth.ACCESS_TOKEN_KEY()) 41 | return key ? key : "" 42 | } -------------------------------------------------------------------------------- /frontend/src/assets/ts/client.ts: -------------------------------------------------------------------------------- 1 | import * as F from "../../../../backend/public/gomarvin.gen" 2 | 3 | /** Pass in the auth token to set it in the header */ 4 | export function clientWithAuth(authToken: string): F.Client { 5 | const init = F.defaultClient 6 | 7 | const client: F.Client = { 8 | api_prefix: init.api_prefix, 9 | host_url: init.host_url, 10 | headers: { 11 | "Content-type": "application/json;charset=UTF-8", 12 | "Authorization": `Bearer ${authToken}` 13 | } 14 | } 15 | 16 | return client 17 | } -------------------------------------------------------------------------------- /frontend/src/assets/ts/index.ts: -------------------------------------------------------------------------------- 1 | export { StorageKey } from "./localstorage" 2 | export { Auth } from "./auth" 3 | export * from "./API" -------------------------------------------------------------------------------- /frontend/src/assets/ts/localstorage.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Object that holds all of the util functions that are 4 | * associated with localstorage manipulation 5 | */ 6 | export const StorageKey = { 7 | /** 8 | * Set and store a value in localstorage. 9 | * Note that this overrides the value that was 10 | * stored for the key previously. 11 | */ 12 | Set: function (key: string, value: string): void { 13 | localStorage.setItem(key, value) 14 | }, 15 | /** 16 | * If localstorage key exists, return true. 17 | * Else return false. 18 | */ 19 | Exists: function (key: string): boolean { 20 | return localStorage.getItem(key) ? true : false 21 | }, 22 | /** 23 | * Delete Key from localstorage 24 | */ 25 | Delete: function (key: string): void { 26 | localStorage.removeItem(key); 27 | }, 28 | /** 29 | * 30 | */ 31 | Get: function (key: string): string | null { 32 | return localStorage.getItem(key) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/global/ApiValidationFailedErrors.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/components/global/Container.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/global/DebugGrid.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/components/global/Header.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/global/InputComponent.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/global/LoadingSpinner.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/pages/Authenticate/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | -------------------------------------------------------------------------------- /frontend/src/components/pages/Authenticate/SignupForm.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 72 | -------------------------------------------------------------------------------- /frontend/src/components/pages/Home/TransactionsTable.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/components/pages/Home/UserDetails.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /frontend/src/layout/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import router from './router' 3 | import './assets/css/index.css' 4 | import App from './App.vue' 5 | 6 | createApp(App).use(router).mount('#app') 7 | -------------------------------------------------------------------------------- /frontend/src/pages/Authenticate.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | -------------------------------------------------------------------------------- /frontend/src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 62 | -------------------------------------------------------------------------------- /frontend/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createWebHistory, createRouter } from "vue-router"; 2 | import { isAuthenticated } from "./assets/ts/auth"; 3 | 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(), 7 | routes: [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | component: () => import('./pages/Home.vue'), 12 | async beforeEnter(to, from, next) { 13 | /** 14 | * Block visit to homepage and redirect to 15 | * login page if user is not authenticated 16 | */ 17 | if (await isAuthenticated()) next(); 18 | else next("/auth"); 19 | } 20 | }, 21 | { 22 | path: '/auth', 23 | name: 'Authenticate', 24 | component: () => import('./pages/Authenticate.vue'), 25 | } 26 | ], 27 | }) 28 | 29 | export default router -------------------------------------------------------------------------------- /frontend/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | .card { 60 | padding: 2em; 61 | } 62 | 63 | #app { 64 | max-width: 1280px; 65 | margin: 0 auto; 66 | padding: 2rem; 67 | text-align: center; 68 | } 69 | 70 | @media (prefers-color-scheme: light) { 71 | :root { 72 | color: #213547; 73 | background-color: #ffffff; 74 | } 75 | a:hover { 76 | color: #747bff; 77 | } 78 | button { 79 | background-color: #f9f9f9; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/**/*.{js,jsx,ts,tsx,vue}", 4 | "./pages/**/*.{js,jsx,ts,tsx,vue}", 5 | "./lib/**/*.{js,jsx,ts,tsx,vue}", 6 | ], 7 | theme: { 8 | screens: { 9 | xl: { max: "1600px" }, 10 | lg: { max: "1064px" }, 11 | md: { max: "920px" }, 12 | sm: { max: "750px" }, 13 | xs: { max: "550px" }, 14 | }, 15 | fontSize: { 16 | "7xl": "var(--fs-1)", 17 | "6xl": "var(--fs-2)", 18 | "5xl": "var(--fs-3)", 19 | "4xl": "var(--fs-4)", 20 | "3xl": "var(--fs-5)", 21 | "2xl": "var(--fs-6)", 22 | xl: "var(--fs-7)", 23 | lg: "var(--fs-8)", 24 | base: "var(--fs-9)", 25 | sm: ".875rem", 26 | xs: ".75rem", 27 | tiny: ".875rem", 28 | }, 29 | extend: {}, 30 | }, 31 | plugins: [], 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | server: { 8 | port: 3000 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /frontend/w.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "deno.enable": true, 9 | "deno.unstable": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gomarvin.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "go_version": 1.19, 4 | "name": "backend", 5 | "framework": "chi", 6 | "port": 4444, 7 | "api_prefix": "/api/v1", 8 | "config_version": 0.1, 9 | "db_type": "postgres", 10 | "include_sql": false, 11 | "include_fetch": true, 12 | "gomarvin_version": "v0.6.x" 13 | }, 14 | "modules": [ 15 | { 16 | "name": "User", 17 | "endpoints": [ 18 | { 19 | "url": "/user", 20 | "method": "GET", 21 | "response_type": "with_pagination", 22 | "controller_name": "GetUsers", 23 | "url_params": [], 24 | "body": [] 25 | }, 26 | { 27 | "url": "/user/id", 28 | "method": "GET", 29 | "response_type": "default", 30 | "controller_name": "GetUserByID", 31 | "url_params": [ 32 | { 33 | "field": "id", 34 | "type": "string" 35 | } 36 | ], 37 | "body": [] 38 | }, 39 | { 40 | "url": "/user/register", 41 | "method": "POST", 42 | "response_type": "default", 43 | "controller_name": "RegisterUser", 44 | "url_params": [], 45 | "body": [ 46 | { 47 | "validate": "required,min=5,max=250", 48 | "field": "username", 49 | "type": "string" 50 | }, 51 | { 52 | "validate": "required,min=10,max=250", 53 | "field": "password", 54 | "type": "string" 55 | }, 56 | { 57 | "validate": "required,email", 58 | "field": "email", 59 | "type": "string" 60 | } 61 | ] 62 | }, 63 | { 64 | "url": "/user", 65 | "method": "DELETE", 66 | "response_type": "default", 67 | "controller_name": "DeleteUser", 68 | "url_params": [], 69 | "body": [] 70 | }, 71 | { 72 | "url": "/user/username", 73 | "method": "GET", 74 | "response_type": "default", 75 | "controller_name": "GetUserByUsername", 76 | "url_params": [ 77 | { 78 | "field": "username", 79 | "type": "string" 80 | } 81 | ], 82 | "body": [] 83 | }, 84 | { 85 | "url": "/user/login", 86 | "method": "POST", 87 | "response_type": "default", 88 | "controller_name": "LoginUser", 89 | "url_params": [], 90 | "body": [ 91 | { 92 | "validate": "required,min=5,max=250", 93 | "field": "username", 94 | "type": "string" 95 | }, 96 | { 97 | "validate": "required,min=10,max=250", 98 | "field": "password", 99 | "type": "string" 100 | } 101 | ] 102 | }, 103 | { 104 | "url": "/user/details", 105 | "method": "GET", 106 | "response_type": "default", 107 | "controller_name": "GetUserDetailsWithAuth", 108 | "url_params": [], 109 | "body": [] 110 | } 111 | ] 112 | }, 113 | { 114 | "name": "Transaction", 115 | "endpoints": [ 116 | { 117 | "url": "/transaction", 118 | "method": "GET", 119 | "response_type": "default", 120 | "controller_name": "GetTransactionsForUser", 121 | "url_params": [], 122 | "body": [] 123 | }, 124 | { 125 | "url": "/transaction", 126 | "method": "POST", 127 | "response_type": "default", 128 | "controller_name": "CreateTransaction", 129 | "url_params": [], 130 | "body": [ 131 | { 132 | "validate": "required,uuid4", 133 | "field": "sender_id", 134 | "type": "string" 135 | }, 136 | { 137 | "validate": "required,uuid4", 138 | "field": "reciever_id", 139 | "type": "string" 140 | }, 141 | { 142 | "validate": "required", 143 | "field": "amount", 144 | "type": "int32" 145 | } 146 | ] 147 | }, 148 | { 149 | "url": "/transaction", 150 | "method": "GET", 151 | "response_type": "default", 152 | "controller_name": "GetTransactionByID", 153 | "url_params": [ 154 | { 155 | "field": "transaction_id", 156 | "type": "string" 157 | } 158 | ], 159 | "body": [] 160 | } 161 | ] 162 | }, 163 | { 164 | "name": "Auth", 165 | "endpoints": [ 166 | { 167 | "url": "/auth/token", 168 | "method": "POST", 169 | "response_type": "default", 170 | "controller_name": "TokenIsValid", 171 | "url_params": [], 172 | "body": [ 173 | { 174 | "validate": "required", 175 | "field": "access_token", 176 | "type": "string" 177 | } 178 | ] 179 | } 180 | ] 181 | } 182 | ] 183 | } -------------------------------------------------------------------------------- /seeder.ts: -------------------------------------------------------------------------------- 1 | import * as F from './backend/public/gomarvin.gen.ts' 2 | import { placeholderUser, placeholderUserSecond, PlaceholderUser } from './backend/public/placeholder.ts'; 3 | import { faker } from "https://cdn.skypack.dev/@faker-js/faker"; 4 | 5 | /** 6 | * Use the default API client to seed the db 7 | */ 8 | const client = F.defaultClient 9 | 10 | async function seedFakeUsers(AMOUNT: number) { 11 | for (let i = 0; i < AMOUNT; i++) { 12 | 13 | const username = faker.internet.userName() 14 | 15 | const res = await F.UserEndpoints.RegisterUser(client, { 16 | password: faker.internet.password(), 17 | username: faker.internet.userName(username), 18 | email: faker.internet.email(username), 19 | }); 20 | 21 | const data = await res.json(); 22 | console.log(data.status); 23 | } 24 | } 25 | 26 | 27 | /** 28 | * Create a predefine user so that you can log in with credentials 29 | */ 30 | async function CreatePlaceholderUser(user: PlaceholderUser) { 31 | const res = await F.UserEndpoints.RegisterUser(client, { 32 | password: user.password, 33 | username: user.username, 34 | email: user.email, 35 | }); 36 | const data = await res.json(); 37 | console.log(data); 38 | } 39 | 40 | /** 41 | * Log in the predefined user after registration 42 | */ 43 | async function LoginPlaceholderUser(user: PlaceholderUser) { 44 | const res = await F.UserEndpoints.LoginUser(client, { 45 | password: user.password, 46 | username: user.username, 47 | }); 48 | const data = await res.json(); 49 | console.log(data); 50 | // console.log(data.data.token.access_token); 51 | } 52 | 53 | async function createTransactionBetweenAccounts(body: F.CreateTransactionBody) { 54 | const res = await F.TransactionEndpoints.CreateTransaction(client, body) 55 | const data = await res.json(); 56 | console.log(data); 57 | } 58 | 59 | 60 | /** Create 100 fake users */ 61 | // seedFakeUsers(100) 62 | 63 | /** Create 2 predefined placeholder users */ 64 | // CreatePlaceholderUser(placeholderUser) 65 | // CreatePlaceholderUser(placeholderUserSecond) 66 | 67 | /** Test if Login Works */ 68 | LoginPlaceholderUser(placeholderUser) 69 | // LoginPlaceholderUser(placeholderUserSecond) 70 | 71 | const user_1 = "2d74b17c-2041-49fd-861b-72cd1bc7a903" 72 | const user_2 = "cc720122-04df-47c3-98ab-854bdedb9f8c" 73 | 74 | async function seedTransactionsBetweenUsers(sender: string, reciever: string, AMOUNT: number) { 75 | for (let i = 0; i < AMOUNT; i++) { 76 | const amount = Math.random() * (100 - 1) + 1; 77 | await createTransactionBetweenAccounts( 78 | { 79 | sender_id: sender, 80 | reciever_id: reciever, 81 | amount: Math.round(amount) // TODO : Fix this, only ints are correct currently 82 | } 83 | ) 84 | } 85 | } 86 | 87 | /** Seed fake transactions between both predefined users */ 88 | // seedTransactionsBetweenUsers(user_1, user_2, 50) 89 | // seedTransactionsBetweenUsers(user_2, user_1, 50) -------------------------------------------------------------------------------- /w.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "deno.enable": true, 9 | "deno.unstable": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------