├── vendor └── .gitignore ├── CONTRIBUTORS ├── model └── skel_full.mwb ├── static ├── images │ ├── home.png │ ├── user.png │ ├── cache.png │ ├── reset.png │ ├── signin.png │ ├── signup.png │ └── caution.png └── offline.html ├── ui ├── home.html ├── index.html ├── style.html ├── reset.html ├── navbar.html ├── validate.html ├── signin.html ├── signup.html ├── user.html └── head.html ├── ui_home.go ├── database.sql ├── config.json ├── CHANGES ├── database.go ├── ui_signup.go ├── ui_user.go ├── ui_signin.go ├── ui_reset.go ├── ui_reset_random.go ├── db_user.go ├── db_user_sha1.go ├── db_user_salt_sha1.go ├── main.go ├── README.md └── LICENSE /vendor/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | geosoft1@gmail.com 2 | -------------------------------------------------------------------------------- /model/skel_full.mwb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geosoft1/ssas/HEAD/model/skel_full.mwb -------------------------------------------------------------------------------- /static/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geosoft1/ssas/HEAD/static/images/home.png -------------------------------------------------------------------------------- /static/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geosoft1/ssas/HEAD/static/images/user.png -------------------------------------------------------------------------------- /static/images/cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geosoft1/ssas/HEAD/static/images/cache.png -------------------------------------------------------------------------------- /static/images/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geosoft1/ssas/HEAD/static/images/reset.png -------------------------------------------------------------------------------- /static/images/signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geosoft1/ssas/HEAD/static/images/signin.png -------------------------------------------------------------------------------- /static/images/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geosoft1/ssas/HEAD/static/images/signup.png -------------------------------------------------------------------------------- /static/images/caution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geosoft1/ssas/HEAD/static/images/caution.png -------------------------------------------------------------------------------- /ui/home.html: -------------------------------------------------------------------------------- 1 | [[define "home"]] 2 | 3 | 4 | 5 | 6 | [[template "head"]] 7 | 8 | 9 | [[template "navbar" .]] 10 | 11 | 12 | [[end]] -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | [[define "index"]] 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | [[end]] -------------------------------------------------------------------------------- /ui_home.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func home(w http.ResponseWriter, r *http.Request) { 8 | session, _ := store.Get(r, SESSID) 9 | user := session.Values["user"].(User) 10 | templ.ExecuteTemplate(w, "home", user) 11 | } 12 | -------------------------------------------------------------------------------- /static/offline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

¯\_(ツ)_/¯

9 |

503 Service Unavailable

10 |
11 | 12 | -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `skel`.`user` ( 2 | `user_id` INT NOT NULL AUTO_INCREMENT, 3 | `name` VARCHAR(45) NULL, 4 | `email` VARCHAR(50) NULL, 5 | `password` VARCHAR(128) NULL, 6 | `active` TINYINT(1) NULL DEFAULT 1, 7 | PRIMARY KEY (`user_id`), 8 | UNIQUE INDEX `email_UNIQUE` (`email` ASC)) 9 | ENGINE = InnoDB -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "ip": "example.com", 4 | "port": 3306, 5 | "user": "root", 6 | "password": "password", 7 | "name": "skel" 8 | }, 9 | "smtp": "smtp.gmail.com", 10 | "port": 587, 11 | "user": "mailer@gmail.com", 12 | "password": "password", 13 | "name": "mailer name" 14 | } 15 | -------------------------------------------------------------------------------- /ui/style.html: -------------------------------------------------------------------------------- 1 | [[define "style"]] 2 | 3 | 10 | [[end]] -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 1.0.0-release-build230419 2 | first release 3 | 4 | 1.1.0-release-build300419 5 | - salted and hashed passwords in database 6 | - send random recovery passwords on mail 7 | 8 | 1.1.1-release-build030519 9 | - reset the password to random only if the mail was successfully sent 10 | 11 | 1.1.2-release-build160619 12 | - sticky navbar in ui 13 | 14 | 1.2.0-release-build290619 15 | - add bootstrap support 16 | 17 | 1.2.1-release-build160719 18 | - add vendor folder 19 | - navbar add w3-margin-bottom 20 | - head add shortcut icon -------------------------------------------------------------------------------- /ui/reset.html: -------------------------------------------------------------------------------- 1 | [[define "reset"]] 2 | 3 | 4 | 5 | 6 | [[template "head"]] 7 | 8 | 9 |
10 |
11 |

