backoffice.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. // Functions to deal with the backoffice.
  2. package main
  3. import (
  4. // "bytes"
  5. "crypto/md5"
  6. "database/sql"
  7. "encoding/hex"
  8. "fmt"
  9. "github.com/gorilla/securecookie"
  10. "github.com/heatxsink/go-gravatar"
  11. "html/template"
  12. // "io/ioutil"
  13. "net/http"
  14. // "strconv"
  15. // "strings"
  16. )
  17. // GoSLRentalTemplatesType expands on template.Template.
  18. // need to expand it so I can add a few more methods here
  19. type GoSLRentalTemplatesType struct{
  20. template.Template
  21. }
  22. // GoSLRentalTemplates stores all parsed templates for the backoffice.
  23. var GoSLRentalTemplates GoSLRentalTemplatesType
  24. // init parses all templates and puts it inside a (global) var.
  25. // This is supposed to be called just once! (in func main())
  26. func (gt *GoSLRentalTemplatesType)init(globbedPath string) error {
  27. temp, err := template.ParseGlob(globbedPath)
  28. checkErr(err) // move out later, we just need it here to check what's wrong with the templates (20170706)
  29. //Log.Info("Path is (inside init):", globbedPath)
  30. gt.Template = *temp
  31. return err
  32. }
  33. // GoSLRentalRenderer assembles the correct templates together and executes them.
  34. // this is mostly to deal with code duplication
  35. func (gt *GoSLRentalTemplatesType)GoSLRentalRenderer(w http.ResponseWriter, r *http.Request, tplName string, tplParams templateParameters) error {
  36. thisUserName := getUserName(r)
  37. // add cookie to all templates
  38. tplParams["SetCookie"] = thisUserName
  39. // Add URLPathPrefix
  40. tplParams["URLPathPrefix"] = URLPathPrefix
  41. // add Gravatar to templates (note that all logins are supposed to be emails)
  42. // calculate hash for the Gravatar hovercard
  43. hasher := md5.Sum([]byte(thisUserName))
  44. hash := hex.EncodeToString(hasher[:])
  45. tplParams["GravatarHash"] = hash // we ought to cache this somewhere
  46. // deal with sizes, we want to have a specific size for the top menu
  47. var gravatarSize, gravatarSizeMenu = 32, 32
  48. // if someone set the sizes, then use them; if not, use defaults
  49. // note that this required type assertion since tplParams is interface{}
  50. // see https://stackoverflow.com/questions/14289256/cannot-convert-data-type-interface-to-type-string-need-type-assertion
  51. if tplParams["GravatarSize"] == nil {
  52. tplParams["GravatarSize"] = gravatarSize
  53. } else {
  54. gravatarSize = tplParams["GravatarSize"].(int)
  55. }
  56. if tplParams["GravatarSizeMenu"] == nil {
  57. tplParams["GravatarSizeMenu"] = gravatarSizeMenu
  58. } else {
  59. gravatarSizeMenu = tplParams["GravatarSizeMenu"].(int)
  60. }
  61. // for Retina displays; we could add a multiplication function for Go templates, but I'm lazy (20170706)
  62. tplParams["GravatarTwiceSize"] = 2 * gravatarSize
  63. tplParams["GravatarTwiceSizeMenu"] = 2 * gravatarSizeMenu
  64. // Now call the nice library function to get us the URL to the image, for the two sizes
  65. g := gravatar.New("identicon", gravatarSize, "g", true)
  66. tplParams["Gravatar"] = g.GetImageUrl(thisUserName) // we also ought to cache this somewhere
  67. g = gravatar.New("identicon", gravatarSizeMenu, "g", true)
  68. tplParams["GravatarMenu"] = g.GetImageUrl(thisUserName) // we also ought to cache this somewhere
  69. w.Header().Set("X-Clacks-Overhead", "GNU Terry Pratchett") // do a tribute to one of my best fantasy authors (see http://www.gnuterrypratchett.com/) (20170807)
  70. return gt.ExecuteTemplate(w, tplName, tplParams)
  71. }
  72. // Auxiliary functions for session handling
  73. // see https://mschoebel.info/2014/03/09/snippet-golang-webapp-login-logout/ (20170603)
  74. var cookieHandler = securecookie.New( // from gorilla/securecookie
  75. securecookie.GenerateRandomKey(64),
  76. securecookie.GenerateRandomKey(32))
  77. // setSession returns a new session cookie with an encoded username.
  78. func setSession(userName string, response http.ResponseWriter) {
  79. value := map[string]string{
  80. "name": userName,
  81. }
  82. if encoded, err := cookieHandler.Encode("session", value); err == nil {
  83. cookie := &http.Cookie{
  84. Name: "session",
  85. Value: encoded,
  86. Path: "/",
  87. }
  88. // Log.Debug("Encoded cookie:", cookie)
  89. http.SetCookie(response, cookie)
  90. } else {
  91. Log.Error("Error encoding cookie:", err)
  92. }
  93. }
  94. // getUserName sees if we have a session cookie with an encoded user name, returning nil if not found.
  95. func getUserName(request *http.Request) (userName string) {
  96. if cookie, err := request.Cookie("session"); err == nil {
  97. cookieValue := make(map[string]string)
  98. if err = cookieHandler.Decode("session", cookie.Value, &cookieValue); err == nil {
  99. userName = cookieValue["name"]
  100. }
  101. }
  102. return userName
  103. }
  104. // clearSession will remove a cookie by setting its MaxAge to -1 and clearing its value.
  105. func clearSession(response http.ResponseWriter) {
  106. cookie := &http.Cookie{
  107. Name: "session",
  108. Value: "",
  109. Path: "/",
  110. MaxAge: -1,
  111. }
  112. http.SetCookie(response, cookie)
  113. }
  114. // checkSession will see if we have a valid cookie; if not, redirects to login.
  115. func checkSession(w http.ResponseWriter, r *http.Request) {
  116. // valid cookie and no errors?
  117. if getUserName(r) == "" {
  118. http.Redirect(w, r, URLPathPrefix + "/admin/login/", http.StatusFound)
  119. }
  120. }
  121. // Function handlers for HTTP requests (main functions for this file)
  122. // backofficeMain is the main page, has some minor statistics, may do this fancier later on.
  123. func backofficeMain(w http.ResponseWriter, r *http.Request) {
  124. checkSession(w, r) // make sure we've got a valid cookie, or else send to login page
  125. // let's load the main template for now, just to make sure this works
  126. tplParams := templateParameters{ "Title": "GoSLRental Administrator Panel - main",
  127. }
  128. err := GoSLRentalTemplates.GoSLRentalRenderer(w, r, "main", tplParams)
  129. checkErr(err)
  130. }
  131. // backofficeUserManagement deals with adding/removing application users. Just login(email) and password right now, no profiles, no email confirmations, etc. etc. etc.
  132. // This is basically a stub for more complex user management, to be reused by other developments...
  133. // I will not develop this further, except perhaps to link usernames to in-world avatars (may be useful)
  134. func backofficeUserManagement(w http.ResponseWriter, r *http.Request) {
  135. checkSession(w, r)
  136. tplParams := templateParameters{ "Title": "GoSLRental Administrator Panel - User Management",
  137. "Content": "Hi there, this is the User Management template",
  138. "URLPathPrefix": URLPathPrefix,
  139. "GoSLRentalJS": "user-management.js",
  140. }
  141. err := GoSLRentalTemplates.GoSLRentalRenderer(w, r, "user-management", tplParams)
  142. checkErr(err)
  143. }
  144. // backofficeLogin deals with authentication.
  145. func backofficeLogin(w http.ResponseWriter, r *http.Request) {
  146. // Log.Debug("Entered backoffice login for URL:", r.URL, "using method:", r.Method)
  147. if r.Method == "GET" {
  148. tplParams := templateParameters{ "Title": "GoSLRental Administrator Panel - login",
  149. "URLPathPrefix": URLPathPrefix,
  150. }
  151. err := GoSLRentalTemplates.GoSLRentalRenderer(w, r, "login", tplParams)
  152. checkErr(err)
  153. } else { // POST is assumed
  154. r.ParseForm()
  155. // logic part of logging in
  156. email := r.Form.Get("email")
  157. password := r.Form.Get("password")
  158. // Log.Debug("email:", email)
  159. // Log.Debug("password:", password)
  160. if email == "" || password == "" { // should never happen, since the form checks this
  161. http.Redirect(w, r, URLPathPrefix + "/", http.StatusFound)
  162. }
  163. // Check username on database
  164. db, err := sql.Open(PDO_Prefix, GoSLRentalDSN)
  165. checkErr(err)
  166. defer db.Close()
  167. // query
  168. rows, err := db.Query("SELECT Email, Password FROM Users")
  169. checkErr(err)
  170. defer rows.Close()
  171. var (
  172. Email string
  173. Password string
  174. )
  175. // enhash the received password; I just use MD5 for now because there is no backoffice to create
  176. // new users, so it's easy to generate passwords manually using md5sum;
  177. // however, MD5 is not strong enough for 'real' applications, it's just what we also use to
  178. // communicate with the in-world scripts (20170604)
  179. pwdmd5 := fmt.Sprintf("%x", md5.Sum([]byte(password))) //this has the hash we need to check
  180. authorised := false // outside of the for loop because of scope
  181. for rows.Next() { // we ought just to have one entry, but...
  182. _ = rows.Scan(&Email, &Password)
  183. // ignore errors for now, either it checks true or any error means no authentication possible
  184. if Password == pwdmd5 {
  185. authorised = true
  186. break
  187. }
  188. }
  189. if authorised {
  190. // we need to set a cookie here
  191. setSession(email, w)
  192. // redirect to home
  193. http.Redirect(w, r, URLPathPrefix + "/admin", http.StatusFound)
  194. } else {
  195. // possibly we ought to give an error and then redirect, but I don't know how to do that (20170604)
  196. http.Redirect(w, r, URLPathPrefix + "/", http.StatusFound) // will ask for login again
  197. }
  198. return
  199. }
  200. }
  201. // backofficeLogout clears session and returns to login prompt.
  202. func backofficeLogout(w http.ResponseWriter, r *http.Request) {
  203. clearSession(w)
  204. http.Redirect(w, r, URLPathPrefix + "/", http.StatusFound)
  205. }
  206. // backofficeLSLRegisterObject creates a LSL script for registering cubes, using the defaults set by the user.
  207. // This is better than using 'template' LSL scripts which people may fill in wrongly, this way at least
  208. // we won't get errors about wrong signature PIN or hostnames etc.
  209. func backofficeLSLRegisterObject(w http.ResponseWriter, r *http.Request) {
  210. checkSession(w, r)
  211. tplParams := templateParameters{ "Title": "GoSLRental LSL Generator - register object.lsl",
  212. "URLPathPrefix": URLPathPrefix,
  213. "Host": Host,
  214. "ServerPort": ServerPort,
  215. "LSLSignaturePIN": LSLSignaturePIN,
  216. "LSL": "lsl-register-object", // this will change some formatting on the 'main' template (20170706)
  217. }
  218. // check if we have a frontend (it's configured on the config.toml file); if no, use the ServerPort
  219. // the 'frontend' will be nginx, Apache, etc. to cache replies from Go and serve static files from port 80 (20170706)
  220. if FrontEnd == "" {
  221. tplParams["ServerPort"] = ServerPort
  222. }
  223. err := GoSLRentalTemplates.GoSLRentalRenderer(w, r, "main", tplParams)
  224. checkErr(err)
  225. }