├── .env.example ├── .gitignore ├── Dockerfile ├── README.md ├── common └── utils.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal ├── config │ ├── config.go │ └── db.go ├── handler │ ├── auth.handler.go │ ├── home.handler.go │ ├── post.handler.go │ └── user.handler.go ├── model │ ├── post.go │ └── user.go ├── router │ ├── auth.route.go │ ├── base.route.go │ ├── post.route.go │ └── user.route.go └── views │ ├── _posts.html │ ├── createpost.html │ ├── footer.html │ ├── header.html │ ├── index.html │ ├── login.html │ ├── profile.html │ ├── signup.html │ └── users.html ├── main.go └── public ├── .DS_Store ├── css └── style.css ├── img ├── .DS_Store ├── chat_icon.svg ├── delete.svg ├── guy_standing.svg ├── profile_header.jpg └── profile_pic.svg ├── js └── index.js └── test.txt /.env.example: -------------------------------------------------------------------------------- 1 | JWT_KEY= 2 | DB_HOST= 3 | DB_PORT= 4 | DB_USER= 5 | DB_PASS= 6 | DB_NAME= 7 | HASH_KEY= 8 | PGADMIN_DEFAULT_EMAIL=live@admin.com 9 | PGADMIN_DEFAULT_PASSWORD=password -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .air.toml 3 | /tmp 4 | tmp -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | FROM golang:alpine 3 | # AS builder 4 | ADD . /go/src/go-social-network 5 | WORKDIR /go/src/go-social-network 6 | # # Git is required for fetching the dependencies. 7 | # RUN ls 8 | RUN apk update && apk add --no-cache git 9 | # RUN go get github.com/dsa0x/go-social-network 10 | COPY . . 11 | RUN go get 12 | RUN go install 13 | 14 | # # Build the binary. 15 | # RUN go build -o main . 16 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o main . 17 | 18 | 19 | 20 | RUN ls 21 | ENV PORT=8080 22 | EXPOSE 8080 23 | RUN chmod +x /go/src/go-social-network/main 24 | CMD ["/go/src/go-social-network/main"] 25 | ENTRYPOINT ["/go/src/go-social-network/main"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chipper :: go-social-network 2 | 3 | 4 | 5 | # Introduction 6 | This is a mini social network created with golang, gorilla/mux and gorm. I built this to get more familiar with the Go language. 7 | I also decided to use go template just to get familiar with them. I'll probably go with rest apis for future projects. 8 | I intend to do some refactoring later on, but the functionalities are currently working. Feel free to fork it or make some suggestions to the current implementation. 9 | Inspiration gotten from yTakkar's . 10 | The website is live at https://chipper.daredev.xyz 11 | 12 | # Requirements 13 | Go - from v1.2 14 | 15 | PostgreSQL - from v10 16 | 17 | # Setup 18 | - Before setup, you must have PostgreSQL on your machine. Then clone this repository 19 | 20 | > https://github.com/dsa0x/go-social-network.git 21 | 22 | - cd into the project directory 23 | 24 | > cd go-social-network 25 | 26 | - Set environment variables 27 | 28 | - A sample is presented in the repository. 29 | 30 | > cp .env.example .env 31 | 32 | - Run application 33 | 34 | > go run main.go 35 | 36 | 37 | # Usage 38 | 39 | Endpoint | Usage 40 | ------------ | ------------- 41 | / | The Home page. It also lists the posts of all users 42 | /signup | Use this route to sign up a new user 43 | /login | Authenticate user to access protected endpoints 44 | /user/{id} | The User profile. Also showing the number of posts, followers, and followings of the user 45 | /logout | Logout the user 46 | /user/follow | To follow a user 47 | /user/unfollow | To unfollow a user 48 | /users | Displays a list of all users 49 | /post/create | To create a new post 50 | /post/{id}/update | status.NotYetImplemented 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "reflect" 9 | "strings" 10 | 11 | "golang.org/x/crypto/bcrypt" 12 | 13 | "github.com/dsa0x/go-social-network/internal/config" 14 | "github.com/go-playground/validator/v10" 15 | ) 16 | 17 | // Err error logger 18 | func Err(err interface{}, message ...string) { 19 | if err != nil { 20 | log.Fatal(message[0], err) 21 | } 22 | } 23 | 24 | //hashPassword to hash password 25 | func hashPassword(password string) ([]byte, error) { 26 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) 27 | return bytes, err 28 | } 29 | 30 | var validate *validator.Validate 31 | 32 | func init() { 33 | validate = validator.New() 34 | } 35 | 36 | // Validate v 37 | func Validate(s interface{}) map[string][]string { 38 | 39 | if err := validate.Struct(s); err != nil { 40 | 41 | if err, ok := err.(*validator.InvalidValidationError); ok { 42 | panic(err) 43 | } 44 | 45 | //Validation errors occurred 46 | errors := make(map[string][]string) 47 | 48 | reflected := reflect.ValueOf(s) 49 | for _, err := range err.(validator.ValidationErrors) { 50 | 51 | field, _ := reflected.Type().FieldByName(err.StructField()) 52 | var name string 53 | 54 | if name = field.Tag.Get("json"); name == "" { 55 | name = strings.ToLower(err.StructField()) 56 | } 57 | 58 | switch err.Tag() { 59 | case "required": 60 | errors[name] = append(errors[name], "The "+name+" field is required") 61 | break 62 | case "email": 63 | errors[name] = append(errors[name], "The "+name+" field is not valid email") 64 | break 65 | case "eqfield": 66 | errors[name] = append(errors[name], "The "+name+" should be equal to the "+err.Param()) 67 | break 68 | default: 69 | errors[name] = append(errors[name], "The "+name+" is invalid") 70 | break 71 | } 72 | } 73 | 74 | return errors 75 | } 76 | 77 | return nil 78 | 79 | } 80 | 81 | // CheckPasswordHash check the hash password match 82 | func CheckPasswordHash(password, hashedPassword string) bool { 83 | err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 84 | return err == nil 85 | } 86 | 87 | //HashPassword to hash password 88 | func HashPassword(password string) ([]byte, error) { 89 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) 90 | return bytes, err 91 | } 92 | 93 | // Jsonify js 94 | func Jsonify(message interface{}) interface{} { 95 | // res := make(map[string]interface{}) 96 | res, _ := json.Marshal(map[string]interface{}{"message": message}) 97 | return res 98 | 99 | } 100 | 101 | // ExecTemplate execute template 102 | func ExecTemplate(w http.ResponseWriter, template string, data interface{}) { 103 | 104 | err := config.Tpl.ExecuteTemplate(w, template, data) 105 | if err != nil { 106 | log.Fatalln("template didn't execute: ", template, err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | container_name: go_social 5 | build: 6 | dockerfile: Dockerfile 7 | context: . 8 | ports: 9 | - 8080:8080 10 | # restart: on-failure 11 | # volumes: 12 | # - .:/go/src/go-social-network/ 13 | # working_dir: /go/src/go-social-network/ 14 | # command: ls 15 | depends_on: 16 | - db # Uncomment this when using postgres. 17 | networks: 18 | - fullstack 19 | db: 20 | image: postgres:latest 21 | container_name: db_postgres 22 | environment: 23 | - POSTGRES_USER=${DB_USER} 24 | - POSTGRES_PASSWORD=${DB_PASS} 25 | - POSTGRES_DB=${DB_NAME} 26 | - DATABASE_HOST=${DB_HOST} 27 | ports: 28 | - "5432:5432" 29 | volumes: 30 | - database_postgres:/var/lib/postgresql/data 31 | networks: 32 | - fullstack 33 | pgadmin: 34 | image: dpage/pgadmin4 35 | container_name: pgadmin_container 36 | environment: 37 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} 38 | PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} 39 | depends_on: 40 | - db 41 | ports: 42 | - "5050:80" 43 | networks: 44 | - fullstack 45 | restart: unless-stopped 46 | 47 | volumes: 48 | api: 49 | database_postgres: # Uncomment this when using postgres. 50 | 51 | # Networks to be created to facilitate communication between containers 52 | networks: 53 | fullstack: 54 | driver: bridge 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dsa0x/go-social-network 2 | 3 | go 1.14 4 | 5 | require ( 6 | // github.com/cosmtrek/air v1.21.2 // indirect 7 | github.com/creack/pty v1.1.11 // indirect 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 9 | github.com/dsa0x/go-esocial v0.0.0-20200815154133-0c309e8e80f3 10 | github.com/fatih/color v1.9.0 // indirect 11 | github.com/go-playground/validator/v10 v10.3.0 12 | github.com/gorilla/mux v1.7.4 13 | github.com/gorilla/securecookie v1.1.1 14 | github.com/imdario/mergo v0.3.11 // indirect 15 | github.com/jinzhu/gorm v1.9.16 16 | github.com/joho/godotenv v1.3.0 17 | github.com/kelseyhightower/envconfig v1.4.0 18 | github.com/mattn/go-colorable v0.1.7 // indirect 19 | github.com/pelletier/go-toml v1.8.0 // indirect 20 | github.com/satori/go.uuid v1.2.0 21 | go.mongodb.org/mongo-driver v1.3.5 22 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd 23 | golang.org/x/sys v0.0.0-20200819171115-d785dc25833f // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 7 | github.com/cosmtrek/air v1.21.2 h1:PwChdKs3qlSkKucKwwC04daw5eoy4SVgiEBQiHX5L9A= 8 | github.com/cosmtrek/air v1.21.2/go.mod h1:5EsgUqrBIHlW2ghNoevwPBEG1FQvF5XNulikjPte538= 9 | github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= 12 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 16 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 17 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 18 | github.com/dsa0x/go-esocial v0.0.0-20200815154133-0c309e8e80f3 h1:AFgXy7/KciCppnoNuAJq3XQeubWa4aag2AE0l8Ye4mA= 19 | github.com/dsa0x/go-esocial v0.0.0-20200815154133-0c309e8e80f3/go.mod h1:vlX24kG19TcHA2QfAkSXdK6jooDuLWOPYpTd3LXJd7g= 20 | github.com/dsa0x/social-network v0.0.0-20200815150311-363d00a49209 h1:SHnYXMDQT5I+G9D11mEY1gNEgdT9kB2eCOE6AfAYJ0k= 21 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 22 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 23 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 24 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 25 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 26 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 27 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 28 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 29 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 30 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 31 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 32 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 33 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 34 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 35 | github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= 36 | github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 37 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 38 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 39 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 40 | github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= 41 | github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= 42 | github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= 43 | github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 44 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 45 | github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= 46 | github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 47 | github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 48 | github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= 49 | github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= 50 | github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= 51 | github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= 52 | github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= 53 | github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= 54 | github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= 55 | github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= 56 | github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= 57 | github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 58 | github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 59 | github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 60 | github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 61 | github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= 62 | github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= 63 | github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= 64 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 65 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 66 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 67 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 69 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 70 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 71 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 72 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 73 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 74 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 75 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 76 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 77 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 78 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 79 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 80 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 81 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 82 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 83 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 84 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 85 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 86 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 87 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 88 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 89 | github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= 90 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 91 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= 92 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 93 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 94 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 95 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 96 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 97 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 98 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 99 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 100 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 101 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 102 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 103 | github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= 104 | github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= 105 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 106 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 107 | github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M= 108 | github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 109 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 110 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 111 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 112 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 113 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 114 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 115 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 116 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 117 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 118 | github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= 119 | github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= 120 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 121 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 122 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= 123 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 124 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 125 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= 126 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 127 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 128 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 129 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 130 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 131 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 132 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 133 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 134 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 135 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 136 | github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= 137 | github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= 138 | github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 139 | github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= 140 | github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= 141 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 142 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 143 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 145 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 146 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 147 | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 148 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 149 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 150 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 151 | github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 152 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 153 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 154 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 155 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 156 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 157 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 158 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 159 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 160 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 161 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 162 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= 163 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 164 | github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc h1:n+nNi93yXLkJvKwXNP9d55HC7lGK4H/SRcwB5IaUZLo= 165 | github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 166 | go.mongodb.org/mongo-driver v1.3.5 h1:S0ZOruh4YGHjD7JoN7mIsTrNjnQbOjrmgrx6l6pZN7I= 167 | go.mongodb.org/mongo-driver v1.3.5/go.mod h1:Ual6Gkco7ZGQw8wE1t4tLnvBsf6yVSM60qW6TgOeJ5c= 168 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 169 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 170 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 171 | golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 172 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 173 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= 174 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 175 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= 176 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 177 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 178 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 179 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 180 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 181 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 182 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 183 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 184 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 185 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 186 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 187 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 188 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 189 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 190 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 191 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 192 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 193 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 194 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 195 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 196 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 197 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 198 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 199 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 200 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 201 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c= 212 | golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20200819171115-d785dc25833f h1:KJuwZVtZBVzDmEDtB2zro9CXkD9O0dpCv4o2LHbQIAw= 214 | golang.org/x/sys v0.0.0-20200819171115-d785dc25833f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 216 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 217 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 218 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 219 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 220 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 221 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 222 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 223 | golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 224 | golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 225 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 226 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 227 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 228 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 229 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 230 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 231 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 232 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 233 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 234 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 235 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 236 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 237 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 238 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 239 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 240 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 241 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 242 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 243 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 244 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 245 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 246 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 247 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 248 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 249 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 250 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 251 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 252 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 253 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 254 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "os" 7 | 8 | "github.com/joho/godotenv" 9 | "github.com/kelseyhightower/envconfig" 10 | ) 11 | 12 | type envVars struct { 13 | Dbhost string `required:"true" envconfig:"DB_HOST"` 14 | Dbport string `required:"true" envconfig:"DB_PORT"` 15 | Dbuser string `required:"true" envconfig:"DB_USER"` 16 | Dbpassword string `required:"true" envconfig:"DB_PASS"` 17 | Dbname string `required:"true" envconfig:"DB_NAME"` 18 | JwtKey string `required:"true" envconfig:"JWT_KEY"` 19 | HashKey string `required:"true" envconfig:"HASH_KEY"` 20 | } 21 | 22 | //Env holds application config variables 23 | var Env envVars 24 | 25 | // Tpl template 26 | var Tpl *template.Template 27 | 28 | func init() { 29 | 30 | wd, err := os.Getwd() 31 | if err != nil { 32 | log.Println(err, "::Unable to get paths") 33 | } 34 | 35 | Tpl = template.Must(template.ParseGlob(wd + "/internal/views/*.html")) 36 | 37 | //load .env file 38 | err = godotenv.Load(wd + "/./.env") 39 | 40 | if err != nil { 41 | log.Println("Error loading .env file, falling back to cli passed env") 42 | } 43 | 44 | err = envconfig.Process("", &Env) 45 | 46 | if err != nil { 47 | log.Fatalln("Error loading environment variables", err) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /internal/config/db.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/jinzhu/gorm" 9 | _ "github.com/jinzhu/gorm/dialects/postgres" 10 | ) 11 | 12 | // DB returns the database object 13 | func DB() *gorm.DB { 14 | 15 | host := os.Getenv("DB_HOST") 16 | port := os.Getenv("DB_PORT") 17 | dbname := os.Getenv("DB_NAME") 18 | username := os.Getenv("DB_USER") 19 | password := os.Getenv("DB_PASS") 20 | 21 | psqlInfo := fmt.Sprintf("host=%s port=%s user=%s "+ 22 | "password=%s dbname=%s sslmode=disable", 23 | host, port, username, password, dbname) 24 | 25 | db, err := gorm.Open("postgres", psqlInfo) 26 | 27 | db = db.BlockGlobalUpdate(true) 28 | 29 | if err != nil { 30 | log.Fatal("Database error: ", err) 31 | panic(err) 32 | } 33 | log.Println("Database Connected") 34 | 35 | return db 36 | 37 | } 38 | -------------------------------------------------------------------------------- /internal/handler/auth.handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/dgrijalva/jwt-go" 12 | "github.com/dsa0x/go-social-network/common" 13 | "github.com/dsa0x/go-social-network/internal/config" 14 | "github.com/dsa0x/go-social-network/internal/model" 15 | "github.com/gorilla/securecookie" 16 | ) 17 | 18 | type Credentials struct { 19 | Email string `json:"email" validate:"required,email"` 20 | Password string `json:"password" validate:"required"` 21 | } 22 | 23 | type Claims struct { 24 | Email string `json:"email"` 25 | ID uint `json:"id"` 26 | Name string `json:"username"` 27 | jwt.StandardClaims 28 | } 29 | 30 | type ClaimsCred struct { 31 | Email string `json:"email"` 32 | ID uint `json:"id"` 33 | Name string `json:"username"` 34 | } 35 | 36 | type ContextKey string 37 | 38 | type Error map[string][]string 39 | 40 | var jwtKey = []byte(config.Env.JwtKey) 41 | var hashKey = []byte(config.Env.HashKey) 42 | var secureCookie = securecookie.New(hashKey, nil) 43 | 44 | func Login(w http.ResponseWriter, r *http.Request) { 45 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 46 | 47 | var errs = make(Error) 48 | 49 | if r.Method == http.MethodPost { 50 | creds := Credentials{} 51 | email := r.FormValue("email") 52 | password := r.FormValue("password") 53 | creds.Email = strings.ToLower(email) 54 | creds.Password = password 55 | 56 | if err := common.Validate(creds); err != nil { 57 | w.WriteHeader(http.StatusBadRequest) 58 | log.Println(err, "::Validation failed") 59 | errs["mismatch"] = append(errs["mismatch"], "Username or password incorrect") 60 | common.ExecTemplate(w, "login.html", err) 61 | return 62 | } 63 | 64 | user, err := model.FindOne(creds.Email) 65 | if err != nil { 66 | w.WriteHeader(http.StatusNotFound) 67 | log.Println(err, "::No user found") 68 | errs["mismatch"] = append(errs["mismatch"], "Username or password incorrect") 69 | common.ExecTemplate(w, "login.html", errs) 70 | return 71 | } 72 | 73 | matched := common.CheckPasswordHash(creds.Password, user.Password) 74 | if err != nil || !matched { 75 | log.Println(err, user.ID, user.UserName, "::password mismatch") 76 | w.WriteHeader(http.StatusForbidden) 77 | errs["mismatch"] = append(errs["mismatch"], "Username or password incorrect") 78 | common.ExecTemplate(w, "login.html", errs) 79 | return 80 | } 81 | 82 | expirationTime := time.Now().Add(30 * time.Minute) 83 | 84 | claims := &Claims{ 85 | Email: user.Email, 86 | ID: user.ID, 87 | Name: user.UserName, 88 | StandardClaims: jwt.StandardClaims{ 89 | // In JWT, the expiry time is expressed as unix milliseconds 90 | ExpiresAt: expirationTime.Unix(), 91 | }, 92 | } 93 | 94 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 95 | tokenString, err := token.SignedString(jwtKey) 96 | 97 | if err != nil { 98 | log.Println(err, "token signature error") 99 | w.WriteHeader(http.StatusInternalServerError) 100 | errs["mismatch"] = append(errs["mismatch"], "An error occured. please try again") 101 | common.ExecTemplate(w, "login.html", errs) 102 | return 103 | } 104 | 105 | encoded, err := secureCookie.Encode("session", tokenString) 106 | expire := time.Now().Add(24 * time.Hour) 107 | cookie := &http.Cookie{ 108 | Name: "session", Value: encoded, HttpOnly: true, Expires: expire, 109 | } 110 | 111 | http.SetCookie(w, cookie) 112 | 113 | http.Redirect(w, r, "/", http.StatusSeeOther) 114 | } 115 | 116 | common.ExecTemplate(w, "login.html", nil) 117 | 118 | } 119 | 120 | func SignUp(w http.ResponseWriter, r *http.Request) { 121 | var errs = make(Error) 122 | if r.Method == http.MethodPost { 123 | 124 | email := r.FormValue("email") 125 | password := r.FormValue("password") 126 | cpassword := r.FormValue("cpassword") 127 | username := r.FormValue("username") 128 | user := model.User{} 129 | user.Email = email 130 | user.Password = password 131 | user.ConfirmPassword = cpassword 132 | user.UserName = username 133 | 134 | if user.Password == "" || user.Password != user.ConfirmPassword { 135 | w.WriteHeader(http.StatusNotFound) 136 | errs["mismatch"] = append(errs["mismatch"], "Passwords do not match") 137 | common.ExecTemplate(w, "signup.html", errs) 138 | return 139 | } 140 | 141 | id, err := model.CreateUser(user) 142 | if err != nil { 143 | log.Println(err) 144 | w.WriteHeader(http.StatusBadRequest) 145 | if strings.Contains(err.Error(), `unique constraint "users_user_name_key"`) { 146 | errs["mismatch"] = append(errs["mismatch"], "Username already exists") 147 | } else if strings.Contains(err.Error(), `unique constraint "users_email_key"`) { 148 | errs["mismatch"] = append(errs["mismatch"], "Email already exists") 149 | } else { 150 | errs["mismatch"] = append(errs["mismatch"], err.Error()) 151 | } 152 | common.ExecTemplate(w, "signup.html", errs) 153 | return 154 | } 155 | 156 | log.Printf("User with id %d created", id) 157 | 158 | // redirect 159 | http.Redirect(w, r, "/login", http.StatusSeeOther) 160 | 161 | } 162 | 163 | common.ExecTemplate(w, "signup.html", nil) 164 | 165 | } 166 | 167 | func Logout(w http.ResponseWriter, r *http.Request) { 168 | 169 | expire := time.Now().Add(-7 * 24 * time.Hour) 170 | cookie := &http.Cookie{ 171 | Name: "session", Value: "", Expires: expire, 172 | } 173 | 174 | http.SetCookie(w, cookie) 175 | http.Redirect(w, r, "/", http.StatusSeeOther) 176 | 177 | } 178 | 179 | //Auth authenticate user 180 | func Auth(f http.HandlerFunc) http.HandlerFunc { 181 | return func(w http.ResponseWriter, r *http.Request) { 182 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 183 | cookie, err := r.Cookie("session") 184 | if err != nil { 185 | log.Println(err, "Invalid Session") 186 | http.Redirect(w, r, "/guest", http.StatusSeeOther) 187 | return 188 | } 189 | var reqToken string 190 | if err = secureCookie.Decode("session", cookie.Value, &reqToken); err != nil { 191 | log.Println(err, "Unable to decode session") 192 | http.Redirect(w, r, "/login", http.StatusSeeOther) 193 | return 194 | } 195 | claims := &Claims{} 196 | 197 | token, err := jwt.ParseWithClaims(reqToken, claims, func(token *jwt.Token) (interface{}, error) { 198 | return jwtKey, nil 199 | }) 200 | if err != nil { 201 | log.Println(err, "Invalid Authorization") 202 | http.Redirect(w, r, "/login", http.StatusSeeOther) 203 | return 204 | } 205 | if !token.Valid { 206 | log.Println(err, "Invalid Token") 207 | w.WriteHeader(http.StatusUnauthorized) 208 | http.Redirect(w, r, "/login", http.StatusSeeOther) 209 | return 210 | } 211 | const cKey = ContextKey("user") 212 | ctx := context.WithValue(r.Context(), cKey, ClaimsCred{Email: claims.Email, ID: claims.ID, Name: claims.Name}) 213 | 214 | var data struct { 215 | ID string 216 | Name string 217 | Title string 218 | } 219 | data.ID = fmt.Sprint(claims.ID) 220 | data.Name = claims.Name 221 | 222 | r = r.WithContext(ctx) 223 | f(w, r) 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /internal/handler/home.handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/dsa0x/go-social-network/internal/model" 8 | 9 | "github.com/dsa0x/go-social-network/common" 10 | ) 11 | 12 | type HomePosts struct { 13 | Posts []model.Post 14 | User ClaimsCred 15 | ID string 16 | LoggedInUserId string 17 | } 18 | 19 | // Home function for home handler 20 | func Home(w http.ResponseWriter, r *http.Request) { 21 | 22 | //get user from context 23 | const cKey = ContextKey("user") 24 | user := r.Context().Value(cKey) 25 | 26 | deletePostID := r.FormValue("postId") 27 | if user != nil && r.Method == http.MethodPost && deletePostID == "" { 28 | CreatePost2(w, r, "/", deletePostID) 29 | } 30 | if user != nil && r.Method == http.MethodPost && deletePostID != "" { 31 | DeletePost2(w, r, "/", deletePostID) 32 | } 33 | 34 | // fetch all posts for homepage 35 | _posts := []model.Post{} 36 | err := model.FetchPosts(&_posts) 37 | if err != nil { 38 | http.Redirect(w, r, "/login", http.StatusSeeOther) 39 | return 40 | } 41 | posts := HomePosts{} 42 | if user != nil { 43 | posts.User = user.(ClaimsCred) 44 | posts.ID = strconv.Itoa(int(posts.User.ID)) 45 | posts.LoggedInUserId = posts.ID 46 | } 47 | 48 | posts.Posts = _posts 49 | common.ExecTemplate(w, "index.html", posts) 50 | 51 | } 52 | 53 | func Wrap(ID string, Title string) map[string]interface{} { 54 | return map[string]interface{}{ 55 | "ID": ID, 56 | "Title": Title, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/handler/post.handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/gorilla/mux" 11 | 12 | "github.com/dsa0x/go-social-network/common" 13 | "github.com/dsa0x/go-social-network/internal/model" 14 | ) 15 | 16 | func CreatePost(w http.ResponseWriter, r *http.Request) { 17 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 18 | post := model.Post{} 19 | 20 | err := json.NewDecoder(r.Body).Decode(&post) 21 | if err != nil { 22 | http.Error(w, err.Error(), http.StatusBadRequest) 23 | return 24 | } 25 | 26 | const cKey = ContextKey("user") 27 | user := r.Context().Value(cKey).(ClaimsCred) 28 | 29 | post.CreatedBy = user.ID 30 | post.Author = user.Name 31 | 32 | post.Title = strings.TrimSpace(post.Title) 33 | post.Content = strings.TrimSpace(post.Content) 34 | 35 | ID, err := model.CreatePost(post) 36 | if err != nil { 37 | w.WriteHeader(http.StatusInternalServerError) 38 | w.Write([]byte(`{"message": "Internal server error"}`)) 39 | return 40 | } 41 | 42 | resp := map[string]interface{}{"message": "Post created", "postId": ID} 43 | jsonPost, _ := json.Marshal(resp) 44 | w.WriteHeader(http.StatusOK) 45 | w.Write([]byte(jsonPost)) 46 | 47 | } 48 | 49 | func CreatePost2(w http.ResponseWriter, r *http.Request, redirectPath string, deletePostID string) { 50 | const cKey = ContextKey("user") 51 | user := r.Context().Value(cKey) 52 | if user == nil { 53 | common.ExecTemplate(w, "index.html", user) 54 | http.Redirect(w, r, "/login", http.StatusSeeOther) 55 | 56 | } 57 | 58 | if user != nil && r.Method == http.MethodPost && deletePostID == "" { 59 | user := user.(ClaimsCred) 60 | post := model.Post{} 61 | chip := r.FormValue("chip") 62 | post.Content = strings.TrimSpace(chip) 63 | if post.Content == "" { 64 | http.Redirect(w, r, redirectPath, http.StatusSeeOther) 65 | 66 | } 67 | post.Author = user.Name 68 | post.CreatedBy = user.ID 69 | ID, err := model.CreatePost(post) 70 | if err != nil { 71 | w.WriteHeader(http.StatusInternalServerError) 72 | w.Write([]byte(`{"message": "Internal server error"}` + fmt.Sprint(ID))) 73 | return 74 | } 75 | http.Redirect(w, r, redirectPath, http.StatusSeeOther) 76 | 77 | } 78 | } 79 | 80 | func DeletePost2(w http.ResponseWriter, r *http.Request, redirectPath string, deletePostID string) { 81 | //delete post from homepage 82 | const cKey = ContextKey("user") 83 | user := r.Context().Value(cKey) 84 | if user == nil { 85 | common.ExecTemplate(w, "index.html", user) 86 | http.Redirect(w, r, "/login", http.StatusSeeOther) 87 | 88 | } 89 | if user != nil && r.Method == http.MethodPost && deletePostID != "" { 90 | user := user.(ClaimsCred) 91 | deletePostIDInt, _ := strconv.Atoi(deletePostID) 92 | _, err := model.DeletePost(uint(deletePostIDInt), user.ID) 93 | if err != nil { 94 | w.WriteHeader(http.StatusInternalServerError) 95 | w.Write([]byte(`{"message": "Internal server error"}`)) 96 | return 97 | } 98 | // common.ExecTemplate(w, "index.html", posts) 99 | http.Redirect(w, r, "/", http.StatusSeeOther) 100 | // return 101 | } 102 | } 103 | 104 | func DeletePost(w http.ResponseWriter, r *http.Request) { 105 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 106 | 107 | var postId, err = strconv.Atoi(mux.Vars(r)["id"]) 108 | if err != nil { 109 | w.WriteHeader(http.StatusBadRequest) 110 | w.Write([]byte(`{"message": "Bad Request"}`)) 111 | return 112 | } 113 | 114 | const cKey = ContextKey("user") 115 | user := r.Context().Value(cKey).(ClaimsCred) 116 | 117 | ID, err := model.DeletePost(uint(postId), user.ID) 118 | if err != nil { 119 | w.WriteHeader(http.StatusNotFound) 120 | w.Write([]byte(`{"message": "Post not found"}`)) 121 | return 122 | } 123 | 124 | resp := map[string]interface{}{"message": "Post deleted", "postId": ID} 125 | jsonPost, _ := json.Marshal(resp) 126 | // w.WriteHeader(http.StatusFound) 127 | w.Write([]byte(jsonPost)) 128 | 129 | } 130 | 131 | func FetchAllPosts(w http.ResponseWriter, r *http.Request) { 132 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 133 | posts := []model.Post{} 134 | 135 | err := model.FetchPosts(&posts) 136 | if err != nil { 137 | w.WriteHeader(http.StatusInternalServerError) 138 | w.Write([]byte(`{"message": "An error occurred"}`)) 139 | return 140 | } 141 | 142 | jsonPosts, _ := json.Marshal(posts) 143 | w.WriteHeader(http.StatusFound) 144 | w.Write([]byte(jsonPosts)) 145 | 146 | } 147 | func FetchUserPosts(w http.ResponseWriter, r *http.Request) { 148 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 149 | posts := []model.Post{} 150 | 151 | const cKey = ContextKey("user") 152 | user := r.Context().Value(cKey).(ClaimsCred) 153 | 154 | err := model.FetchUserPosts(&posts, user.ID) 155 | if err != nil { 156 | w.WriteHeader(http.StatusInternalServerError) 157 | w.Write([]byte(`{"message": "An error occurred"}`)) 158 | return 159 | } 160 | 161 | jsonPosts, _ := json.Marshal(posts) 162 | w.WriteHeader(http.StatusFound) 163 | w.Write([]byte(jsonPosts)) 164 | 165 | } 166 | func UpdatePost(w http.ResponseWriter, r *http.Request) { 167 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 168 | post := model.Post{} 169 | err := json.NewDecoder(r.Body).Decode(&post) 170 | if err != nil { 171 | http.Error(w, err.Error(), http.StatusBadRequest) 172 | return 173 | } 174 | 175 | postID, err := strconv.Atoi(mux.Vars(r)["id"]) 176 | if err != nil { 177 | w.WriteHeader(http.StatusBadRequest) 178 | w.Write([]byte(`{"message": "Bad Request"}`)) 179 | return 180 | } 181 | 182 | const cKey = ContextKey("user") 183 | user := r.Context().Value(cKey).(ClaimsCred) 184 | 185 | post, err = model.UpdatePost(post, uint(postID), user.ID) 186 | if err != nil { 187 | w.WriteHeader(http.StatusInternalServerError) 188 | w.Write([]byte(`{"message": "An error occurred"}`)) 189 | return 190 | } 191 | 192 | jsonPost, _ := json.Marshal(post) 193 | w.WriteHeader(http.StatusFound) 194 | w.Write([]byte(jsonPost)) 195 | 196 | } 197 | -------------------------------------------------------------------------------- /internal/handler/user.handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/dsa0x/go-social-network/common" 9 | "github.com/dsa0x/go-social-network/internal/model" 10 | 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | type Profile struct { 15 | Username string 16 | ID string 17 | Following int 18 | Followers int 19 | IsFollower bool 20 | Avatar string 21 | MyProfile bool 22 | Posts []model.Post 23 | User ClaimsCred 24 | LoggedInUserId string 25 | LoggedInUsername string 26 | PostCount int 27 | } 28 | 29 | func GetUser(w http.ResponseWriter, r *http.Request) { 30 | 31 | const cKey = ContextKey("user") 32 | loggedUser := r.Context().Value(cKey) 33 | 34 | if loggedUser == nil { 35 | common.ExecTemplate(w, "index.html", loggedUser) 36 | http.Redirect(w, r, "/login", http.StatusSeeOther) 37 | // return 38 | } 39 | params := mux.Vars(r) 40 | redirectPath := "/user/" + params["id"] 41 | deletePostID := r.FormValue("postId") 42 | if loggedUser != nil && r.Method == http.MethodPost && deletePostID == "" { 43 | CreatePost2(w, r, redirectPath, deletePostID) 44 | } 45 | if loggedUser != nil && r.Method == http.MethodPost && deletePostID != "" { 46 | DeletePost2(w, r, "/", deletePostID) 47 | } 48 | 49 | profile := Profile{} 50 | 51 | id, err := strconv.Atoi(params["id"]) 52 | if err != nil { 53 | w.WriteHeader(http.StatusBadRequest) 54 | w.Write([]byte(`{"message": "Bad request"}`)) 55 | return 56 | } 57 | user, err := model.FindByID(uint(id)) 58 | if err != nil { 59 | w.WriteHeader(http.StatusNotFound) 60 | w.Write([]byte(`{"message": "User not found"}`)) 61 | return 62 | } 63 | loggedInUser := loggedUser.(ClaimsCred) 64 | if loggedInUser.ID == user.ID { 65 | profile.MyProfile = true 66 | } 67 | 68 | followings, err := model.CountFollowings(user.ID) 69 | if err != nil { 70 | w.WriteHeader(http.StatusNotFound) 71 | w.Write([]byte(`{"message": "User not found"}`)) 72 | return 73 | } 74 | followers, err := model.CountFollowers(user.ID) 75 | if err != nil { 76 | w.WriteHeader(http.StatusNotFound) 77 | w.Write([]byte(`{"message": "User not found"}`)) 78 | return 79 | } 80 | isFollower, _ := model.IsFollower(loggedInUser.ID, user.ID) 81 | 82 | posts := []model.Post{} 83 | err = model.FetchUserPosts(&posts, user.ID) 84 | if err != nil { 85 | w.WriteHeader(http.StatusInternalServerError) 86 | w.Write([]byte(`{"message": "An error occurred"}`)) 87 | return 88 | } 89 | count, err := model.CountUserPosts(user.ID) 90 | if err != nil { 91 | w.WriteHeader(http.StatusInternalServerError) 92 | w.Write([]byte(`{"message": "An error occurred"}`)) 93 | return 94 | } 95 | 96 | profile.Following = followings 97 | profile.Followers = followers 98 | profile.IsFollower = isFollower 99 | profile.ID = strconv.Itoa(int(user.ID)) 100 | profile.Username = strings.Title(user.UserName) 101 | profile.Avatar = "profile_pic.svg" 102 | profile.Avatar = "/public/img/" + profile.Avatar 103 | profile.Posts = posts 104 | profile.User.Name = profile.Username 105 | profile.User.ID = loggedInUser.ID 106 | profile.LoggedInUserId = strconv.Itoa(int(loggedInUser.ID)) 107 | profile.LoggedInUsername = loggedInUser.Name 108 | profile.PostCount = count 109 | 110 | // w.WriteHeader(http.StatusFound) 111 | common.ExecTemplate(w, "profile.html", profile) 112 | 113 | } 114 | func FollowUser(w http.ResponseWriter, r *http.Request) { 115 | // w.Header().Set("Content-Type", "text/html; charset=utf-8") 116 | params := mux.Vars(r) 117 | id, err := strconv.Atoi(params["id"]) 118 | if err != nil { 119 | w.WriteHeader(http.StatusBadRequest) 120 | w.Write([]byte(`{"message": "Bad request"}`)) 121 | return 122 | } 123 | const cKey = ContextKey("user") 124 | user := r.Context().Value(cKey).(ClaimsCred) 125 | 126 | _, err = model.Follow(user.ID, uint(id)) 127 | if err != nil { 128 | http.Redirect(w, r, "/login", http.StatusNotFound) 129 | w.WriteHeader(http.StatusNotFound) 130 | w.Write([]byte(`{"message": "User not found"}`)) 131 | return 132 | } 133 | http.Redirect(w, r, "/user/"+params["id"], http.StatusSeeOther) 134 | return 135 | } 136 | 137 | func UnfollowUser(w http.ResponseWriter, r *http.Request) { 138 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 139 | params := mux.Vars(r) 140 | id, err := strconv.Atoi(params["id"]) 141 | if err != nil { 142 | w.WriteHeader(http.StatusBadRequest) 143 | w.Write([]byte(`{"message": "Bad request"}`)) 144 | return 145 | } 146 | const cKey = ContextKey("user") 147 | user := r.Context().Value(cKey).(ClaimsCred) 148 | 149 | _, err = model.Unfollow(user.ID, uint(id)) 150 | if err != nil { 151 | w.WriteHeader(http.StatusNotFound) 152 | w.Write([]byte(`{"message": "User not found"}`)) 153 | return 154 | } 155 | 156 | http.Redirect(w, r, "/user/"+params["id"], http.StatusSeeOther) 157 | return 158 | } 159 | 160 | func GetAllUsers(w http.ResponseWriter, r *http.Request) { 161 | users := []model.User{} 162 | err := model.FetchUsers(&users) 163 | if err != nil { 164 | common.ExecTemplate(w, "users.html", nil) 165 | return 166 | } 167 | 168 | const cKey = ContextKey("user") 169 | user := r.Context().Value(cKey).(ClaimsCred) 170 | userID := strconv.Itoa(int(user.ID)) 171 | data := struct { 172 | LoggedInUserId string 173 | Users []model.User 174 | }{ 175 | userID, 176 | users, 177 | } 178 | 179 | common.ExecTemplate(w, "users.html", data) 180 | } 181 | -------------------------------------------------------------------------------- /internal/model/post.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | type Post struct { 10 | gorm.Model `json:"-"` 11 | Title string 12 | Content string 13 | CreatedBy uint 14 | Author string 15 | } 16 | 17 | func init() { 18 | // Db.CreateTable(&Post{}) 19 | } 20 | 21 | // CreatePost create post 22 | func CreatePost(post Post) (uint, error) { 23 | createdPost := Db.Create(&post) 24 | if createdPost.Error != nil { 25 | log.Println(createdPost.Error) 26 | return post.ID, createdPost.Error 27 | } 28 | return post.ID, nil 29 | } 30 | 31 | // DeletePost delete post 32 | func DeletePost(postID uint, userID uint) (uint, error) { 33 | deletedPost := Db.Unscoped().Delete(Post{}, "ID = ? and created_by = ?", postID, userID) 34 | if deletedPost.Error != nil { 35 | log.Println(deletedPost.Error) 36 | return postID, deletedPost.Error 37 | } 38 | 39 | return postID, nil 40 | } 41 | 42 | // UpdatePost updates the post 43 | func UpdatePost(post Post, postID uint, userID uint) (Post, error) { 44 | oldPost := Post{} 45 | updatedPost := Db.First(&oldPost, postID) 46 | oldPost.Title = post.Title 47 | oldPost.Content = post.Content 48 | updatedPost = Db.Save(&oldPost) 49 | if updatedPost.Error != nil { 50 | log.Println(updatedPost.Error) 51 | return oldPost, updatedPost.Error 52 | } 53 | 54 | return oldPost, nil 55 | } 56 | 57 | // FetchPosts fetch post 58 | func FetchPosts(posts *[]Post) error { 59 | allPosts := Db.Model(&Post{}).Order("created_at desc").Find(&posts) 60 | if allPosts.Error != nil { 61 | log.Println(allPosts.Error) 62 | return allPosts.Error 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func FetchUserPosts(posts *[]Post, userID uint) error { 69 | allPosts := Db.Model(&Post{}).Order("created_at desc").Find(&posts, "created_by = ?", userID) 70 | if allPosts.Error != nil { 71 | log.Println(allPosts.Error) 72 | return allPosts.Error 73 | } 74 | 75 | return nil 76 | } 77 | func CountUserPosts(userID uint) (int, error) { 78 | var count int 79 | allPosts := Db.Model(&Post{}).Where("created_by = ?", userID).Count(&count) 80 | if allPosts.Error != nil { 81 | log.Println(allPosts.Error) 82 | return count, allPosts.Error 83 | } 84 | 85 | return count, nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/dsa0x/go-social-network/common" 7 | "github.com/dsa0x/go-social-network/internal/config" 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | // Db database 12 | var Db = config.DB() 13 | 14 | func init() { 15 | // defer db.Close() 16 | // Db.DropTableIfExists(&User{}) 17 | // Db.CreateTable(&User{}) 18 | Db.AutoMigrate(&User{}) 19 | } 20 | 21 | // User struct declaration 22 | type User struct { 23 | gorm.Model 24 | UserName string `json:"username" binding:"required" gorm:"unique;not null" validate:"required,min=5,max=100"` 25 | Email string `json:"email" binding:"required" gorm:"unique;not null" validate:"required,min=8,max=100"` 26 | Friends []User `gorm:"many2many:friendships;association_jointable_foreignkey:friend_id"` 27 | Password string `json:"password,omitempty" binding:"required" validate:"required,min=6,max=100"` 28 | ConfirmPassword string `json:"confirmPassword,omitempty" binding:"required" sql:"-"` 29 | } 30 | 31 | func FetchUsers(users *[]User) error { 32 | allUsers := Db.Model(&User{}).Order("user_name asc").Find(&users) 33 | if allUsers.Error != nil { 34 | log.Println(allUsers.Error) 35 | return allUsers.Error 36 | } 37 | 38 | return nil 39 | } 40 | 41 | // FindOne finds one user by emails 42 | func FindOne(email string) (*User, error) { 43 | user := &User{} 44 | 45 | if err := Db.Where("email = ?", email).First(user).Error; err != nil { 46 | // var resp := map[string]interface{} {"status": false, "message": "invalid "} 47 | return user, err 48 | } 49 | 50 | return user, nil 51 | 52 | } 53 | 54 | // FindByID finds one user by id 55 | func FindByID(ID uint) (*User, error) { 56 | user := &User{} 57 | 58 | if err := Db.First(user, ID).Error; err != nil { 59 | return user, err 60 | } 61 | 62 | return user, nil 63 | 64 | } 65 | 66 | // CreateUser creates a new user 67 | func CreateUser(user User) (uint, error) { 68 | pass, err := common.HashPassword(user.Password) 69 | 70 | if err != nil { 71 | return user.ID, err 72 | } 73 | user.Password = string(pass) 74 | createdUser := Db.Create(&user) 75 | var errMessage = createdUser.Error 76 | 77 | if createdUser.Error != nil { 78 | return user.ID, errMessage 79 | } 80 | return user.ID, nil 81 | } 82 | 83 | func Follow(userID uint, friendID uint) (uint, error) { 84 | 85 | user := User{} 86 | friend, err := FindByID(friendID) 87 | if err != nil { 88 | log.Println(err) 89 | return userID, err 90 | } 91 | Db.Preload("Friends").First(&user, "id = ?", userID) 92 | Db.Model(&user).Association("Friends").Append(friend) 93 | return userID, nil 94 | } 95 | 96 | func Unfollow(userID uint, friendID uint) (uint, error) { 97 | 98 | user := User{} 99 | friend, err := FindByID(friendID) 100 | if err != nil { 101 | log.Println(err) 102 | return userID, err 103 | } 104 | Db.Preload("Friends").First(&user, "id = ?", userID) 105 | Db.Model(&user).Association("Friends").Delete(friend) 106 | return userID, nil 107 | } 108 | 109 | func IsFollower(userID uint, friendID uint) (bool, error) { 110 | 111 | friend := User{} 112 | user := User{} 113 | friend.ID = friendID 114 | user.ID = userID 115 | Db.Model(&user).Association("Friends").Find(&friend) 116 | if friend.Email == "" { 117 | return false, nil 118 | } 119 | return true, nil 120 | } 121 | func CountFollowings(userID uint) (int, error) { 122 | user := User{} 123 | user.ID = userID 124 | count := Db.Model(&user).Association("Friends").Count() 125 | return count, nil 126 | } 127 | func CountFollowers(userID uint) (int, error) { 128 | count := 0 129 | Db.Table("friendships").Select("friend_id").Where("friend_id = ?", userID).Count(&count) 130 | return count, nil 131 | } 132 | -------------------------------------------------------------------------------- /internal/router/auth.route.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/dsa0x/go-social-network/internal/handler" 5 | ) 6 | 7 | func init() { 8 | 9 | APIRouter.HandleFunc("/login", handler.Login).Methods("GET", "POST") 10 | APIRouter.HandleFunc("/logout", handler.Logout).Methods("GET", "POST") 11 | APIRouter.HandleFunc("/signup", handler.SignUp).Methods("GET", "POST") 12 | 13 | } 14 | -------------------------------------------------------------------------------- /internal/router/base.route.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/dsa0x/go-social-network/internal/handler" 5 | "github.com/gorilla/mux" 6 | ) 7 | 8 | // APIRouter is the main router 9 | var APIRouter = mux.NewRouter() 10 | 11 | func init() { 12 | 13 | APIRouter.HandleFunc("/", handler.Auth(handler.Home)).Methods("GET", "POST") 14 | APIRouter.HandleFunc("/guest", handler.Home).Methods("GET", "POST") 15 | 16 | } 17 | -------------------------------------------------------------------------------- /internal/router/post.route.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/dsa0x/go-social-network/internal/handler" 5 | ) 6 | 7 | func init() { 8 | 9 | APIRouter.HandleFunc("/post/create", handler.Auth(handler.CreatePost)).Methods("POST") 10 | APIRouter.HandleFunc("/post/{id}", handler.Auth(handler.DeletePost)).Methods("DELETE") 11 | APIRouter.HandleFunc("/posts", handler.Auth(handler.FetchAllPosts)).Methods("GET") 12 | APIRouter.HandleFunc("/posts/mine", handler.Auth(handler.FetchUserPosts)).Methods("GET") 13 | APIRouter.HandleFunc("/post/{id}", handler.Auth(handler.UpdatePost)).Methods("PUT") 14 | 15 | } 16 | -------------------------------------------------------------------------------- /internal/router/user.route.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/dsa0x/go-social-network/internal/handler" 5 | ) 6 | 7 | func init() { 8 | 9 | APIRouter.HandleFunc("/user/{id}", handler.Auth(handler.GetUser)).Methods("GET", "POST") 10 | APIRouter.HandleFunc("/user/{id}/follow", handler.Auth(handler.FollowUser)).Methods("GET", "POST") 11 | APIRouter.HandleFunc("/user/{id}/unfollow", handler.Auth(handler.UnfollowUser)).Methods("GET", "POST") 12 | APIRouter.HandleFunc("/users", handler.Auth(handler.GetAllUsers)).Methods("GET", "POST") 13 | 14 | } 15 | -------------------------------------------------------------------------------- /internal/views/_posts.html: -------------------------------------------------------------------------------- 1 | {{define "_posts"}} {{$loggedInUserID := print .ID}}
2 | {{$loggedInUserInt := .User.ID}} 3 |
4 | 17 |
18 | {{if eq $loggedInUserID .LoggedInUserId }} {{if .User.Name}} 19 |
{{template "createpost"}}
20 | {{end}} {{end}} {{if .Posts}} {{range .Posts}} 21 | 22 |
25 |
26 | Avatar 31 | 32 | {{.Author}} 33 | {{if eq $loggedInUserInt .CreatedBy }} 34 |
35 | 36 | 37 | 48 | 49 |
50 | {{end}} 51 |
52 |
53 | {{.Content}} 54 |
55 |
56 | 57 | {{end}} {{else}} 58 |
61 | Nothing to see here. Send your first chip dear. Don't be shy :). 62 |
63 | {{end}} 64 |
65 |
66 | {{end}} 67 | -------------------------------------------------------------------------------- /internal/views/createpost.html: -------------------------------------------------------------------------------- 1 | {{define "createpost"}} 2 |
3 | {{if .mismatch}} 4 |
5 |