Forgot password

12 |

13 |

Back 14 | 15 |

16 | 17 | 18 | [[end]] -------------------------------------------------------------------------------- /ui/navbar.html: -------------------------------------------------------------------------------- 1 | [[define "navbar"]] 2 |
3 |
4 | 5 | 6 | 7 | Sign out 8 | [[.Name]] 9 |
10 |
11 | [[end]] -------------------------------------------------------------------------------- /ui/validate.html: -------------------------------------------------------------------------------- 1 | [[define "validate_password"]] 2 | 16 | [[end]] -------------------------------------------------------------------------------- /ui/signin.html: -------------------------------------------------------------------------------- 1 | [[define "signin"]] 2 | 3 | 4 | 5 | 6 | [[template "head"]] 7 | 8 | 9 |
10 |

Sign in

11 |
12 |

13 |

14 |

15 |

Sign up | Forgot password 16 |

17 |
18 | 19 | 20 | [[end]] -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | var db *sql.DB 12 | 13 | // Connect to database server. 14 | // https://github.com/go-sql-driver/mysql#timetime-support 15 | // https://stackoverflow.com/a/52895312 16 | func sqlConnect() { 17 | var err error 18 | if db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true&timeout=5s", config.Database.User, config.Database.Password, config.Database.Ip, config.Database.Port, config.Database.Name)); err != nil { 19 | log.Fatalln(err) 20 | } 21 | if err := db.Ping(); err != nil { 22 | log.Fatalln(err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui_signup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func signup(w http.ResponseWriter, r *http.Request) { 8 | switch r.Method { 9 | case "GET": 10 | templ.ExecuteTemplate(w, "signup", nil) 11 | case "POST": 12 | var user User 13 | user.Name = r.FormValue("name") 14 | user.Email = r.FormValue("email") 15 | user.Password = r.FormValue("password") 16 | if err := sqlInsert(&user); err != nil { 17 | http.Error(w, http.StatusText(http.StatusNotAcceptable), http.StatusNotAcceptable) 18 | return 19 | } 20 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 21 | w.Write([]byte("User created, sign in")) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui_user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func user(w http.ResponseWriter, r *http.Request) { 8 | session, _ := store.Get(r, SESSID) 9 | user := session.Values["user"].(User) 10 | switch r.Method { 11 | case "GET": 12 | templ.ExecuteTemplate(w, "user", user) 13 | case "POST": 14 | switch r.FormValue("submit") { 15 | case "save": 16 | user.Name = r.FormValue("name") 17 | user.Password = r.FormValue("password") 18 | if err := sqlUpdateUser(&user); err != nil { 19 | http.Error(w, http.StatusText(http.StatusNotAcceptable), http.StatusNotAcceptable) 20 | return 21 | } 22 | case "delete": 23 | sqlDeleteUser(&user) 24 | } 25 | signout(w, r) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/signup.html: -------------------------------------------------------------------------------- 1 | [[define "signup"]] 2 | 3 | 4 | 5 | 6 | [[template "head"]] 7 | 8 | 9 |
10 |

Sign up

11 |
12 |

13 |

14 |

15 |

16 |

Back 17 |

18 |
19 | [[template "validate_password"]] 20 | 21 | 22 | [[end]] -------------------------------------------------------------------------------- /ui_signin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func signin(w http.ResponseWriter, r *http.Request) { 8 | switch r.Method { 9 | case "GET": 10 | if err, n := sqlUserCount(); err != nil || n == 0 { 11 | http.Redirect(w, r, "/signup", http.StatusSeeOther) 12 | return 13 | } 14 | templ.ExecuteTemplate(w, "signin", nil) 15 | case "POST": 16 | var user User 17 | user.Email = r.FormValue("email") 18 | user.Password = r.FormValue("password") 19 | if err := sqlAuthenticateUser(&user); err != nil || !user.isActive { 20 | http.Redirect(w, r, "/", http.StatusSeeOther) 21 | return 22 | } 23 | session, _ := store.Get(r, SESSID) 24 | session.Values["user"] = user 25 | session.Values["authenticated"] = true 26 | session.Save(r, w) 27 | http.Redirect(w, r, "/a/home", http.StatusSeeOther) 28 | } 29 | } 30 | 31 | func signout(w http.ResponseWriter, r *http.Request) { 32 | session, _ := store.Get(r, SESSID) 33 | session.Values["authenticated"] = false 34 | session.Save(r, w) 35 | http.Redirect(w, r, "/", http.StatusSeeOther) 36 | } 37 | -------------------------------------------------------------------------------- /ui/user.html: -------------------------------------------------------------------------------- 1 | [[define "user"]] 2 | 3 | 4 | 5 | 6 | [[template "head"]] 7 | 8 | 9 | [[template "navbar" .]] 10 |
11 |
12 |

