├── backend
├── models
│ ├── users.go
│ ├── permissions.go
│ ├── filters.go
│ ├── tokens.go
│ ├── models.go
│ └── handlers.go
├── migrations
│ ├── 000002_create_users_table.down.sql
│ ├── 000003_create_data_table.down.sql
│ ├── 000004_create_tokens_table.down.sql
│ ├── 000005_add_permissions.down.sql
│ ├── 000004_create_tokens_table.up.sql
│ ├── 000003_create_data_table.up.sql
│ ├── 000002_create_users_table.up.sql
│ └── 000005_add_permissions.up.sql
├── cmd
│ ├── tmp
│ │ ├── main
│ │ └── build-errors.log
│ ├── healthcheck.go
│ ├── context.go
│ ├── routes.go
│ ├── tokens.go
│ ├── main.go
│ ├── errors.go
│ ├── utilities.go
│ ├── middleware.go
│ └── handlers.go
├── types
│ └── structures.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── mailer
│ ├── templates
│ │ └── user_welcome.tmpl
│ └── mailer.go
├── validator
│ └── validator.go
└── jsonlog
│ └── jsonlog.go
├── frontend
├── src
│ ├── vite-env.d.ts
│ ├── index.css
│ ├── main.tsx
│ ├── favicon.svg
│ ├── App.tsx
│ └── components
│ │ ├── Login.tsx
│ │ └── Register.tsx
├── .gitignore
├── postcss.config.js
├── vite.config.ts
├── tailwind.config.js
├── index.html
├── tsconfig.json
└── package.json
└── ReadME.md
/backend/models/users.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 |
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/backend/migrations/000002_create_users_table.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS users;
2 |
--------------------------------------------------------------------------------
/backend/migrations/000003_create_data_table.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS dataload;
2 |
--------------------------------------------------------------------------------
/backend/migrations/000004_create_tokens_table.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS tokens;
2 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/backend/cmd/tmp/main:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Melkeydev/React_Go_Boiler/HEAD/backend/cmd/tmp/main
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | /* ./src/index.css */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
--------------------------------------------------------------------------------
/backend/migrations/000005_add_permissions.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS permissions;
2 | DROP TABLE IF EXISTS users_permissions;
3 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()]
7 | })
8 |
--------------------------------------------------------------------------------
/backend/migrations/000004_create_tokens_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS tokens (
2 | hash bytea PRIMARY KEY,
3 | user_id bigint NOT NULL REFERENCES users ON DELETE CASCADE,
4 | expiry timestamp(0) with time zone NOT NULL,
5 | scope text NOT NULL
6 | )
7 |
--------------------------------------------------------------------------------
/backend/migrations/000003_create_data_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS dataload (
2 | ID BIGSERIAL PRIMARY KEY NOT NULL,
3 | DBDATAONE TEXT NOT NULL,
4 | DBDATATWO TEXT NOT NULL,
5 | DBDATATHREE TEXT NOT NULL,
6 | VERSION INTEGER NOT NULL DEFAULT 1
7 | );
8 |
9 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {},
6 | },
7 | variants: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | };
12 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | );
12 |
--------------------------------------------------------------------------------
/backend/migrations/000002_create_users_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS users (
2 | id bigserial PRIMARY KEY,
3 | created_at timestamp(0) with time zone NOT NULL DEFAULT NOW(),
4 | name text NOT NULL,
5 | email text UNIQUE NOT NULL,
6 | password_Hash bytea NOT NULL,
7 | activated bool NOT NULL,
8 | version integer NOT NULL DEFAULT 1
9 | );
10 |
11 |
--------------------------------------------------------------------------------
/backend/types/structures.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type Config struct {
4 | Port int
5 | Env string
6 | Db struct {
7 | Dsn string
8 | }
9 | Jwt struct {
10 | Secret string
11 | }
12 | Limiter struct {
13 | Rps float64
14 | Burst int
15 | Enabled bool
16 | }
17 | SMTP struct {
18 | Host string
19 | Port int
20 | Username string
21 | Password string
22 | Sender string
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/backend/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.1"
2 |
3 | services:
4 | db:
5 | container_name: "YOUR_CONTAINER_NAME"
6 | image: postgres:12.4-alpine
7 | volumes:
8 | - "./database/postgres-data:/var/lib/postgresql/data:rw"
9 | ports:
10 | - "5432:5432"
11 | environment:
12 | POSTGRES_DB: "YOUR_DB_NAME"
13 | POSTGRES_USER: "postgres"
14 | POSTGRES_PASSWORD: "postgres"
15 | restart: unless-stopped
16 |
--------------------------------------------------------------------------------
/backend/cmd/healthcheck.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
8 |
9 | env := envelope{
10 | "status": "available",
11 | "system_info": map[string]string{
12 | "environment": app.config.Env,
13 | },
14 | }
15 |
16 | err := app.writeJSON(w, http.StatusOK, env, nil)
17 | if err != nil {
18 | app.serverErrorResponse(w, r, err)
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/backend/go.mod:
--------------------------------------------------------------------------------
1 | module backend
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/go-mail/mail/v2 v2.3.0 // indirect
7 | github.com/julienschmidt/httprouter v1.3.0 // indirect
8 | github.com/lib/pq v1.10.3 // indirect
9 | github.com/pascaldekloe/jwt v1.10.0 // indirect
10 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
11 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
12 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/backend/migrations/000005_add_permissions.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS permissions (
2 | id bigserial PRIMARY KEY,
3 | code text NOT NULL
4 | );
5 |
6 | CREATE TABLE IF NOT EXISTS users_permissions (
7 | user_id bigint NOT NULL REFERENCES users ON DELETE CASCADE,
8 | permissions_id bigint NOT NULL REFERENCES permissions ON DELETE CASCADE,
9 | PRIMARY KEY (user_id, permissions_id)
10 | );
11 |
12 | INSERT INTO permissions (code)
13 | VALUES
14 | ('dataload:read'),
15 | ('dataload:write');
16 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": false,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["./src"]
20 | }
21 |
--------------------------------------------------------------------------------
/ReadME.md:
--------------------------------------------------------------------------------
1 | ## React/Go Boiler
2 |
3 | ### Setup
4 |
5 | in `main.go` configure new SMTP credentials if you want that functionality.
6 | if not remove.
7 |
8 | Modify the parameters in the `docker-compose.yml` file for what you want to name the database and connections
9 |
10 | Run `docker-compose up`
11 | This will create an instance of a postgres DN in docker
12 |
13 | If successful, the following message should be up:
14 |
15 | ```
16 | {"level":"INFO","time":"2022-01-09T03:11:42Z","message":"Loading server..."}
17 | {"level":"INFO","time":"2022-01-09T03:11:42Z","message":"Server running on port"}
18 | ```
19 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "tsc && vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "@types/react-router-dom": "^5.3.1",
11 | "axios": "^0.23.0",
12 | "react": "^17.0.0",
13 | "react-dom": "^17.0.0",
14 | "react-hook-form": "^7.17.4",
15 | "react-router-dom": "^5.3.0"
16 | },
17 | "devDependencies": {
18 | "@types/react": "^17.0.0",
19 | "@types/react-dom": "^17.0.0",
20 | "@vitejs/plugin-react": "^1.0.0",
21 | "autoprefixer": "^10.3.7",
22 | "postcss": "^8.3.9",
23 | "tailwindcss": "^2.2.16",
24 | "typescript": "^4.3.2",
25 | "vite": "^2.6.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/backend/cmd/context.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 |
4 | // This is going to hold our context for our User (if anonymous or activated)
5 | import (
6 | "context"
7 | "net/http"
8 | "backend/models"
9 | )
10 |
11 | type contextKey string
12 |
13 | // store the value of the token
14 | const userContextKey = contextKey("user")
15 |
16 | // setUserContext
17 | func (app *application) contextSetUser(r *http.Request, user *models.User) *http.Request {
18 | ctx := context.WithValue(r.Context(), userContextKey, user)
19 | return r.WithContext(ctx)
20 | }
21 |
22 | // TODO: need to investigate this
23 | //getUserContext
24 | func (app *application) contextGetUser(r *http.Request) *models.User {
25 | user, ok := r.Context().Value(userContextKey).(*models.User)
26 |
27 | if !ok {
28 | panic("missing user value in request context")
29 | }
30 |
31 | return user
32 | }
33 |
--------------------------------------------------------------------------------
/backend/cmd/tmp/build-errors.log:
--------------------------------------------------------------------------------
1 | exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2exit status 2
--------------------------------------------------------------------------------
/backend/models/permissions.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "context"
5 | "time"
6 | )
7 |
8 | type Permissions []string
9 |
10 | func (p Permissions) Include(code string) bool {
11 | for i := range p {
12 | if code == p[i] {
13 | return true
14 | }
15 | }
16 | return false
17 | }
18 |
19 | func (m *DBModel) GetAllForUser(userID int64) (Permissions, error) {
20 | query := `
21 | SELECT permissions.code
22 | FROM permissions
23 | INNER JOIN users_permissions ON users_permissions.permissions_id = permissions_id
24 | INNER JOIN users ON users_permissions.user_id = users.id
25 | WHERE users.id = $1
26 | `
27 |
28 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
29 | defer cancel()
30 |
31 | rows, err := m.DB.QueryContext(ctx, query, userID)
32 | if err != nil {
33 | return nil, err
34 | }
35 | defer rows.Close()
36 |
37 | var permissions Permissions
38 |
39 | // every permission needs to be appended to permissions
40 | for rows.Next() {
41 | var permission string
42 |
43 | err := rows.Scan(&permission)
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | permissions = append(permissions, permission)
49 | }
50 |
51 | if err = rows.Err(); err != nil {
52 | return nil, err
53 | }
54 |
55 | return permissions, nil
56 | }
57 |
--------------------------------------------------------------------------------
/backend/go.sum:
--------------------------------------------------------------------------------
1 | github.com/go-mail/mail/v2 v2.3.0 h1:wha99yf2v3cpUzD1V9ujP404Jbw2uEvs+rBJybkdYcw=
2 | github.com/go-mail/mail/v2 v2.3.0/go.mod h1:oE2UK8qebZAjjV1ZYUpY7FPnbi/kIU53l1dmqPRb4go=
3 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
4 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
5 | github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
6 | github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
7 | github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs=
8 | github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A=
9 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
10 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
11 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
12 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
13 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
14 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
15 |
--------------------------------------------------------------------------------
/backend/mailer/templates/user_welcome.tmpl:
--------------------------------------------------------------------------------
1 | {{define "subject"}}Welcome to Go-React-Boiler!{{end}}
2 |
3 | {{define "plainBody"}}
4 | Hi,
5 | Thanks for signing up for a Go-react-boiler account. We're excited to have you on board!
6 |
7 | For future reference, your user ID number is {{.userID}}.
8 |
9 | Please send a request to the `PUT /v1/users/activated` endpoint with the following JSON
10 | body to activate your account:
11 |
12 | {"token": "{{.activationToken}}"}
13 | Please note that this is a one-time use token and it will expire in 3 days.
14 | Thanks,
15 | The MelkeyDev Team
16 | {{end}}
17 |
18 | {{define "htmlBody"}}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Hi,
27 | Thanks for signing up for a Greenlight account. We're excited to have you on board!
28 | For future reference, your user ID number is {{.userID}}.
29 | Please send a request to the PUT /v1/users/activated endpoint with the
30 | following JSON body to activate your account:
31 |
32 | {"token": "{{.activationToken}}"}
33 |
34 | Please note that this is a one-time use token and it will expire in 3 days.
35 | Thanks,
36 | The Greenlight Team
37 |
38 |
39 | {{end}}
40 |
--------------------------------------------------------------------------------
/backend/cmd/routes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "github.com/julienschmidt/httprouter"
6 | )
7 |
8 | func (app *application) routes() http.Handler {
9 | router := httprouter.New()
10 | //Add our custom error handling
11 | router.NotFound = http.HandlerFunc(app.notFoundResponse)
12 | router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
13 |
14 | // we need to put the authetnication wrapper on each route
15 |
16 | router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
17 | router.HandlerFunc(http.MethodGet, "/v1/status", app.statusHandler)
18 | router.HandlerFunc(http.MethodGet, "/v1/data/:id", app.requireActivatedUser(app.getData))
19 | router.HandlerFunc(http.MethodGet, "/v1/data", app.requireActivatedUser(app.listAllDBData))
20 | router.HandlerFunc(http.MethodPost, "/v1/register", app.registerUser)
21 | //router.HandlerFunc(http.MethodPost, "/v1/login/", app.login)
22 | router.HandlerFunc(http.MethodPost, "/v1/post_data/", app.requireActivatedUser(app.insertPayload))
23 | router.HandlerFunc(http.MethodPatch, "/v1/data/:id", app.requireActivatedUser(app.updateDBData))
24 | router.HandlerFunc(http.MethodDelete, "/v1/data/:id", app.requireActivatedUser(app.deleteDBload))
25 | router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUser)
26 | router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)
27 |
28 | return app.recoverPanic(app.rateLimit(app.enableCORS(app.authenticate(router))))
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/backend/mailer/mailer.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import (
4 | "embed"
5 | "bytes"
6 | "github.com/go-mail/mail/v2"
7 | "html/template"
8 | "time"
9 | )
10 |
11 | //go:embed "templates"
12 | var templateFS embed.FS
13 |
14 | // mailer struct is going to contain our connection to the SMTP
15 | type Mailer struct {
16 | dialer *mail.Dialer
17 | sender string
18 | }
19 |
20 | func New(host string, port int, username, password, sender string) Mailer {
21 | // this is where we connect to our SMTP server (third party provider)
22 | dialer := mail.NewDialer(host, port, username, password)
23 | dialer.Timeout = 5 * time.Second
24 |
25 | return Mailer{
26 | dialer: dialer,
27 | sender: sender,
28 | }
29 | }
30 |
31 | func (m Mailer)Send(recipient, templateFile string, data interface{}) error {
32 | // parse the templateFS
33 | tmpl, err := template.New("email").ParseFS(templateFS, "templates/"+templateFile)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | subject := new(bytes.Buffer)
39 | err = tmpl.ExecuteTemplate(subject, "subject", data)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | plainBody := new(bytes.Buffer)
45 | err = tmpl.ExecuteTemplate(plainBody, "plainBody", data)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | htmlBody := new(bytes.Buffer)
51 | err = tmpl.ExecuteTemplate(htmlBody, "htmlBody", data)
52 | if err != nil {
53 | return err
54 | }
55 |
56 | msg := mail.NewMessage()
57 | msg.SetHeader("To", recipient)
58 | msg.SetHeader("From", m.sender)
59 | msg.SetHeader("Subject", subject.String())
60 | msg.SetBody("text/plain", plainBody.String())
61 | msg.AddAlternative("text/html", htmlBody.String())
62 |
63 | err = m.dialer.DialAndSend(msg)
64 |
65 | if err != nil {
66 | return err
67 | }
68 |
69 | return nil
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/backend/cmd/tokens.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "backend/models"
5 | "backend/validator"
6 | "errors"
7 | "net/http"
8 | "time"
9 | )
10 |
11 | func (app *application) createAuthenticationTokenHandler(w http.ResponseWriter, r *http.Request) {
12 | var input struct {
13 | Email string `json:"email"`
14 | Password string `json:"password"`
15 | }
16 |
17 | err := app.readJSON(w, r, &input)
18 |
19 | if err != nil {
20 | app.badRequestResponse(w, r, err)
21 | return
22 | }
23 |
24 | // Validate the email and password
25 | v := validator.New()
26 |
27 | models.ValidateEmail(v, input.Email)
28 | models.ValidatePasswordPlaintext(v, input.Password)
29 |
30 | if !v.Valid() {
31 | app.failedValidationResponse(w, r, v.Errors)
32 | return
33 | }
34 |
35 | user, err := app.models.DB.GetUserByEmail(input.Email)
36 | if err != nil {
37 | switch {
38 | case errors.Is(err, models.ErrRecordNotFound):
39 | app.invalidCredentialResponse(w, r)
40 | default:
41 | app.serverErrorResponse(w, r, err)
42 | }
43 | return
44 | }
45 |
46 | // Compare and match the hash passwords
47 | match, err := user.Password.Matches(input.Password)
48 | if err != nil {
49 | app.serverErrorResponse(w, r, err)
50 | return
51 | }
52 |
53 | // if passwords do not match
54 | if !match {
55 | app.invalidCredentialResponse(w, r)
56 | return
57 | }
58 |
59 | // Creates a new auth token and saves it
60 | token, err := app.models.DB.NewToken(user.ID, 24*time.Hour, models.ScopeAuthentication)
61 | if err != nil {
62 | app.serverErrorResponse(w, r, err)
63 | return
64 | }
65 |
66 | err = app.writeJSON(w, http.StatusCreated, envelope{"authentication_token":token}, nil)
67 | if err != nil {
68 | app.serverErrorResponse(w, r, err)
69 | }
70 | }
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/backend/validator/validator.go:
--------------------------------------------------------------------------------
1 | package validator
2 |
3 | import (
4 | "regexp"
5 | )
6 |
7 | // Declare a regex for checking the format of email addresses
8 | var (
9 | EmailRX = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
10 | )
11 |
12 | type Validator struct {
13 | Errors map[string]string
14 | }
15 |
16 | // Helper which creates a new Validator instance
17 | func New() *Validator {
18 | return &Validator{Errors: make(map[string]string)}
19 | }
20 |
21 | // Valid returns true if the errors map doesn't contain any entries.
22 | func (v *Validator) Valid() bool {
23 | return len(v.Errors) == 0
24 | }
25 |
26 | // AddError adds an error message to the map (so long as no entry already exists for the given key).
27 | func (v *Validator) AddError(key, message string) {
28 | if _, exists := v.Errors[key]; !exists {
29 | v.Errors[key] = message
30 | }
31 | }
32 |
33 | // Check adds an error message to the map only if a validation check is not 'ok'.
34 | func (v *Validator) Check(ok bool, key, message string) {
35 | if !ok {
36 | v.AddError(key, message)
37 | }
38 | }
39 |
40 | // In returns true if a specific value is in a list of strings.
41 | func In(value string, list ...string) bool {
42 | for i := range list {
43 | if value == list[i] {
44 | return true
45 | }
46 | }
47 | return false
48 | }
49 |
50 | // Matches returns true if a string value matches a specific regexp pattern.
51 | func Matches(value string, rx *regexp.Regexp) bool {
52 | return rx.MatchString(value)
53 | }
54 |
55 |
56 | // Unique returns true if all string values in a slice are unique.
57 | func Unique(values []string) bool {
58 | uniqueValues := make(map[string]bool)
59 | for _, value := range values {
60 | uniqueValues[value] = true
61 | }
62 | return len(values) == len(uniqueValues)
63 | }
64 |
--------------------------------------------------------------------------------
/backend/models/filters.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | // This file is responsible for all of our smart filtering for searches
4 |
5 | import (
6 | "math"
7 | "strings"
8 | "backend/validator"
9 | )
10 |
11 | type Filters struct {
12 | Page int
13 | PageSize int
14 | Sort string
15 | SortSafeList []string
16 | }
17 |
18 | type Metadata struct {
19 | CurrentPage int `json:"curent_page,omitempty"`
20 | PageSize int `json:"page_size,omitempty"`
21 | FirstPage int `json:"first_page,omitempty"`
22 | LastPage int `json:"last_page,omitempty"`
23 | TotalRecords int `json:"total_records,omitempty"`
24 | }
25 |
26 | func (f Filters) sortColumn() string {
27 | for _, safeValue := range f.SortSafeList {
28 | if f.Sort == safeValue {
29 | return strings.TrimPrefix(f.Sort, "-")
30 | }
31 | }
32 | panic("unfase sort parameter" + f.Sort)
33 | }
34 |
35 | func (f Filters) sortDirection() string {
36 | if strings.HasPrefix(f.Sort, "-") {
37 | return "DESC"
38 | }
39 | return "ASC"
40 | }
41 |
42 | func ValidateFilters(v *validator.Validator, f Filters) {
43 | // Filters for filters
44 | v.Check(f.Page > 0, "page", "page must be greater than 0")
45 | v.Check(f.Page < 100, "page", "page must be less than 100")
46 | v.Check(f.PageSize > 0, "page_size", "must be greater than 0")
47 | v.Check(f.PageSize <= 100, "page_size", "must be a maximum of 100")
48 | v.Check(validator.In(f.Sort, f.SortSafeList...), "sort", "invalid sort value")
49 | }
50 |
51 | func (f Filters) limit() int {
52 | return f.PageSize
53 | }
54 |
55 | func (f Filters) offset() int {
56 | return (f.Page - 1) * f.PageSize
57 | }
58 |
59 | func createMetadata(totalRecords, page, pagesize int) Metadata {
60 | if totalRecords == 0 {
61 | return Metadata{}
62 | }
63 |
64 | return Metadata{
65 | CurrentPage: page,
66 | PageSize: pagesize,
67 | FirstPage: 1,
68 | LastPage: int(math.Ceil(float64(totalRecords)/ float64(pagesize))),
69 | TotalRecords: totalRecords,
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/backend/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "backend/jsonlog"
5 | "backend/mailer"
6 | "backend/models"
7 | "backend/types"
8 | "flag"
9 | "fmt"
10 | _ "github.com/lib/pq"
11 | "net/http"
12 | "os"
13 | "time"
14 | )
15 |
16 | type application struct {
17 | config types.Config
18 | logger *jsonlog.Logger
19 | models models.Models
20 | mailer mailer.Mailer
21 | }
22 |
23 | func main() {
24 | var cfg types.Config
25 | var port = 4000
26 | logger := jsonlog.New(os.Stdout, jsonlog.LevelInfo)
27 |
28 | logger.PrintInfo("Loading server...", nil)
29 |
30 | flag.IntVar(&cfg.Port, "port", port, "server for port to listen")
31 | flag.StringVar(&cfg.Env, "env", "development", "app environment")
32 | // TODO: Add to note to the readme
33 | // CHANGE DSN to your database setting
34 | flag.StringVar(&cfg.Db.Dsn, "dsn", "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable", "Database connection string")
35 | flag.StringVar(&cfg.Jwt.Secret, "jwt-secret", "default-secret", "secret-key")
36 |
37 | // create flags
38 | flag.StringVar(&cfg.SMTP.Host, "smtp-host", "smtp.mailtrap.io", "SMTP host")
39 | flag.IntVar(&cfg.SMTP.Port, "smtp-port", 587, "SMTP Port")
40 | // I need to actually put in my credentials
41 | flag.StringVar(&cfg.SMTP.Username, "smtp-username", "foo", "SMTP host")
42 | flag.StringVar(&cfg.SMTP.Password, "smtp-password", "foo", "SMTP host")
43 | flag.StringVar(&cfg.SMTP.Sender, "smtp-sender", "Thundercock ", "SMTP host")
44 |
45 | flag.Parse()
46 |
47 | db, err := connectDB(cfg)
48 | if err != nil {
49 | logger.PrintFatal(err, nil)
50 | }
51 |
52 | defer db.Close()
53 |
54 | app := &application{
55 | config: cfg,
56 | logger: logger,
57 | models: models.NewModels(db),
58 | mailer: mailer.New(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.Username, cfg.SMTP.Password, cfg.SMTP.Sender),
59 | }
60 |
61 | // Declare Server config
62 | server := http.Server{
63 | Addr: fmt.Sprintf(":%d", cfg.Port),
64 | Handler: app.routes(),
65 | IdleTimeout: time.Minute,
66 | ReadTimeout: 10 * time.Second,
67 | WriteTimeout: 30 * time.Second,
68 | }
69 |
70 | // Run the server
71 | logger.PrintInfo("Server running on port", nil)
72 | err = server.ListenAndServe()
73 | if err != nil {
74 | logger.PrintFatal(err, nil)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/backend/models/tokens.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "context"
5 | "crypto/rand"
6 | "crypto/sha256"
7 | "encoding/base32"
8 | "time"
9 | //"database/sql"
10 | "backend/validator"
11 | )
12 |
13 | const (
14 | ScopeActivation = "activation"
15 | ScopeAuthentication = "authentication"
16 | )
17 |
18 | type Token struct {
19 | Plaintext string `json:"token"`
20 | Hash []byte `json:"-"`
21 | UserID int64 `json:"-"`
22 | Expiry time.Time `json:"expiry"`
23 | Scope string `json:"-"`
24 | }
25 |
26 | // Token validation check
27 | func ValidateTokenPlaintext(v *validator.Validator, tokenPlaintext string) {
28 | v.Check(tokenPlaintext != "", "token", "must be provided")
29 | v.Check(len(tokenPlaintext) == 26, "token", "must be 26 characters long")
30 | }
31 |
32 | func (m *DBModel) NewToken(userID int64, ttl time.Duration, scope string) (*Token, error) {
33 | token, err := generateToken(userID, ttl, scope)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | err = m.InsertToken(token)
39 | return token, err
40 | }
41 |
42 | func (m *DBModel) InsertToken(token *Token) error {
43 | query := `INSERT INTO tokens (hash, user_id, expiry, scope) VALUES ($1, $2, $3, $4)`
44 |
45 | args := []interface{}{token.Hash, token.UserID, token.Expiry, token.Scope}
46 |
47 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
48 | defer cancel()
49 |
50 | _, err := m.DB.ExecContext(ctx, query, args...)
51 | return err
52 | }
53 |
54 | func (m *DBModel) DeleteAlForUser(scope string, userID int64) error {
55 | query := `DELETE FROM tokens WHERE scope = $1 AND user_id = $2`
56 |
57 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
58 | defer cancel()
59 |
60 | _, err := m.DB.ExecContext(ctx, query, scope, userID)
61 | return err
62 | }
63 |
64 | func generateToken(userID int64, ttl time.Duration, scope string) (*Token, error) {
65 | token := &Token{
66 | UserID: userID,
67 | Expiry: time.Now().Add(ttl),
68 | Scope: scope,
69 | }
70 |
71 | randomBytes := make([]byte, 16)
72 |
73 | _, err := rand.Read(randomBytes)
74 | if err != nil {
75 | return nil, err
76 | }
77 |
78 | // Encode the byte slice to a 32 bit encoded string
79 | token.Plaintext = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes)
80 |
81 | // Generate a 256 hash of the plaintext string
82 | hash := sha256.Sum256([]byte(token.Plaintext))
83 | token.Hash = hash[:]
84 |
85 | return token, nil
86 | }
87 |
--------------------------------------------------------------------------------
/backend/jsonlog/jsonlog.go:
--------------------------------------------------------------------------------
1 | package jsonlog
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "os"
7 | "runtime/debug"
8 | "sync"
9 | "time"
10 | )
11 |
12 | type Level int8
13 |
14 | // Declare our log levels
15 | const (
16 | LevelInfo Level = iota
17 | LevelError
18 | LevelFatal
19 | LevelOff
20 | )
21 |
22 | func (l Level) String() string {
23 | switch l {
24 | case LevelInfo:
25 | return "INFO"
26 | case LevelError:
27 | return "ERROR"
28 | case LevelFatal:
29 | return "FATAL"
30 | default:
31 | return ""
32 | }
33 | }
34 |
35 | type Logger struct {
36 | out io.Writer
37 | minLevel Level
38 | mu sync.Mutex
39 | }
40 |
41 | func New(out io.Writer, minLevel Level) *Logger {
42 | return &Logger{
43 | out: out,
44 | minLevel: minLevel,
45 | }
46 | }
47 |
48 | func (l *Logger) print(level Level, message string, properties map[string]string) (int, error) {
49 | if level < l.minLevel {
50 | return 0, nil
51 | }
52 |
53 | aux := struct {
54 | Level string `json:"level"`
55 | Time string `json:"time"`
56 | Message string `json:"message"`
57 | Properties map[string]string `json:"properties,omitempty"`
58 | Trace string `json:"trace,omitempty"`
59 | }{
60 | Level: level.String(),
61 | Time: time.Now().UTC().Format(time.RFC3339),
62 | Message: message,
63 | Properties: properties,
64 | }
65 |
66 | // include a strack trace for error and fatal levels
67 | if level >= LevelError {
68 | aux.Trace = string(debug.Stack())
69 | }
70 |
71 | var line []byte
72 | line, err := json.Marshal(aux)
73 | if err != nil {
74 | line = []byte(LevelError.String() + ": unable to marshal log message:" + err.Error())
75 | }
76 |
77 | l.mu.Lock()
78 | defer l.mu.Unlock()
79 |
80 | return l.out.Write(append(line, '\n'))
81 | }
82 |
83 | func (l *Logger) Write(message []byte) (n int, err error) {
84 | return l.print(LevelError, string(message), nil)
85 | }
86 |
87 | func (l *Logger) PrintInfo(message string, properties map[string]string) {
88 | l.print(LevelInfo, message, nil)
89 | }
90 |
91 | func (l *Logger) PrintError(err error, properties map[string]string) {
92 | l.print(LevelError, err.Error(), properties)
93 | }
94 |
95 | func (l *Logger) PrintFatal(err error, properties map[string]string) {
96 | l.print(LevelFatal, err.Error(), properties)
97 | os.Exit(1)
98 | }
99 |
100 |
101 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { Register } from "./components/Register";
3 | import { Login } from "./components/Login";
4 |
5 | import {
6 | BrowserRouter as Router,
7 | Switch,
8 | Route,
9 | Link,
10 | useParams,
11 | } from "react-router-dom";
12 |
13 | function App() {
14 | const [jwt, setJwt] = useState("");
15 |
16 | const handleJWTChange = (jwtToken: string) => {
17 | setJwt(jwtToken);
18 | };
19 |
20 | const logout = () => {
21 | setJwt("");
22 | window.localStorage.removeItem("jwt");
23 | };
24 |
25 | useEffect(() => {
26 | let t = window.localStorage.getItem("jwt");
27 |
28 | if (t) {
29 | if (jwt === "") {
30 | setJwt(JSON.parse(t));
31 | }
32 | }
33 | }, []);
34 |
35 | let loginLink;
36 |
37 | if (jwt === "") {
38 | loginLink = Login;
39 | } else {
40 | loginLink = (
41 |
42 | Logout
43 |
44 | );
45 | }
46 |
47 | return (
48 |
49 |
50 |
83 |
84 |
85 |
86 | }
89 | >
90 |
91 |
92 | );
93 | }
94 |
95 | export default App;
96 |
--------------------------------------------------------------------------------
/frontend/src/components/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback, useRef } from "react";
2 | import { useForm } from "react-hook-form";
3 | import axios from "axios";
4 |
5 | export interface iLogin {
6 | username: string;
7 | password: string;
8 | }
9 |
10 | export const Login = ({ jwtProps }: { jwtProps: any }) => {
11 | const {
12 | register,
13 | handleSubmit,
14 | formState: { errors },
15 | reset,
16 | getValues,
17 | } = useForm();
18 | const [loginState, setLoginState] = useState();
19 |
20 | const onSubmit = async (data: any) => {
21 | const { username, password } = data;
22 |
23 | const body = JSON.stringify({
24 | username,
25 | password,
26 | });
27 |
28 | const response = await axios.post("http://localhost:4000/v1/login", body);
29 | //TODO: Handle if nothing returns
30 | console.log(response.data.message);
31 | jwtProps(response.data.message);
32 | window.localStorage.setItem("jwt", JSON.stringify(response.data.response));
33 | };
34 |
35 | const handleChange = useCallback((e) => {
36 | const { id, value } = e.target;
37 |
38 | setLoginState((state: any) => ({
39 | ...state,
40 | [id]: value,
41 | }));
42 | }, []);
43 |
44 | return (
45 |
92 | );
93 | };
94 |
--------------------------------------------------------------------------------
/backend/cmd/errors.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | )
7 |
8 | // Generic helper for logging an error message
9 | func (app *application) logError(r *http.Request, err error) {
10 | app.logger.PrintError(err, map[string]string{
11 | "request_method": r.Method,
12 | "request_url": r.URL.String(),
13 | })
14 | }
15 |
16 | // Helper for sending json-formatted error messages to clients w/status code
17 | func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) {
18 | env := envelope{"error": message}
19 |
20 | err := app.writeJSON(w, status, env, nil)
21 | if err != nil {
22 | app.logError(r, err)
23 | w.WriteHeader(500)
24 | }
25 | }
26 |
27 | // Helper when our app encounters an unexpected problem at runtime. Send 500
28 | func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
29 | app.logError(r, err)
30 | message := "The server encountered a proble and could not process your request"
31 | app.errorResponse(w, r, http.StatusInternalServerError, message)
32 | }
33 |
34 | // Helper when we encounter a 404
35 | func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) {
36 | message := "The requested resource could not be found"
37 | app.errorResponse(w, r, http.StatusNotFound, message)
38 | }
39 |
40 | // Helper when request is made with incorrect method
41 | func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
42 | message := fmt.Sprintf("the %s method is not supported for this resource", r.Method)
43 | app.errorResponse(w, r, http.StatusMethodNotAllowed, message)
44 | }
45 |
46 | // Helper for handling bad requests
47 | func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
48 | app.errorResponse(w, r, http.StatusBadRequest, err.Error())
49 | }
50 |
51 | // Helper for failed JSON validation responses
52 | func (app *application) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
53 | app.errorResponse(w, r, http.StatusUnprocessableEntity, errors)
54 | }
55 |
56 | // Data race conditon for editing data
57 | func (app *application) editConflictResponse(w http.ResponseWriter, r *http.Request) {
58 | message := "Unable to update the record due to an edit conflict"
59 | app.errorResponse(w, r, http.StatusConflict, message)
60 | }
61 |
62 | func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http.Request) {
63 | message := "rate limit exceeded"
64 | app.errorResponse(w, r, http.StatusTooManyRequests, message)
65 | }
66 |
67 | func (app *application) invalidCredentialResponse(w http.ResponseWriter, r *http.Request) {
68 | message := "invalid authentication credentials"
69 | app.errorResponse(w, r, http.StatusUnauthorized, message)
70 | }
71 |
72 | func (app *application) invalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) {
73 | message := "invalid authentication token response"
74 | app.errorResponse(w, r, http.StatusUnauthorized, message)
75 | }
76 |
77 | // This is when we make a request as non permissioned user
78 | func (app *application) authenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
79 | message := "you must be authorized to access this route"
80 | app.errorResponse(w, r, http.StatusUnauthorized, message)
81 | }
82 |
83 | // This is when we make a request as a non activated user
84 | func (app *application) inactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
85 | message := "your account must be activated to access this route"
86 | app.errorResponse(w, r, http.StatusUnauthorized, message)
87 | }
88 |
89 | func (app *application) notPermittedResponse(w http.ResponseWriter, r *http.Request) {
90 | message := "You do not have the correct permissions to access this route"
91 | app.errorResponse(w, r, http.StatusForbidden, message)
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/backend/models/models.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "backend/validator"
5 | "database/sql"
6 | "errors"
7 | "time"
8 | "golang.org/x/crypto/bcrypt"
9 | )
10 |
11 | type Models struct {
12 | DB DBModel
13 | }
14 |
15 | type DBModel struct {
16 | DB *sql.DB
17 | }
18 |
19 | var (
20 | ErrRecordNotFound = errors.New("record not found")
21 | ErrEditConflict = errors.New("edit conflict")
22 | ErrDuplicateEmail = errors.New("duplicate email")
23 | )
24 |
25 | func NewModels(db *sql.DB) Models {
26 | return Models{
27 | DB: DBModel{DB: db},
28 | }
29 | }
30 |
31 | // A generic user structure
32 | type User struct {
33 | ID int64 `json:"id"`
34 | CreatedAt time.Time `json:"created_at"`
35 | Name string `json:"name"`
36 | Email string `json:"email"`
37 | Password password `json:"-"`
38 | Activated bool `json:"activated"`
39 | Version int `json:"-"`
40 | }
41 |
42 | type password struct {
43 | plaintext *string
44 | hash []byte
45 | }
46 |
47 | var AnonymousUser = &User{}
48 |
49 | func (u *User) IsAnonymous() bool {
50 | return u == AnonymousUser
51 | }
52 |
53 | func (p *password) Set(plaintextPassword string) error {
54 | hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
55 | if err != nil {
56 | return err
57 | }
58 |
59 | p.plaintext = &plaintextPassword
60 | p.hash = hash
61 |
62 | return nil
63 | }
64 |
65 | func (p *password) Matches(plaintextPassword string) (bool, error) {
66 | err := bcrypt.CompareHashAndPassword(p.hash, []byte(plaintextPassword))
67 | if err != nil {
68 | switch {
69 | case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
70 | return false, nil
71 | default:
72 | return false, err
73 | }
74 | }
75 | return true, nil
76 | }
77 |
78 | func ValidateEmail(v *validator.Validator, email string) {
79 | v.Check(email != "", "email", "must be provided")
80 | v.Check(validator.Matches(email, validator.EmailRX), "email", "must be a valid email address")
81 | }
82 |
83 | func ValidatePasswordPlaintext(v *validator.Validator, password string) {
84 | v.Check(password != "", "password", "must be provided")
85 | v.Check(len(password) >= 8, "password", "password must be atleast 8 chars long")
86 | v.Check(len(password) <= 72, "password", "password must not be more than 72 chars long")
87 | }
88 |
89 | func ValidateUser(v *validator.Validator, user *User) {
90 | v.Check(user.Name != "", "name", "must be provided")
91 | v.Check(len(user.Name) <= 72, "name", "must not be longer than 72")
92 |
93 | ValidateEmail(v, user.Email)
94 |
95 | if user.Password.plaintext != nil {
96 | ValidatePasswordPlaintext(v, *user.Password.plaintext)
97 | }
98 |
99 | if user.Password.hash == nil {
100 | panic("missing password for hash use")
101 | }
102 | }
103 |
104 | // A generic payload structure got API calls
105 | type Payload struct {
106 | SampleOne string `json:"sample_one"`
107 | SampleTwo string `json:"sample_two"`
108 | SampleThree string `json:"sample_three"`
109 | }
110 |
111 | type DBLoad struct {
112 | DBDataOne string `json:db_data_one`
113 | DBDataTwo string `json:db_data_two`
114 | DBDataThree string `json:db_data_three`
115 | ID int64 `json:id`
116 | Version int32 `json:version`
117 | }
118 |
119 | func ValidateDBLoad(v *validator.Validator, dbload *DBLoad) {
120 | v.Check(dbload.DBDataOne != "", "dbdataone", "data for field one must be provided")
121 | v.Check(len(dbload.DBDataOne) <= 500, "dbdataone", "data must be less than 500 chars")
122 | v.Check(dbload.DBDataTwo != "", "dbdatatwo", "data for field two must be provided")
123 | v.Check(len(dbload.DBDataTwo) <= 500, "dbdatatwo", "data must be less than 500 chars")
124 | v.Check(dbload.DBDataThree != "", "dbdatathree", "data for field three must be provided")
125 | v.Check(len(dbload.DBDataThree) <= 500, "dbdatathree", "data must be less than 500 chars")
126 | }
127 |
--------------------------------------------------------------------------------
/frontend/src/components/Register.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback, useRef } from "react";
2 | import { useForm } from "react-hook-form";
3 | import axios from "axios";
4 |
5 | export interface iRegister {
6 | username: string;
7 | password: string;
8 | }
9 |
10 | export const Register = () => {
11 | const {
12 | register,
13 | handleSubmit,
14 | formState: { errors },
15 | reset,
16 | getValues,
17 | } = useForm();
18 | const [registerState, setRegisterState] = useState();
19 |
20 | const onSubmit = async (data: any) => {
21 | console.log(data);
22 |
23 | const { username, password } = data;
24 |
25 | const body = JSON.stringify({
26 | username,
27 | password,
28 | });
29 |
30 | const response = await axios.post(
31 | "http://localhost:4000/v1/register",
32 | body
33 | );
34 |
35 | console.log(response.data);
36 | };
37 |
38 | const handleChange = useCallback((e) => {
39 | const { id, value } = e.target;
40 |
41 | setRegisterState((state: any) => ({
42 | ...state,
43 | [id]: value,
44 | }));
45 | }, []);
46 |
47 | return (
48 |
114 | );
115 | };
116 |
--------------------------------------------------------------------------------
/backend/cmd/utilities.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "fmt"
6 | "log"
7 | "time"
8 | "context"
9 | "strings"
10 | "errors"
11 | "net/url"
12 | "net/http"
13 | "strconv"
14 | "encoding/json"
15 | "database/sql"
16 | "backend/types"
17 | "backend/validator"
18 | "github.com/julienschmidt/httprouter"
19 | )
20 |
21 | //TODO: Add to the Readme
22 | // This will just hold useful functions that server a specific purpose for app handling
23 |
24 | type envelope map[string]interface{}
25 |
26 | func connectDB(cfg types.Config) (*sql.DB, error) {
27 | db, err := sql.Open("postgres", cfg.Db.Dsn)
28 | if err != nil {
29 | log.Fatal("unable to connect to the database")
30 | return nil, err
31 | }
32 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
33 | defer cancel()
34 |
35 | err = db.PingContext(ctx)
36 | if err != nil {
37 | return nil, err
38 | }
39 | return db, nil
40 | }
41 |
42 |
43 | func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error {
44 | js, err := json.MarshalIndent(data, "", "\t")
45 | if err != nil {
46 | return err
47 | }
48 |
49 | js = append(js, '\n')
50 |
51 | for k, v := range headers {
52 | w.Header()[k] = v
53 | }
54 |
55 | w.Header().Set("Content-Type", "application/json")
56 | w.WriteHeader(status)
57 | w.Write(js)
58 | return nil
59 | }
60 |
61 | func (app *application) readIDParam(r *http.Request) (int64, error) {
62 | params := httprouter.ParamsFromContext(r.Context())
63 |
64 | id, err := strconv.ParseInt(params.ByName("id"),10,64)
65 | if err != nil {
66 | return 0, errors.New("invalid id parameter")
67 | }
68 | return id, nil
69 | }
70 |
71 | func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
72 |
73 | // Adds a maximum byte size to the load request
74 | maxBytes := 1_048_576
75 | r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
76 |
77 | // Decoder for our json payload
78 | dec := json.NewDecoder(r.Body)
79 | dec.DisallowUnknownFields()
80 | err := dec.Decode(dst)
81 |
82 | if err != nil {
83 | var syntaxError *json.SyntaxError
84 | var unmarshallTypeError *json.UnmarshalTypeError
85 | var invalidMarshallError *json.InvalidUnmarshalError
86 |
87 | switch {
88 | case errors.As(err, &syntaxError):
89 | return fmt.Errorf("body contains badly formed JSON characters %d", syntaxError.Offset)
90 |
91 | case errors.Is(err, io.ErrUnexpectedEOF):
92 | return errors.New("Badly formatted JSON in body request")
93 |
94 | case errors.As(err, &unmarshallTypeError):
95 | if unmarshallTypeError.Field != "" {
96 | return fmt.Errorf("body contains incorrect JSON type for field %d", unmarshallTypeError.Field)
97 | }
98 | return fmt.Errorf("Body contains incorrect JSON")
99 |
100 | // if there is something in our body thats empty
101 | case errors.Is(err, io.EOF):
102 | return errors.New("body must not be empty")
103 |
104 | case strings.HasPrefix(err.Error(), "json: unknown field"):
105 | // this handles when the body has an incorrect key
106 | fieldName := strings.TrimPrefix(err.Error(), "json: unknown field")
107 | return fmt.Errorf("Body contains unknown key %s", fieldName)
108 |
109 | case err.Error() == "http: request body too large":
110 | return fmt.Errorf("body must not be larger than max size")
111 |
112 | case errors.As(err, &invalidMarshallError):
113 | panic(err)
114 |
115 | default:
116 | return err
117 | }
118 | }
119 |
120 | err = dec.Decode(&struct{}{})
121 | if err != io.EOF {
122 | return errors.New("body must contan only valid JSON values")
123 | }
124 |
125 | return nil
126 | }
127 |
128 | func (app *application) readString(qs url.Values, key string, defaultValue string) string {
129 | s := qs.Get(key)
130 |
131 | if s == "" {
132 | return defaultValue
133 | }
134 |
135 | return s
136 | }
137 |
138 | func (app *application) readCSV(qs url.Values, key string, defaultValue []string) []string{
139 | csv := qs.Get(key)
140 |
141 | if csv == "" {
142 | return defaultValue
143 | }
144 |
145 | return strings.Split(csv, ",")
146 | }
147 |
148 | func (app *application) readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
149 | s := qs.Get(key)
150 |
151 | if s == "" {
152 | return defaultValue
153 | }
154 |
155 | i, err := strconv.Atoi(s)
156 | if err != nil {
157 | v.AddError(key, "must be an integer value")
158 | }
159 |
160 | return i
161 | }
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
--------------------------------------------------------------------------------
/backend/cmd/middleware.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "backend/models"
5 | "backend/validator"
6 | "errors"
7 | "fmt"
8 | "golang.org/x/time/rate"
9 | "net"
10 | "net/http"
11 | "strings"
12 | "sync"
13 | "time"
14 | )
15 |
16 | // We did not fully test this
17 | func (app *application) enableCORS(next http.Handler) http.Handler {
18 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19 | w.Header().Set("Access-Control-Allow-Origin", "*")
20 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization")
21 | next.ServeHTTP(w, r)
22 | })
23 | }
24 |
25 | // route and handle panics better
26 | func (app *application) recoverPanic(next http.Handler) http.Handler {
27 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28 | defer func() {
29 | if err := recover(); err != nil {
30 | w.Header().Set("Connection", "close")
31 | app.serverErrorResponse(w, r, fmt.Errorf("%s", err))
32 | }
33 | }()
34 | next.ServeHTTP(w, r)
35 | })
36 |
37 | }
38 |
39 | func (app *application) rateLimit(next http.Handler) http.Handler {
40 | type client struct {
41 | limiter *rate.Limiter
42 | lastSeen time.Time
43 | }
44 |
45 | var (
46 | mu sync.Mutex
47 | clients = make(map[string]*client)
48 | )
49 |
50 | go func() {
51 | for {
52 | time.Sleep(time.Minute)
53 | mu.Lock()
54 |
55 | for ip, client := range clients {
56 | if time.Since(client.lastSeen) > 3*time.Second {
57 | delete(clients, ip)
58 | }
59 | }
60 | mu.Unlock()
61 | }
62 | }()
63 |
64 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65 | if app.config.Limiter.Enabled {
66 | // get the ip addy from each request
67 | ip, _, err := net.SplitHostPort(r.RemoteAddr)
68 | if err != nil {
69 | app.serverErrorResponse(w, r, err)
70 | return
71 | }
72 |
73 | mu.Lock()
74 |
75 | if _, found := clients[ip]; !found {
76 | clients[ip] = &client{
77 | limiter: rate.NewLimiter(rate.Limit(app.config.Limiter.Rps), app.config.Limiter.Burst),
78 | }
79 | }
80 |
81 | // Every new ip that gets added to our clients slice gets a time stamp
82 | clients[ip].lastSeen = time.Now()
83 |
84 | if !clients[ip].limiter.Allow() {
85 | mu.Unlock()
86 | app.rateLimitExceededResponse(w, r)
87 | return
88 | }
89 |
90 | mu.Unlock()
91 | }
92 |
93 | next.ServeHTTP(w, r)
94 | })
95 | }
96 |
97 | // Create a authenticate middleware
98 | func (app *application) authenticate(next http.Handler) http.Handler {
99 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
100 | // Adding ther VARY and AUTHORIZATION
101 | // This indicates to any cache that the request may vary
102 | w.Header().Add("Vary", "Authorization")
103 |
104 | // Retrieve the value of the Authorization header
105 | authorizationHeader := r.Header.Get("Authorization")
106 |
107 | // The pointer reference might be questionable
108 | // IF there is no authorizationHeader we will set the context
109 | // this will hold an AnonymousUser - gifting bare minimum
110 | if authorizationHeader == "" {
111 | r = app.contextSetUser(r, models.AnonymousUser)
112 | next.ServeHTTP(w, r)
113 | return
114 | }
115 |
116 | headerParts := strings.Split(authorizationHeader, " ")
117 | if len(headerParts) != 2 || headerParts[0] != "Bearer" {
118 | app.invalidCredentialResponse(w, r)
119 | return
120 | }
121 |
122 | token := headerParts[1]
123 | v := validator.New()
124 |
125 | // We need to validate the token to make sure it is correct format
126 | if models.ValidateTokenPlaintext(v, token); !v.Valid() {
127 | app.invalidCredentialResponse(w, r)
128 | return
129 | }
130 |
131 | // then we need to get the user
132 | user, err := app.models.DB.GetForToken(models.ScopeAuthentication, token)
133 | if err != nil {
134 | switch {
135 | case errors.Is(err, models.ErrRecordNotFound):
136 | app.invalidAuthenticationTokenResponse(w, r)
137 | default:
138 | app.serverErrorResponse(w, r, err)
139 | }
140 | return
141 | }
142 |
143 | // Set the user here pointer ref is questionable
144 | r = app.contextSetUser(r, user)
145 |
146 | // Because this is a MW wrapperm we need to pass to the next http handler
147 | next.ServeHTTP(w, r)
148 | })
149 | }
150 |
151 | // We need to split our auth to handle activated routes and authenticated routes
152 | func(app *application) requireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
153 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
154 | user := app.contextGetUser(r)
155 |
156 | // If this is true; You arent authorized
157 | if user.IsAnonymous() {
158 | app.authenticationRequiredResponse(w, r)
159 | return
160 | }
161 |
162 | next.ServeHTTP(w, r)
163 | })
164 | }
165 |
166 | // We need this to wrap and call our requireAuthenticatedUser MW
167 | func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
168 | fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
169 | user := app.contextGetUser(r)
170 |
171 | if !user.Activated {
172 | app.inactiveAccountResponse(w, r)
173 | return
174 | }
175 |
176 | next.ServeHTTP(w,r )
177 | })
178 |
179 | return app.requireAuthenticatedUser(fn)
180 | }
181 |
--------------------------------------------------------------------------------
/backend/models/handlers.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "errors"
7 | "fmt"
8 | "time"
9 | "crypto/sha256"
10 | )
11 |
12 | func (m *DBModel) Insert(user *User) error {
13 | query := `INSERT INTO users (name, email, password_hash, activated) VALUES ($1, $2, $3, $4) RETURNING id, created_at, version`
14 |
15 | args := []interface{}{user.Name, user.Email, user.Password.hash, user.Activated}
16 |
17 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
18 | defer cancel()
19 |
20 | err := m.DB.QueryRowContext(ctx, query, args...).Scan(&user.ID, &user.CreatedAt, &user.Version)
21 | if err != nil {
22 | switch {
23 | case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
24 | return ErrDuplicateEmail
25 | default:
26 | return err
27 | }
28 | }
29 | return nil
30 | }
31 |
32 |
33 | func (m *DBModel) GetData(id int64) (*DBLoad, error) {
34 | if id < 1 {
35 | return nil, ErrRecordNotFound
36 | }
37 |
38 | query := `SELECT dbdataone, dbdatatwo, dbdatathree, version from dataload where id = $1`
39 |
40 | var load DBLoad
41 |
42 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
43 | defer cancel()
44 |
45 | err := m.DB.QueryRowContext(ctx, query, id).Scan(
46 | &load.DBDataOne,
47 | &load.DBDataTwo,
48 | &load.DBDataThree,
49 | &load.Version,
50 | )
51 |
52 | if err != nil {
53 | switch {
54 | case errors.Is(err, sql.ErrNoRows):
55 | return nil, ErrRecordNotFound
56 | default:
57 | return nil, err
58 | }
59 | }
60 |
61 | return &load, nil
62 | }
63 |
64 | func (m *DBModel) InsertDBLoad(load *DBLoad) error {
65 | query := `insert into dataload(dbdataone, dbdatatwo, dbdatathree) VALUES($1, $2, $3) returning version`
66 |
67 | args := []interface{}{load.DBDataOne, load.DBDataTwo, load.DBDataThree}
68 |
69 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
70 | defer cancel()
71 |
72 | return m.DB.QueryRowContext(ctx, query, args...).Scan(&load.DBDataOne)
73 | }
74 |
75 | func (m *DBModel) Delete(id int64) error {
76 | if id < 1 {
77 | return ErrRecordNotFound
78 | }
79 |
80 | query := `DELETE FROM dataload where id = $1`
81 |
82 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
83 | defer cancel()
84 |
85 | results, err := m.DB.ExecContext(ctx, query, id)
86 | if err != nil {
87 | return err
88 | }
89 |
90 | rowsAffected, err := results.RowsAffected()
91 | if err != nil {
92 | return err
93 | }
94 |
95 | if rowsAffected == 0 {
96 | return ErrRecordNotFound
97 | }
98 |
99 | return nil
100 | }
101 |
102 | func (m *DBModel) UpdateUser(user *User) error {
103 | query := `UPDATE users SET name = $1, email = $2, password_hash = $3, activated = $4, version = version + 1 WHERE id = $5 AND version = $6 RETURNING version`
104 |
105 | args := []interface{}{
106 | user.Name,
107 | user.Email,
108 | user.Password.hash,
109 | user.Activated,
110 | user.ID,
111 | user.Version,
112 | }
113 |
114 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
115 | defer cancel()
116 |
117 | err := m.DB.QueryRowContext(ctx, query, args...).Scan(&user.Version)
118 | if err != nil {
119 | switch {
120 | case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
121 | return ErrDuplicateEmail
122 | case errors.Is(err, sql.ErrNoRows):
123 | return ErrEditConflict
124 | default:
125 | return err
126 | }
127 | }
128 | return nil
129 | }
130 |
131 | func (m *DBModel) GetUserByEmail(email string) (*User, error) {
132 | query := `SELECT id, created_at, name, email, password_hash, activated, version FROM users WHERE email = $1`
133 |
134 | var user User
135 |
136 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
137 | defer cancel()
138 |
139 | err := m.DB.QueryRowContext(ctx, query, email).Scan(
140 | &user.ID,
141 | &user.CreatedAt,
142 | &user.Name,
143 | &user.Email,
144 | &user.Password.hash,
145 | &user.Activated,
146 | &user.Version,
147 | )
148 |
149 | if err != nil {
150 | switch {
151 | case errors.Is(err, sql.ErrNoRows):
152 | return nil, ErrRecordNotFound
153 | default:
154 | return nil, err
155 | }
156 | }
157 |
158 | return &user, nil
159 | }
160 |
161 | // This updates the database info - not the user
162 | func (m *DBModel) Update(load *DBLoad) error {
163 | // This will handle DB update race condition
164 | query := `UPDATE dbload SET dbdataone = $1, dbdatatwo = $2, dbdatathree = $3, version = version + 1 where id = $4 and VERSION = $5 RETURNING version`
165 |
166 | args := []interface{}{
167 | load.DBDataOne,
168 | load.DBDataTwo,
169 | load.DBDataThree,
170 | load.ID,
171 | load.Version,
172 | }
173 |
174 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
175 | defer cancel()
176 |
177 | err := m.DB.QueryRowContext(ctx, query, args...).Scan(&load.Version)
178 | if err != nil {
179 | switch {
180 | case errors.Is(err, sql.ErrNoRows):
181 | return ErrEditConflict
182 | default:
183 | return err
184 | }
185 | }
186 | return nil
187 | }
188 |
189 | func (m *DBModel) GetAll(DBDataOne string, filters Filters) ([]*DBLoad, Metadata, error) {
190 | query := fmt.Sprintf(`SELECT count(*) OVER(), dbdataone, dbdatatwo, dbdatathree, id, version FROM dbload WHERE(to_tsvector('simple', dbdataone) @@ plainto_tsquery('simple', $1) OR $1='') ORDER BY %s %s, id ASC LIMIT $2 OFFSET $3`, filters.sortColumn(), filters.sortDirection())
191 |
192 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
193 | defer cancel()
194 |
195 | args := []interface{}{DBDataOne, filters.limit(), filters.offset()}
196 | rows, err := m.DB.QueryContext(ctx, query, args...)
197 | if err != nil {
198 | return nil, Metadata{}, err
199 | }
200 |
201 | defer rows.Close()
202 |
203 | totalRecords := 0
204 | DBdata := []*DBLoad{}
205 |
206 | for rows.Next() {
207 | var data DBLoad
208 |
209 | err := rows.Scan(
210 | &totalRecords,
211 | &data.DBDataOne,
212 | &data.DBDataTwo,
213 | &data.DBDataThree,
214 | &data.ID,
215 | &data.Version,
216 | )
217 |
218 | if err != nil {
219 | return nil, Metadata{}, err
220 | }
221 |
222 | DBdata = append(DBdata, &data)
223 | }
224 |
225 | if err = rows.Err(); err != nil {
226 | return nil, Metadata{}, err
227 | }
228 |
229 | metadata := createMetadata(totalRecords, filters.Page, filters.PageSize)
230 |
231 | return DBdata, metadata, nil
232 | }
233 |
234 | func (m *DBModel) GetForToken(tokenScope, TokenPlaintext string) (*User, error) {
235 | tokenHash := sha256.Sum256([]byte(TokenPlaintext))
236 |
237 | query := `SELECT users.id, users.created_at, users.name, users.email, users.password_hash, users.activated, users.version FROM users INNER JOIN tokens ON users.id = tokens.user_id WHERE tokens.hash = $1 AND tokens.scope = $2 AND tokens.expiry > $3`
238 |
239 | args := []interface{}{tokenHash[:], tokenScope, time.Now()}
240 |
241 | var user User
242 |
243 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
244 | defer cancel()
245 |
246 | err := m.DB.QueryRowContext(ctx, query, args...).Scan(
247 | &user.ID,
248 | &user.CreatedAt,
249 | &user.Name,
250 | &user.Email,
251 | &user.Password.hash,
252 | &user.Activated,
253 | &user.Version,
254 | )
255 |
256 | if err != nil {
257 | switch {
258 | case errors.Is(err, sql.ErrNoRows):
259 | return nil, ErrRecordNotFound
260 | default:
261 | return nil, err
262 | }
263 | }
264 |
265 | return &user, nil
266 | }
267 |
--------------------------------------------------------------------------------
/backend/cmd/handlers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "backend/models"
5 | "backend/validator"
6 | "encoding/json"
7 | "errors"
8 | //"fmt"
9 | //"github.com/pascaldekloe/jwt"
10 | //"golang.org/x/crypto/bcrypt"
11 | "log"
12 | "net/http"
13 | "time"
14 | )
15 |
16 | // Create a JSON message struct
17 | type JSONMessage struct {
18 | Message string `json:"message"`
19 | }
20 |
21 | // Create a register user type
22 | type UserPayload struct {
23 | Username string `json:"username"`
24 | Password string `json:"password"`
25 | }
26 |
27 | // Create a generic DBLoad type
28 | type DBLoadPayload struct {
29 | DBDataOne string `json:"db_data_one"`
30 | DBDataTwo string `json:"db_data_two"`
31 | DBDataThree string `json:"db_data_three"`
32 | }
33 |
34 | func (app *application) statusHandler(w http.ResponseWriter, r *http.Request) {
35 | response := struct {
36 | Status string
37 | }{"Curling Request"}
38 |
39 | js, err := json.MarshalIndent(response, "", "\t")
40 | if err != nil {
41 | app.logger.PrintError(err, nil)
42 | }
43 |
44 | w.Header().Set("Content-Type", "application/json")
45 | w.WriteHeader(http.StatusOK)
46 | w.Write(js)
47 | }
48 |
49 | func (app *application) registerUser(w http.ResponseWriter, r *http.Request) {
50 | var input struct {
51 | Name string `json:"name"`
52 | Email string `json:"email"`
53 | Password string `json:"password"`
54 | }
55 |
56 | err := app.readJSON(w, r, &input)
57 | if err != nil {
58 | app.badRequestResponse(w, r, err)
59 | return
60 | }
61 |
62 | user := &models.User{
63 | Name: input.Name,
64 | Email: input.Email,
65 | Activated: false,
66 | }
67 |
68 | // generate the password hash with bcrypt
69 | err = user.Password.Set(input.Password)
70 | if err != nil {
71 | app.serverErrorResponse(w, r, err)
72 | return
73 | }
74 |
75 | v := validator.New()
76 | if models.ValidateUser(v, user); !v.Valid() {
77 | app.failedValidationResponse(w, r, v.Errors)
78 | return
79 | }
80 |
81 | err = app.models.DB.Insert(user)
82 | if err != nil {
83 | switch {
84 | case errors.Is(err, models.ErrDuplicateEmail):
85 | v.AddError("email", "a user with this email already exists")
86 | app.failedValidationResponse(w, r, v.Errors)
87 | default:
88 | app.serverErrorResponse(w, r, err)
89 | }
90 | return
91 | }
92 |
93 | token, err := app.models.DB.NewToken(user.ID, 3*24*time.Hour, models.ScopeActivation)
94 | if err != nil {
95 | app.serverErrorResponse(w, r, err)
96 | return
97 | }
98 |
99 | // THIS IS WHERE WE SEND TO OUR SMTP
100 | // TODO: How to recover PANICS
101 |
102 | go func(){
103 | data := map[string]interface{}{
104 | "activationToken": token.Plaintext,
105 | "userID": user.ID,
106 | }
107 |
108 | err = app.mailer.Send(user.Email, "user_welcome.tmpl", data)
109 | if err != nil {
110 | app.logger.PrintError(err, nil)
111 | }
112 | }()
113 |
114 | err = app.writeJSON(w, http.StatusAccepted, envelope{"user":user}, nil)
115 | if err != nil {
116 | app.serverErrorResponse(w, r, err)
117 | }
118 | }
119 |
120 | // This function is client side - to - database
121 | func (app *application) getData(w http.ResponseWriter, r *http.Request) {
122 | id, err := app.readIDParam(r)
123 | if err != nil {
124 | app.notFoundResponse(w, r)
125 | return
126 | }
127 |
128 | data, err := app.models.DB.GetData(id)
129 | if err != nil {
130 | switch {
131 | case errors.Is(err, models.ErrRecordNotFound):
132 | app.notFoundResponse(w, r)
133 | default:
134 | app.serverErrorResponse(w, r, err)
135 | }
136 | return
137 | }
138 |
139 | err = app.writeJSON(w, http.StatusOK, envelope{"data": data}, nil)
140 | if err != nil {
141 | app.serverErrorResponse(w, r, err)
142 | }
143 | }
144 |
145 | // TODO: Refactor this aswell
146 | //func (app *application) login(w http.ResponseWriter, r *http.Request) {
147 | //var payload UserPayload
148 |
149 | //err := json.NewDecoder(r.Body).Decode(&payload)
150 | //if err != nil {
151 | //app.logger.PrintError(err, nil)
152 | //}
153 |
154 | ////we need to get the user
155 | //// TODO: replace with get usr by email
156 | //user, err := app.models.DB.GetUser(payload.Username)
157 | //if err != nil {
158 | //app.logger.PrintInfo("User does not exist", nil)
159 | //return
160 | //}
161 |
162 | //hashPassword := user.Password
163 |
164 | //err = bcrypt.CompareHashAndPassword([]byte(hashPassword), []byte(payload.Password))
165 | //// Handle the error for hasing and comparing
166 | //if err != nil {
167 | //log.Println(err)
168 | //_message := JSONMessage{
169 | //Message: "Unauthorized",
170 | //}
171 |
172 | //js, err := json.MarshalIndent(_message, "", "\t")
173 | //if err != nil {
174 | //app.logger.PrintError(err, nil)
175 | //}
176 |
177 | //w.Header().Set("Context-Type", "application/json")
178 | //w.WriteHeader(http.StatusOK)
179 | //w.Write(js)
180 | //return
181 | //}
182 |
183 | //// Validating a users token
184 | //var claims jwt.Claims
185 | //claims.Subject = fmt.Sprint(user.ID)
186 | //claims.Issued = jwt.NewNumericTime(time.Now())
187 | //claims.NotBefore = jwt.NewNumericTime(time.Now())
188 | //claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour))
189 | //// supposed to be a unique domain you own
190 | //claims.Issuer = "github.com/melkeydev"
191 | //claims.Audiences = []string{"github.com/melkeydev"}
192 |
193 | //jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.Jwt.Secret))
194 | //if err != nil {
195 | //fmt.Println(err)
196 | //message := "Could not generate proper access"
197 | //app.errorResponse(w, r, http.StatusInternalServerError, message)
198 | //return
199 | //}
200 |
201 | ////app.writeJSON(w, http.StatusOK, string(jwtBytes), "Successfully logged in")
202 | //_message := JSONMessage{
203 | //Message: string(jwtBytes),
204 | //}
205 |
206 | //js, err := json.MarshalIndent(_message, "", "\t")
207 | //if err != nil {
208 | //app.logger.PrintError(err, nil)
209 | //}
210 |
211 | //w.Header().Set("Context-Type", "application/json")
212 | //w.WriteHeader(http.StatusOK)
213 | //w.Write(js)
214 |
215 | //}
216 |
217 | func (app *application) insertPayload(w http.ResponseWriter, r *http.Request) {
218 | var payload DBLoadPayload
219 |
220 | err := json.NewDecoder(r.Body).Decode(&payload)
221 | if err != nil {
222 | log.Println(err)
223 | return
224 | }
225 |
226 | dbload := &models.DBLoad{
227 | DBDataOne: payload.DBDataOne,
228 | DBDataTwo: payload.DBDataTwo,
229 | DBDataThree: payload.DBDataThree,
230 | }
231 |
232 | v := validator.New()
233 |
234 | if models.ValidateDBLoad(v, dbload); !v.Valid() {
235 | app.failedValidationResponse(w, r, v.Errors)
236 | return
237 | }
238 |
239 | err = app.models.DB.InsertDBLoad(dbload)
240 | if err != nil {
241 | app.serverErrorResponse(w, r, err)
242 | return
243 | }
244 |
245 | err = app.writeJSON(w, http.StatusOK, envelope{"data": dbload}, nil)
246 | if err != nil {
247 | app.serverErrorResponse(w, r, err)
248 | }
249 | }
250 |
251 | func (app *application) deleteDBload(w http.ResponseWriter, r *http.Request) {
252 | id, err := app.readIDParam(r)
253 | if err != nil {
254 | app.notFoundResponse(w, r)
255 | }
256 |
257 | err = app.models.DB.Delete(id)
258 | if err != nil {
259 | switch {
260 | case errors.Is(err, models.ErrRecordNotFound):
261 | app.notFoundResponse(w, r)
262 | default:
263 | app.serverErrorResponse(w, r, err)
264 | }
265 | return
266 | }
267 |
268 | err = app.writeJSON(w, http.StatusOK, envelope{"message": "data deleted Succesfully"}, nil)
269 | if err != nil {
270 | app.serverErrorResponse(w, r, err)
271 | }
272 | }
273 |
274 | func (app *application) updateDBData(w http.ResponseWriter, r *http.Request) {
275 | id, err := app.readIDParam(r)
276 | if err != nil {
277 | app.notFoundResponse(w, r)
278 | return
279 | }
280 |
281 | // This is where we pull all the data first to update
282 | data, err := app.models.DB.GetData(id)
283 | if err != nil {
284 | switch {
285 | case errors.Is(err, models.ErrRecordNotFound):
286 | app.notFoundResponse(w, r)
287 | default:
288 | app.serverErrorResponse(w, r, err)
289 | }
290 | return
291 | }
292 |
293 | var input struct {
294 | DBDataOne *string `json:"db_data_one"`
295 | DBDataTwo *string `json:"db_data_two"`
296 | DBDataThree *string `json:"db_data_three"`
297 | }
298 |
299 | err = app.readJSON(w, r, &input)
300 | if err != nil {
301 | app.badRequestResponse(w, r, err)
302 | return
303 | }
304 |
305 | // Explicitly check each input
306 | if input.DBDataOne != nil {
307 | data.DBDataOne = *input.DBDataOne
308 | }
309 |
310 | if input.DBDataTwo != nil {
311 | data.DBDataTwo = *input.DBDataTwo
312 | }
313 |
314 | if input.DBDataThree != nil {
315 | data.DBDataThree = *input.DBDataThree
316 | }
317 |
318 | data.ID = id
319 |
320 | v := validator.New()
321 |
322 | // validate the json data
323 | if models.ValidateDBLoad(v, data); !v.Valid() {
324 | app.failedValidationResponse(w, r, v.Errors)
325 | return
326 | }
327 |
328 | // This needs to change
329 | err = app.models.DB.Update(data)
330 | if err != nil {
331 | switch {
332 | // the race condition editing error message
333 | case errors.Is(err, models.ErrEditConflict):
334 | app.editConflictResponse(w, r)
335 | default:
336 | app.serverErrorResponse(w, r, err)
337 | }
338 | return
339 | }
340 |
341 | err = app.writeJSON(w, http.StatusOK, envelope{"data": data}, nil)
342 | if err != nil {
343 | app.serverErrorResponse(w, r, err)
344 | }
345 | }
346 |
347 | func (app *application) listAllDBData(w http.ResponseWriter, r *http.Request) {
348 | var input struct {
349 | DBDataOne string
350 | models.Filters
351 | }
352 |
353 | v := validator.New()
354 | qs := r.URL.Query()
355 |
356 | // Query string readers go below
357 | input.DBDataOne = app.readString(qs, "dbdataone", "")
358 |
359 | input.Filters.Page = app.readInt(qs, "page", 1, v)
360 | input.Filters.PageSize = app.readInt(qs, "page_size", 20, v)
361 |
362 | input.Filters.Sort = app.readString(qs, "sort", "id")
363 | input.Filters.SortSafeList = []string{"id", "DBDataOne"}
364 |
365 | if models.ValidateFilters(v, input.Filters); !v.Valid() {
366 | app.failedValidationResponse(w, r, v.Errors)
367 | return
368 | }
369 | // We need to get all
370 |
371 | DBdata, metadata, err := app.models.DB.GetAll(input.DBDataOne, input.Filters)
372 | if err != nil {
373 | app.serverErrorResponse(w, r, err)
374 | return
375 | }
376 |
377 | err = app.writeJSON(w, http.StatusOK, envelope{"DBdata": DBdata, "metadata": metadata}, nil)
378 |
379 | if err != nil {
380 | app.serverErrorResponse(w, r, err)
381 | }
382 | }
383 |
384 | func (app *application) activateUser(w http.ResponseWriter, r *http.Request) {
385 | var input struct {
386 | TokenPlaintext string `json:"token"`
387 | }
388 |
389 | err := app.readJSON(w, r, &input)
390 | if err != nil {
391 | app.badRequestResponse(w, r, err)
392 | return
393 | }
394 |
395 | v := validator.New()
396 |
397 | if models.ValidateTokenPlaintext(v, input.TokenPlaintext); !v.Valid() {
398 | app.failedValidationResponse(w, r, v.Errors)
399 | return
400 | }
401 |
402 | user, err := app.models.DB.GetForToken(models.ScopeActivation, input.TokenPlaintext)
403 | if err != nil {
404 | switch{
405 | case errors.Is(err, models.ErrRecordNotFound):
406 | v.AddError("token", "invalid or expire auth token")
407 | app.failedValidationResponse(w, r, v.Errors)
408 | default:
409 | app.serverErrorResponse(w, r, err)
410 | }
411 | return
412 | }
413 |
414 | user.Activated = true
415 |
416 | // This needs to change
417 | err = app.models.DB.UpdateUser(user)
418 | if err != nil {
419 | switch {
420 | case errors.Is(err, models.ErrEditConflict):
421 | app.editConflictResponse(w, r)
422 | default:
423 | app.serverErrorResponse(w, r, err)
424 | }
425 | return
426 | }
427 |
428 | err = app.models.DB.DeleteAlForUser(models.ScopeActivation, user.ID)
429 | if err != nil {
430 | app.serverErrorResponse(w, r, err)
431 | return
432 | }
433 |
434 | err = app.writeJSON(w, http.StatusOK, envelope{"user":user}, nil)
435 | if err != nil {
436 | app.serverErrorResponse(w, r, err)
437 | }
438 | }
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
--------------------------------------------------------------------------------