From 4f0834c3a0e6cbfe868478053ebadabed48f50ca Mon Sep 17 00:00:00 2001 From: Asara Date: Sat, 13 Apr 2019 23:38:51 -0400 Subject: [PATCH] Why do double the work eh? --- README.md | 74 ++++++++++++++++++++++++-- db_reset.sh | 16 ++++++ packages/auth/auth.go | 97 ++++++++++++++++++++++++++++++----- packages/database/database.go | 5 +- packages/users/users.go | 40 ++++++++++----- settings/db.env | 4 ++ settings/db.env-sample | 4 ++ settings/secrets.env | 1 + settings/secrets.env-sample | 1 + settings/website.env | 2 + settings/website.env-sample | 2 + 11 files changed, 213 insertions(+), 33 deletions(-) create mode 100755 db_reset.sh create mode 100644 settings/db.env create mode 100644 settings/db.env-sample create mode 100644 settings/secrets.env create mode 100644 settings/secrets.env-sample create mode 100644 settings/website.env create mode 100644 settings/website.env-sample diff --git a/README.md b/README.md index f332b11..ae8c585 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,74 @@ # sudoscientist -Sudo Scientist blog +## sudoscientist blog -API_PORT=8080 DBHOST="postgres.localhost" DBPORT="5432" DBUSER="asara" DBPW="PW" DBNAME="sudoscientist" \ -go run main.go +### Setup + +Install steps are for Debian 9 (stretch) + +1. Install docker-ce + ``` + # stolen from https://docs.docker.com/install/linux/docker-ce/debian/ + sudo apt-get remove docker docker-engine docker.io containerd runc + sudo apt-get update + sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common + curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add - + # verify the key's fingerprint + # ---------- + sudo apt-key fingerprint 0EBFCD88 + pub 4096R/0EBFCD88 2017-02-22 + Key fingerprint = 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88 + Docker Release (CE deb) + sub 4096R/F273FCD8 2017-02-22 + # ---------- + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" + sudo apt-get update + sudo apt-get install docker-ce docker-ce-cli containerd.io + ``` + +2. Install golang 1.11 + ``` + # stretch doesn't have the latest golang so we install backports + sudo add-apt-repository "deb http://deb.debian.org/debian stretch-backports main" + sudo apt-get update + sudo apt-get -t stretch-backports install golang + # set the gopath manually for the rest of the setup + export GOPATH=${HOME}/go + ``` + +3. Clone repo and configure the settings + ``` + mkdir -p ${GOPATH}/src/git.minhas.io/asara + cd ${GOPATH}/src/git.minhas.io/asara + git clone https://git.minhas.io/asara/sudoscientist + # iterate through the environment files in the settings directory and set them appropriately + # make sure the extension is .env (db.env, secrets.env, website.env... etc.) + ``` + +4. Configure docker postgres for testing + ``` + # make sure your user is in the docker group + sudo usermod -aG docker $(whoami) + # make sure you have some postgres client installed + sudo apt-get install postgres-client + docker pull postgres + docker run --name sudosci-db -e POSTGRES_PASWORD=${DB_ADMIN_PW} -d postgres # please set the db admin pw manually + # Initalize the postgres DB + cd ${GOPATH}/src/git.minhas.io/asara/sudoscientist + for i in settings/*; do source $i; done + export DB_HOST=$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" sudosci-db) + psql -d postgres -U postgres -h ${DB_HOST} << EOF + CREATE DATABASE ${DB_NAME}; + CREATE USER ${DB_USER} WITH ENCRYPTED PASSWORD '${DB_PW}'; + GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER}; + EOF + ``` + +5. Run the application! + ``` + cd ${GOPATH}/src/git.minhas.io/asara/sudoscientist + for i in settings/*; do source $i; done + export DB_HOST=$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" sudosci-db) + go get + go run main.go + ``` diff --git a/db_reset.sh b/db_reset.sh new file mode 100755 index 0000000..0cb3f74 --- /dev/null +++ b/db_reset.sh @@ -0,0 +1,16 @@ +#!/bin/bash +docker stop sudosci-db +docker rm sudosci-db +export DB_ADMIN_PW=test +docker run --name sudosci-db -e POSTGRES_PASWORD=${DB_ADMIN_PW} -d postgres +cd ${GOPATH}/src/git.minhas.io/asara/sudoscientist +for i in settings/*; do source $i; done +export DB_HOST=$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" sudosci-db) +# instance takes a while to setup and configure postgis +sleep 10 +psql -d postgres -U postgres -h ${DB_HOST} << EOF +CREATE DATABASE ${DB_NAME}; +CREATE USER ${DB_USER} WITH ENCRYPTED PASSWORD '${DB_PW}'; +GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER}; +EOF +echo Done diff --git a/packages/auth/auth.go b/packages/auth/auth.go index f3803b9..1b10da4 100644 --- a/packages/auth/auth.go +++ b/packages/auth/auth.go @@ -1,16 +1,18 @@ package auth import ( - "fmt" "database/sql" "encoding/json" + "fmt" "git.minhas.io/asara/sudoscientist/packages/users" + "github.com/badoux/checkmail" "github.com/dgrijalva/jwt-go" "github.com/go-chi/chi" "github.com/go-chi/jwtauth" "github.com/go-chi/render" "golang.org/x/crypto/bcrypt" "net/http" + "time" ) var ( @@ -18,13 +20,32 @@ var ( TokenAuth *jwtauth.JWTAuth ) -type Credentials struct { +type RegistrationError struct { + Message string `json:"error"` +} + +type SignUpCredentials struct { + Username string `json:"username", db:"username"` + Email string `json:"email", db:"email"` + Password string `json:"password", db:"password"` +} + +type SignInCredentials struct { Username string `json:"username", db:"username"` Password string `json:"password", db:"password"` } +type Claims struct { + Username string `json:"username"` + jwt.StandardClaims +} + +type ReturnToken struct { + JWT string `json:"jwt"` +} + func Init() { - DB.Exec("CREATE TABLE IF NOT EXISTS users (username text primary key, password text, admin boolean);" ) + DB.Exec("CREATE TABLE IF NOT EXISTS users (username text primary key, email text, password text, admin boolean);") } func Routes() *chi.Mux { @@ -35,26 +56,66 @@ func Routes() *chi.Mux { } func signup(w http.ResponseWriter, r *http.Request) { - creds := &Credentials{} + returnError := RegistrationError{} + creds := &SignUpCredentials{} err := json.NewDecoder(r.Body).Decode(creds) if err != nil { w.WriteHeader(http.StatusBadRequest) + fmt.Println(err) return } + if creds.Username == "" { + returnError.Message = "username is required" + w.WriteHeader(http.StatusBadRequest) + render.JSON(w, r, returnError) + return + } + if creds.Password == "" { + returnError.Message = "password is required" + w.WriteHeader(http.StatusBadRequest) + render.JSON(w, r, returnError) + return + } + if creds.Email == "" { + returnError.Message = "email is required" + w.WriteHeader(http.StatusBadRequest) + render.JSON(w, r, returnError) + return + } + err = checkmail.ValidateFormat(creds.Email) + if err != nil { + returnError.Message = "email not valid" + w.WriteHeader(http.StatusBadRequest) + render.JSON(w, r, returnError) + return + } + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(creds.Password), 10) - s := `INSERT INTO users (username, password, admin) - VALUES ($1, $2, $3)` - if _, err = DB.Exec(s, creds.Username, string(hashedPassword), false); err != nil { + s := `INSERT INTO users (username, email, password, admin) + VALUES ($1, $2, $3, $4)` + if _, err = DB.Exec(s, creds.Username, creds.Email, string(hashedPassword), false); err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Println(err) return } - users.CreateProfile(creds.Username) + users.CreateProfile(creds.Username, creds.Email) w.WriteHeader(http.StatusCreated) + expirationTime := time.Now().Add(6 * time.Hour) + claims := &Claims{ + Username: creds.Username, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expirationTime.Unix(), + }, + } + _, tokenString, _ := TokenAuth.Encode(claims) + token := ReturnToken{ + JWT: tokenString, + } + render.JSON(w, r, token) } func signin(w http.ResponseWriter, r *http.Request) { - creds := &Credentials{} + creds := &SignInCredentials{} err := json.NewDecoder(r.Body).Decode(creds) if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -65,7 +126,7 @@ func signin(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) return } - storedCreds := &Credentials{} + storedCreds := &SignInCredentials{} err = result.Scan(&storedCreds.Password) if err != nil { if err == sql.ErrNoRows { @@ -78,9 +139,17 @@ func signin(w http.ResponseWriter, r *http.Request) { if err = bcrypt.CompareHashAndPassword([]byte(storedCreds.Password), []byte(creds.Password)); err != nil { w.WriteHeader(http.StatusUnauthorized) } - _, tokenString, _ := TokenAuth.Encode(jwt.MapClaims{ - "username": creds.Username, - }) w.WriteHeader(http.StatusOK) - render.JSON(w, r, tokenString) + expirationTime := time.Now().Add(5 * time.Hour) + claims := &Claims{ + Username: creds.Username, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expirationTime.Unix(), + }, + } + _, tokenString, _ := TokenAuth.Encode(claims) + token := ReturnToken{ + JWT: tokenString, + } + render.JSON(w, r, token) } diff --git a/packages/database/database.go b/packages/database/database.go index 4e74e45..48eefc2 100644 --- a/packages/database/database.go +++ b/packages/database/database.go @@ -1,12 +1,13 @@ package database + import ( + "database/sql" "fmt" "os" - "database/sql" ) func NewDB() (*sql.DB, error) { - psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", os.Getenv("DBHOST"), os.Getenv("DBPORT"), os.Getenv("DBUSER"), os.Getenv("DBPW"), os.Getenv("DBNAME")) + psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"), os.Getenv("DB_PW"), os.Getenv("DB_NAME"), os.Getenv("DB_SSL")) db, err := sql.Open("postgres", psqlInfo) if err != nil { panic(err) diff --git a/packages/users/users.go b/packages/users/users.go index aef3b13..c65c157 100644 --- a/packages/users/users.go +++ b/packages/users/users.go @@ -1,8 +1,8 @@ package users import ( - "fmt" "database/sql" + "fmt" "github.com/go-chi/chi" "github.com/go-chi/jwtauth" "github.com/go-chi/render" @@ -15,9 +15,10 @@ var ( ) type User struct { - Username string `json:"username,string"` - Country string `json:"country,string"` - Bio string `json:"bio,string"` + Username string `json:"username"` + Email string `json:"email"` + Country string `json:"country"` + Bio string `json:"bio"` } func Init() { @@ -25,6 +26,7 @@ func Init() { CREATE TABLE IF NOT EXISTS user_profiles (id SERIAL PRIMARY KEY, username text REFERENCES users (username), + email text, country text, bio text)` DB.Exec(dbCreateStatement) @@ -35,29 +37,39 @@ func Routes() *chi.Mux { r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(TokenAuth)) r.Use(jwtauth.Authenticator) - r.Get("/{username}", getUser) + r.Put("/{username}", updateUser) }) - r.Post("/{username}", updateUser) + r.Get("/{username}", getUser) return r } func getUser(w http.ResponseWriter, r *http.Request) { username := chi.URLParam(r, "username") - user := User{ - Username: username, + result := DB.QueryRow("SELECT username, email, country, bio FROM user_profiles WHERE username=$1", username) + user := User{} + err := result.Scan(&user.Username, &user.Email, &user.Country, &user.Bio) + fmt.Println(err) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return } render.JSON(w, r, user) } func updateUser(w http.ResponseWriter, r *http.Request) { + _, claims, _ := jwtauth.FromContext(r.Context()) + username := claims["username"] + searchname := chi.URLParam(r, "username") + if username != searchname { + w.WriteHeader(http.StatusUnauthorized) + return + } return } -func CreateProfile(username string) { - fmt.Println("CREATING PROFILE") +func CreateProfile(username string, email string) { blankProfileStatement := ` - INSERT INTO user_profiles (username, country, bio) - VALUES ($1, $2, $3)` - DB.Exec(blankProfileStatement, username, "", "") - fmt.Println("CREATED") + INSERT INTO user_profiles (username, email, country, bio) + VALUES ($1, $2, $3, $4)` + DB.Exec(blankProfileStatement, username, email, "", "") } diff --git a/settings/db.env b/settings/db.env new file mode 100644 index 0000000..ef6bef0 --- /dev/null +++ b/settings/db.env @@ -0,0 +1,4 @@ +export DB_PORT="5432" +export DB_USER="sudosci" +export DB_NAME="sudosci" +export DB_SSL="disable" diff --git a/settings/db.env-sample b/settings/db.env-sample new file mode 100644 index 0000000..ef6bef0 --- /dev/null +++ b/settings/db.env-sample @@ -0,0 +1,4 @@ +export DB_PORT="5432" +export DB_USER="sudosci" +export DB_NAME="sudosci" +export DB_SSL="disable" diff --git a/settings/secrets.env b/settings/secrets.env new file mode 100644 index 0000000..6b6346f --- /dev/null +++ b/settings/secrets.env @@ -0,0 +1 @@ +export DB_PW="CHANGEME" diff --git a/settings/secrets.env-sample b/settings/secrets.env-sample new file mode 100644 index 0000000..6b6346f --- /dev/null +++ b/settings/secrets.env-sample @@ -0,0 +1 @@ +export DB_PW="CHANGEME" diff --git a/settings/website.env b/settings/website.env new file mode 100644 index 0000000..26a28d8 --- /dev/null +++ b/settings/website.env @@ -0,0 +1,2 @@ +export API_PORT="8080" +export JWT_SECRET="CHANGEMEALSO" diff --git a/settings/website.env-sample b/settings/website.env-sample new file mode 100644 index 0000000..26a28d8 --- /dev/null +++ b/settings/website.env-sample @@ -0,0 +1,2 @@ +export API_PORT="8080" +export JWT_SECRET="CHANGEMEALSO"