Edit user

13 |
14 |

15 |

16 |

17 |

18 |

19 |
20 |

Danger zone!

21 |
22 |

I understand this will delete my account 23 |

24 |

25 |
26 |
27 | [[template "validate_password"]] 28 | 29 | 30 | [[end]] -------------------------------------------------------------------------------- /ui_reset.go: -------------------------------------------------------------------------------- 1 | // +build plain 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | 9 | gomail "gopkg.in/gomail.v2" 10 | ) 11 | 12 | func reset(w http.ResponseWriter, r *http.Request) { 13 | switch r.Method { 14 | case "GET": 15 | templ.ExecuteTemplate(w, "reset", nil) 16 | case "POST": 17 | var user User 18 | user.Email = r.FormValue("email") 19 | if err := sqlGetUser(&user); err != nil { 20 | http.Error(w, http.StatusText(http.StatusNotAcceptable), http.StatusNotAcceptable) 21 | return 22 | } 23 | // https://stackoverflow.com/a/24431749 24 | mail := gomail.NewMessage() 25 | mail.SetAddressHeader("From", config.SMTP.User, config.SMTP.Name) 26 | mail.SetAddressHeader("To", r.FormValue("email"), "") 27 | mail.SetHeader("Subject", "Check your password request") 28 | mail.SetBody("text/html", user.Password) 29 | dialer := gomail.NewPlainDialer(config.SMTP.Server, config.SMTP.Port, config.SMTP.User, config.SMTP.Password) 30 | //dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true} 31 | if err := dialer.DialAndSend(mail); err != nil { 32 | log.Println(err) 33 | } 34 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 35 | w.Write([]byte("A mail with instructions was send, read and sign in")) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ui_reset_random.go: -------------------------------------------------------------------------------- 1 | // +build random 2 | 3 | package main 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/geosoft1/token" 9 | 10 | gomail "gopkg.in/gomail.v2" 11 | ) 12 | 13 | func reset(w http.ResponseWriter, r *http.Request) { 14 | switch r.Method { 15 | case "GET": 16 | templ.ExecuteTemplate(w, "reset", nil) 17 | case "POST": 18 | var user User 19 | user.Email = r.FormValue("email") 20 | if err := sqlGetUser(&user); err != nil { 21 | http.Error(w, http.StatusText(http.StatusNotAcceptable), http.StatusNotAcceptable) 22 | return 23 | } 24 | // generate a random password 25 | user.Password = token.GetToken(token_len) 26 | // https://stackoverflow.com/a/24431749 27 | mail := gomail.NewMessage() 28 | mail.SetAddressHeader("From", config.SMTP.User, config.SMTP.Name) 29 | mail.SetAddressHeader("To", r.FormValue("email"), "") 30 | mail.SetHeader("Subject", "Check your password request") 31 | mail.SetBody("text/html", user.Password) 32 | dialer := gomail.NewPlainDialer(config.SMTP.Server, config.SMTP.Port, config.SMTP.User, config.SMTP.Password) 33 | //dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true} 34 | if err := dialer.DialAndSend(mail); err != nil { 35 | http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 36 | return 37 | } 38 | // reset the password to random only if the mail was successfully sent 39 | if err := sqlUpdateUser(&user); err != nil { 40 | http.Error(w, http.StatusText(http.StatusNotAcceptable), http.StatusNotAcceptable) 41 | return 42 | } 43 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 44 | w.Write([]byte("A mail with instructions was send, read and sign in")) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /db_user.go: -------------------------------------------------------------------------------- 1 | // +build plain 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | // NOTE user management 12 | 13 | // Count users from database. 14 | func sqlUserCount() (error, int) { 15 | var n int 16 | if err := db.QueryRow("SELECT COUNT(*) from user").Scan(&n); err != nil { 17 | log.Println(err) 18 | return err, 0 19 | } 20 | return nil, n 21 | } 22 | 23 | // Get a user identifyed by email and password. 24 | func sqlAuthenticateUser(u *User) error { 25 | if err := db.QueryRow("SELECT * FROM user WHERE email=? AND password=?", u.Email, u.Password).Scan(&u.Id, &u.Name, &u.Email, &u.Password, &u.isActive); err != nil { 26 | log.Println(err) 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | // Get the user identifyed by email. 33 | func sqlGetUser(u *User) error { 34 | if err := db.QueryRow("SELECT * FROM user WHERE email=?", u.Email).Scan(&u.Id, &u.Name, &u.Email, &u.Password, &u.isActive); err != nil { 35 | log.Println(err) 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | // Create new user identifyed by name, email and password. 42 | func sqlInsert(u *User) error { 43 | if _, err := db.Exec("INSERT user SET name=?, email=?, password=?", &u.Name, &u.Email, &u.Password); err != nil { 44 | log.Println(err) 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | // Update a user identifyed by email and password. 51 | func sqlUpdateUser(u *User) error { 52 | if _, err := db.Exec("UPDATE user SET name=?, password=? WHERE email=?", u.Name, u.Password, u.Email); err != nil { 53 | log.Println(err) 54 | return err 55 | } 56 | return nil 57 | } 58 | 59 | // Delete a user identifyed by email. 60 | func sqlDeleteUser(u *User) error { 61 | if _, err := db.Exec("DELETE FROM user WHERE email=?", u.Email); err != nil { 62 | log.Println(err) 63 | return err 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /db_user_sha1.go: -------------------------------------------------------------------------------- 1 | // +build sha1 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | // NOTE user management 12 | 13 | // Count users from database. 14 | func sqlUserCount() (error, int) { 15 | var n int 16 | if err := db.QueryRow("SELECT COUNT(*) from user").Scan(&n); err != nil { 17 | log.Println(err) 18 | return err, 0 19 | } 20 | return nil, n 21 | } 22 | 23 | // Get a user identifyed by email and password. 24 | func sqlAuthenticateUser(u *User) error { 25 | if err := db.QueryRow("SELECT * FROM user WHERE email=? AND password=SHA1(?)", u.Email, u.Password).Scan(&u.Id, &u.Name, &u.Email, &u.Password, &u.isActive); err != nil { 26 | log.Println(err) 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | // Get the user identifyed by email. 33 | func sqlGetUser(u *User) error { 34 | if err := db.QueryRow("SELECT * FROM user WHERE email=?", u.Email).Scan(&u.Id, &u.Name, &u.Email, &u.Password, &u.isActive); err != nil { 35 | log.Println(err) 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | // Create new user identifyed by name, email and password. 42 | func sqlInsert(u *User) error { 43 | if _, err := db.Exec("INSERT user SET name=?, email=?, password=SHA1(?)", &u.Name, &u.Email, &u.Password); err != nil { 44 | log.Println(err) 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | // Update a user identifyed by email and password. 51 | func sqlUpdateUser(u *User) error { 52 | if _, err := db.Exec("UPDATE user SET name=?, password=SHA1(?) WHERE email=?", u.Name, u.Password, u.Email); err != nil { 53 | log.Println(err) 54 | return err 55 | } 56 | return nil 57 | } 58 | 59 | // Delete a user identifyed by email. 60 | func sqlDeleteUser(u *User) error { 61 | if _, err := db.Exec("DELETE FROM user WHERE email=?", u.Email); err != nil { 62 | log.Println(err) 63 | return err 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /db_user_salt_sha1.go: -------------------------------------------------------------------------------- 1 | // +build saltsha1 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | // NOTE user management 12 | 13 | // Count users from database. 14 | func sqlUserCount() (error, int) { 15 | var n int 16 | if err := db.QueryRow("SELECT COUNT(*) from user").Scan(&n); err != nil { 17 | log.Println(err) 18 | return err, 0 19 | } 20 | return nil, n 21 | } 22 | 23 | // Get a user identifyed by email and password. 24 | func sqlAuthenticateUser(u *User) error { 25 | if err := db.QueryRow("SELECT * FROM user WHERE email=? AND password=SHA1(?)", u.Email, u.Password+salt).Scan(&u.Id, &u.Name, &u.Email, &u.Password, &u.isActive); err != nil { 26 | log.Println(err) 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | // Get the user identifyed by email. 33 | func sqlGetUser(u *User) error { 34 | if err := db.QueryRow("SELECT * FROM user WHERE email=?", u.Email).Scan(&u.Id, &u.Name, &u.Email, &u.Password, &u.isActive); err != nil { 35 | log.Println(err) 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | // Create new user identifyed by name, email and password. 42 | func sqlInsert(u *User) error { 43 | u.Password += salt 44 | if _, err := db.Exec("INSERT user SET name=?, email=?, password=SHA1(?)", &u.Name, &u.Email, &u.Password); err != nil { 45 | log.Println(err) 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | // Update a user identifyed by email and password. 52 | func sqlUpdateUser(u *User) error { 53 | if _, err := db.Exec("UPDATE user SET name=?, password=SHA1(?) WHERE email=?", u.Name, u.Password+salt, u.Email); err != nil { 54 | log.Println(err) 55 | return err 56 | } 57 | return nil 58 | } 59 | 60 | // Delete a user identifyed by email. 61 | func sqlDeleteUser(u *User) error { 62 | if _, err := db.Exec("DELETE FROM user WHERE email=?", u.Email); err != nil { 63 | log.Println(err) 64 | return err 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /ui/head.html: -------------------------------------------------------------------------------- 1 | [[define "head"]] 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | [[template "style"]] 29 | 30 | 31 | [[end]] 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/gob" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "text/template" 14 | 15 | "github.com/geosoft1/json" 16 | "github.com/gorilla/mux" 17 | "github.com/gorilla/sessions" 18 | ) 19 | 20 | // TODO application version 21 | // https://semver.org/#semantic-versioning-200 22 | const SW_VERSION = "1.2.0-release-build290619" 23 | 24 | // NOTE session cookie name 25 | const SESSID = "SESSID" 26 | 27 | // NOTE cache manifest file 28 | const MANIFEST = `CACHE MANIFEST 29 | FALLBACK: 30 | / /static/offline.html 31 | ` 32 | 33 | type User struct { 34 | Id int `json:"id"` 35 | Name string `json:"name"` 36 | Email string `json:"email"` 37 | Password string `json:"password"` 38 | isActive bool `json:"is_active"` 39 | } 40 | 41 | type Database struct { 42 | Ip string `json:"ip"` 43 | Port int `json:"port"` 44 | User string `json:"user"` 45 | Password string `json:"password"` 46 | Name string `json:"name"` 47 | } 48 | 49 | type SMTP struct { 50 | Server string `json:"smtp"` 51 | Port int `json:"port"` 52 | User string `json:"user"` 53 | Password string `json:"password"` 54 | Name string `json:"name"` 55 | } 56 | 57 | // main configuration structure 58 | type Config struct { 59 | Database `json:"database"` 60 | SMTP 61 | } 62 | 63 | var httpAddress = flag.String("http", ":8080", "http address") 64 | var httpsAddress = flag.String("https", ":8090", "https address") 65 | var httpsEnabled = flag.Bool("https-enabled", false, "enable https server") 66 | 67 | var config = &Config{} 68 | var templ = template.New("templ").Delims("[[", "]]") // integrate with angular 69 | var router = mux.NewRouter() // main router 70 | 71 | // NOTE this is the salt for secured passwords 72 | const salt = "super-secret-key" 73 | 74 | // NOTE random recovery password length 75 | const token_len = 8 76 | 77 | var ( 78 | // key must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256) 79 | key = []byte(salt) 80 | store = sessions.NewCookieStore(key) 81 | ) 82 | 83 | func main() { 84 | // NOTE this will avoid processor overload in some circumstances 85 | runtime.GOMAXPROCS(1) 86 | 87 | flag.Usage = func() { 88 | fmt.Printf("usage: %s [options]\n", filepath.Base(os.Args[0])) 89 | flag.PrintDefaults() 90 | } 91 | flag.Parse() 92 | log.SetFlags(log.LstdFlags | log.Lshortfile) 93 | 94 | folder, err := filepath.Abs(filepath.Dir(os.Args[0])) 95 | if err != nil { 96 | log.Fatalln(err) 97 | } 98 | 99 | file, err := os.Open(filepath.Join(folder, "config.json")) 100 | if err != nil { 101 | log.Fatalln(err) 102 | } 103 | json.Decode(file, &config) 104 | // NOTE [DEBUG] print configuration 105 | // log.Println(config) 106 | 107 | if _, err := templ.ParseGlob(filepath.Join(folder, "ui", "*.html")); err != nil { 108 | log.Fatalln(err) 109 | } 110 | 111 | sqlConnect() 112 | 113 | //https://github.com/gorilla/sessions/issues/58#issuecomment-154217736 114 | gob.Register(User{}) 115 | 116 | router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 117 | templ.ExecuteTemplate(w, "index", nil) 118 | }) 119 | router.HandleFunc("/signin", signin) 120 | router.HandleFunc("/signup", signup) 121 | router.HandleFunc("/reset", reset) // reset password if you forgot 122 | router.HandleFunc("/signout", signout) 123 | router.HandleFunc("/cache.manifest", func(w http.ResponseWriter, r *http.Request) { 124 | // offline page is shown if network is down 125 | w.Header().Set("Content-Type", "text/cache-manifest") 126 | w.Write([]byte(MANIFEST)) 127 | }) 128 | application := router.PathPrefix("/a").Subrouter() 129 | application.Use(func(next http.Handler) http.Handler { 130 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 131 | // resolve cross-site access issues 132 | w.Header().Set("Access-Control-Allow-Origin", "*") 133 | session, _ := store.Get(r, SESSID) 134 | if session.IsNew || session.Values["authenticated"] == false { 135 | http.Error(w, "Session expired", http.StatusOK) 136 | return 137 | } 138 | next.ServeHTTP(w, r) 139 | }) 140 | }) 141 | application.HandleFunc("/home", home) 142 | application.HandleFunc("/user", user) 143 | 144 | // TODO application handlers come here 145 | 146 | // NOTE [DEBUG] print registered routes 147 | router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 148 | // if name, err := route.GetPathTemplate(); err == nil { 149 | // log.Println(name) 150 | // } 151 | return nil 152 | }) 153 | 154 | // file server 155 | router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(filepath.Join(folder, "static"))))) 156 | // --- error handlers (NotFoundHandler, MethodNotAllowedHandler) 157 | router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 158 | w.WriteHeader(http.StatusNotFound) 159 | //http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 160 | }) 161 | router.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 162 | w.WriteHeader(http.StatusMethodNotAllowed) 163 | }) 164 | 165 | if *httpsEnabled { 166 | go func() { 167 | // allow you to use self signed certificates 168 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 169 | // NOTE geneate self signed certificates for tests 170 | // openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt 171 | log.Printf("start https server on %s", *httpsAddress) 172 | if err := http.ListenAndServeTLS(*httpsAddress, filepath.Join(folder, "server.crt"), filepath.Join(folder, "server.key"), router); err != nil { 173 | log.Fatalln(err) 174 | } 175 | }() 176 | } 177 | 178 | log.Printf("start http server on %s", *httpAddress) 179 | if err := http.ListenAndServe(*httpAddress, router); err != nil { 180 | log.Fatalln(err) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Server side application skeleton 2 | === 3 | ![caution](https://user-images.githubusercontent.com/6298396/39394204-c8a60516-4ad7-11e8-8f07-5c28de190586.png) 4 | 5 | Basic server side web application with the following functionality: 6 | - HTTP/HTTPS servers 7 | - routes and authentication on middleware (use [gorilla mux](https://github.com/gorilla/mux)) 8 | - session management (use [gorilla sessions](https://github.com/gorilla/sessions)) 9 | - user management (registration, change and delete user, password recovery by mail) 10 | - offline fallback page if network fail 11 | - modular design, can be extended very easy 12 | - config file in JSON format 13 | - database generated from file or model (with [Mysql Workbench](https://dev.mysql.com/downloads/workbench/)) 14 | - responsive interface for mobile access, browser back button disabled 15 | - salted and hashed passwords in database 16 | 17 | ### Integrations 18 | 19 | The skeleton is based on [W3CSS](https://www.w3schools.com/w3css/), a very lightweight CSS framework and is designed to integrate with [JQuery](https://jquery.com/), [Angular](https://angular.io/), [Fontawesome](https://fontawesome.com/v4.7.0/icons/) and some useful libraries like [Datetime picker](https://trentrichardson.com/examples/timepicker/) and [Google Charts](https://developers.google.com/chart/) (see `ui/head.html` file). 20 | 21 | ### Compilation modes 22 | 23 | #### Plaint text passwords 24 | 25 | go build -tags="plain" 26 | 27 | Passwords are stored in plain text in the database and sent as plain text on mail to recovery (unrecommended). 28 | 29 | #### Hashed passwords 30 | 31 | go build -tags="sha1 random" 32 | 33 | Passwords are stored hashed in the database and sent as random on mail to recovery (recommended). 34 | 35 | #### Salted and hashed passwords 36 | 37 | go build -tags="saltsha1 random" 38 | 39 | Passwords are stored salted and hashed in the database and sent as random on mail to recovery (highly recommended). 40 | 41 | The tag `random` mean send password as random string. 42 | 43 | ### How is look like? 44 | 45 | #### Signup screen 46 | ![signup](./static/images/signup.png) 47 | #### Mail/reset my password 48 | ![reset](./static/images/reset.png) 49 | #### Signin screen 50 | ![signin](./static/images/signin.png) 51 | #### Basic home screen 52 | ![home](./static/images/home.png) 53 | #### Local account management 54 | ![user](./static/images/user.png) 55 | #### Network fail page 56 | ![cache](./static/images/cache.png) 57 | 58 | ### Configuration file 59 | 60 | { 61 | "database": { 62 | "ip": "example.com", 63 | "port": 3306, 64 | "user": "root", 65 | "password": "password", 66 | "name": "skel" 67 | }, 68 | "smtp": "smtp.gmail.com", 69 | "port": 587, 70 | "user": "mailer@gmail.com", 71 | "password": "password", 72 | "name": "mailer name" 73 | } 74 | 75 | Database must be created by hand before running the program. Mailer is used to recovery forgotten passwords. 76 | 77 | ### Database 78 | 79 | Database structure is described in `database.sql` file. Adjust the database name and import the file in Mysql. Alternatively you can use the model from `model/skel_full.mwb` (with [Mysql Workbench](https://dev.mysql.com/downloads/workbench/)) 80 | 81 | ### Parameters 82 | 83 | Start the program with the following parameters: 84 | 85 | #### `-http` 86 | 87 | Listening address and port for HTTP server. Default `:8080`. 88 | 89 | #### `-https` 90 | 91 | Listening address and port for HTTPS server. Default `:8090`. 92 | 93 | #### `-https-enabled` 94 | 95 | Enable HTTPS server. Default `false`. 96 | 97 | ### Adding a simple application 98 | 99 | Add menu option in `ui/navbar.html` file 100 | 101 | Page 102 | 103 | Add route to the application in `main.go` file 104 | 105 | application.HandleFunc("/dummy", dummy) 106 | 107 | Create `ui/dummy.html` file for application user interface 108 | 109 | [[define "dummy"]] 110 | 111 | 112 | 113 | 114 | [[template "head"]] 115 | 116 | 117 | [[template "navbar" .]] 118 |
119 |
120 |
121 | 122 |