{{.mismatch}}

6 |
7 | {{end}} 8 |
9 | 13 | 14 | 20 | 27 |
28 |
29 | {{end}} 30 | -------------------------------------------------------------------------------- /internal/views/footer.html: -------------------------------------------------------------------------------- 1 | {{ define "footer" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ end }} -------------------------------------------------------------------------------- /internal/views/header.html: -------------------------------------------------------------------------------- 1 | {{ define "header" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Chipper 9 | 10 | 14 | 18 | 22 | 26 | 30 | 34 | 35 | 36 | 37 | 102 | 103 |
104 | {{ end }} 105 | 106 | 107 | -------------------------------------------------------------------------------- /internal/views/index.html: -------------------------------------------------------------------------------- 1 | {{ if .LoggedInUserId}} {{template "header" .LoggedInUserId }} {{else}} 2 | {{template "header" }} {{end}} {{template "_posts" . }} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{template "footer"}} 12 | -------------------------------------------------------------------------------- /internal/views/login.html: -------------------------------------------------------------------------------- 1 | {{template "header" }} 2 |
3 | 74 |
75 | 76 | {{template "footer" }} 77 | -------------------------------------------------------------------------------- /internal/views/profile.html: -------------------------------------------------------------------------------- 1 | {{template "header" .LoggedInUserId }} 2 | 3 |
4 |
7 | Profile Header 12 | 13 |
14 | {{if .Avatar}} 15 | Avatar 16 | {{end}} 17 |
18 |

{{ .Username }}

19 |
20 | 21 |
22 | {{if (and (not .MyProfile) (not .IsFollower)) }} 23 | 29 | {{end}} {{if (and (not .MyProfile) (.IsFollower)) }} 30 | 37 | {{end}} 38 |
39 |
42 | {{.PostCount}} 43 | Chips 44 |
45 |
48 | {{.Followers}} 49 | Followers 50 |
51 |
52 | {{.Following}} 53 | Following 54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {{template "_posts" . }} 62 |
63 | {{template "footer" }} 64 | -------------------------------------------------------------------------------- /internal/views/signup.html: -------------------------------------------------------------------------------- 1 | {{template "header" }} 2 |
3 | 81 |
82 | 83 | {{template "footer" }} 84 | -------------------------------------------------------------------------------- /internal/views/users.html: -------------------------------------------------------------------------------- 1 | {{template "header" .LoggedInUserId }} 2 | 3 |
6 | {{range .Users}} {{ $path := printf "%s/%d" "/user" .ID }} 7 | 11 |
12 | Avatar 17 |
18 |
21 | {{.UserName}} 22 |
23 |
24 | {{end}} 25 |
26 | {{template "footer" }} 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/dsa0x/go-social-network/internal/router" 8 | ) 9 | 10 | func main() { 11 | port := "8080" 12 | staticDir := "/public/" 13 | router.APIRouter. 14 | PathPrefix(staticDir). 15 | Handler(http.StripPrefix(staticDir, http.FileServer(http.Dir("."+staticDir)))) 16 | log.Println("Listening on :8080...") 17 | http.ListenAndServe(":"+port, router.APIRouter) 18 | } 19 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsa0x/go-social-network/dd89c73564495414639dd3b6a2b05c0e54d881c8/public/.DS_Store -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100vh; 3 | min-width: 420px; 4 | } 5 | 6 | .profile-img { 7 | bottom: -2rem; 8 | padding-left: 0; 9 | } 10 | 11 | @media (min-width: 768px) { 12 | .profile-img { 13 | /* padding: 4rem; */ 14 | } 15 | } 16 | @media (max-width: 550px) { 17 | .uname { 18 | top: -50px; 19 | position: absolute; 20 | right: 60px; 21 | } 22 | } 23 | 24 | .users-name { 25 | text-transform: capitalize; 26 | } 27 | 28 | .users { 29 | width: 500px; 30 | } 31 | 32 | @media (max-width: 768px) { 33 | .profile-stats { 34 | flex-direction: column; 35 | align-items: center; 36 | transform: translateY(-25px); 37 | } 38 | .profile-stats > div { 39 | padding-top: 10px; 40 | } 41 | } 42 | 43 | button.follow-btn { 44 | width: 6rem; 45 | } 46 | 47 | button.follow-btn:hover a { 48 | display: none; 49 | } 50 | button.follow-btn a.unfollow { 51 | display: none; 52 | } 53 | button.follow-btn:hover a.unfollow { 54 | display: block; 55 | } 56 | 57 | textarea { 58 | min-height: 100px; 59 | } 60 | 61 | .sidebar img { 62 | top: -1rem; 63 | /* max-height: 100vh; */ 64 | } 65 | 66 | .sidebar { 67 | width: 260px; 68 | } 69 | 70 | main { 71 | width: 100%; 72 | margin: auto; 73 | } 74 | 75 | @media (min-width: 768px) { 76 | main { 77 | width: 800px; 78 | margin: auto; 79 | } 80 | } 81 | /* button.follow-btn:hover::before span { 82 | content: "Unfollow"; 83 | } */ 84 | 85 | /* button.follow-btn ::before { 86 | content: "Unfollow"; 87 | } */ 88 | -------------------------------------------------------------------------------- /public/img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsa0x/go-social-network/dd89c73564495414639dd3b6a2b05c0e54d881c8/public/img/.DS_Store -------------------------------------------------------------------------------- /public/img/chat_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | messages_chat [#1557] 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/img/delete.svg: -------------------------------------------------------------------------------- 1 | x -------------------------------------------------------------------------------- /public/img/guy_standing.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/profile_header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsa0x/go-social-network/dd89c73564495414639dd3b6a2b05c0e54d881c8/public/img/profile_header.jpg -------------------------------------------------------------------------------- /public/img/profile_pic.svg: -------------------------------------------------------------------------------- 1 | profile pic -------------------------------------------------------------------------------- /public/js/index.js: -------------------------------------------------------------------------------- 1 | async function postData(url = "", method, data = {}) { 2 | // Default options are marked with * 3 | const response = await fetch(url, { 4 | method: method, // *GET, POST, PUT, DELETE, etc. 5 | mode: "cors", // no-cors, *cors, same-origin 6 | cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached 7 | credentials: "same-origin", // include, *same-origin, omit 8 | headers: { 9 | "Content-Type": "application/json", 10 | // 'Content-Type': 'application/x-www-form-urlencoded', 11 | }, 12 | redirect: "follow", // manual, *follow, error 13 | referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url 14 | body: JSON.stringify(data), // body data type must match "Content-Type" header 15 | }); 16 | return response.json(); // parses JSON response into native JavaScript objects 17 | } 18 | 19 | const followBtn = document 20 | .querySelector(".followBtn") 21 | .addEventListener("click", (e) => { 22 | e.preventDefault(); 23 | postData("/follow", "POST").then((data) => { 24 | console.log(data); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /public/test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsa0x/go-social-network/dd89c73564495414639dd3b6a2b05c0e54d881c8/public/test.txt --------------------------------------------------------------------------------