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 | 
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 | 
47 | #### Mail/reset my password
48 | 
49 | #### Signin screen
50 | 
51 | #### Basic home screen
52 | 
53 | #### Local account management
54 | 
55 | #### Network fail page
56 | 
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 |
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 |