[[.Message]] 123 |

124 |
125 |
126 | 127 | 128 | [[end]] 129 | 130 | Create `ui_dummy.go` file who handle the application 131 | 132 | package main 133 | 134 | import ( 135 | "net/http" 136 | ) 137 | 138 | func dummy(w http.ResponseWriter, r *http.Request) { 139 | session, _ := store.Get(r, SESSID) 140 | user := session.Values["user"].(User) 141 | params := struct { 142 | Name string // this is mandatory 143 | Message string // TODO add fields as you need 144 | }{Name: user.Name} 145 | switch r.Method { 146 | case "GET": 147 | templ.ExecuteTemplate(w, "dummy", params) 148 | case "POST": 149 | params.Message = "You pressed dummy button!" 150 | templ.ExecuteTemplate(w, "dummy", params) 151 | } 152 | } 153 | 154 | Take care to the template parameters. Add them as you need. `Name` parameter must appear in all applications because is the user name from the top right navbar. 155 | 156 | Look carefully over **TODO** and **NOTE** comments because you may need to adjust as you need. 157 | 158 | ### Make your application look better with Bootstrap 159 | 160 | For keeping things simple, the skeleton comes with a very basic set of widgets based on W3CSS. You can use [Bootstrap](https://getbootstrap.com/) following the next steps: 161 | 162 | - remove inputs `height` from `style.html` because is useful just for W3CSS framework 163 | 164 | select,input,button { 165 | -height:30px; /* all inputs must have the same height */ 166 | 167 | - use fixed length in forms containers, eg: 168 | 169 |
170 | 171 | - add bootstrap classes on inputs (or other elements), eg: 172 | 173 | 174 |