mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-11-01 02:43:35 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			639 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			639 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package model
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"crypto/tls"
 | |
| 	"database/sql"
 | |
| 	"encoding/json"
 | |
| 	. "github.com/mickael-kerjean/filestash/server/common"
 | |
| 	"golang.org/x/crypto/bcrypt"
 | |
| 	"gopkg.in/gomail.v2"
 | |
| 	"html/template"
 | |
| 	sqlite "modernc.org/sqlite"
 | |
| 	"net/http"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| type Proof struct {
 | |
| 	Id      string  `json:"id"`
 | |
| 	Key     string  `json:"key"`
 | |
| 	Value   string  `json:"-"`
 | |
| 	Message *string `json:"message,omitempty"`
 | |
| 	Error   *string `json:"error,omitempty"`
 | |
| }
 | |
| 
 | |
| func ShareList(backend string, path string) ([]Share, error) {
 | |
| 	stmt, err := DB.Prepare("SELECT id, related_path, params FROM Share WHERE related_backend = ? AND related_path LIKE ? || '%' ")
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	rows, err := stmt.Query(backend, path)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	sharedFiles := []Share{}
 | |
| 	for rows.Next() {
 | |
| 		var a Share
 | |
| 		var params []byte
 | |
| 		rows.Scan(&a.Id, &a.Path, ¶ms)
 | |
| 		json.Unmarshal(params, &a)
 | |
| 		sharedFiles = append(sharedFiles, a)
 | |
| 	}
 | |
| 	rows.Close()
 | |
| 	return sharedFiles, nil
 | |
| }
 | |
| 
 | |
| func ShareGet(id string) (Share, error) {
 | |
| 	var p Share
 | |
| 	stmt, err := DB.Prepare("SELECT id, related_backend, related_path, auth, params FROM share WHERE id = ?")
 | |
| 	if err != nil {
 | |
| 		return p, err
 | |
| 	}
 | |
| 	defer stmt.Close()
 | |
| 	row := stmt.QueryRow(id)
 | |
| 	var str []byte
 | |
| 	if err = row.Scan(&p.Id, &p.Backend, &p.Path, &p.Auth, &str); err != nil {
 | |
| 		if err == sql.ErrNoRows {
 | |
| 			return p, ErrNotFound
 | |
| 		}
 | |
| 		return p, err
 | |
| 	}
 | |
| 	json.Unmarshal(str, &p)
 | |
| 	return p, nil
 | |
| }
 | |
| 
 | |
| func ShareUpsert(p *Share) error {
 | |
| 	if p.Password != nil {
 | |
| 		if *p.Password == PASSWORD_DUMMY {
 | |
| 			if s, err := ShareGet(p.Id); err != nil {
 | |
| 				p.Password = s.Password
 | |
| 			}
 | |
| 		} else {
 | |
| 			hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(*p.Password), bcrypt.DefaultCost)
 | |
| 			p.Password = NewString(string(hashedPassword))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	stmt, err := DB.Prepare("INSERT INTO Location(backend, path) VALUES($1, $2)")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	_, err = stmt.Exec(p.Backend, p.Path)
 | |
| 	if err != nil {
 | |
| 		throw := true
 | |
| 		errConstraintPrimaryKey := 1555
 | |
| 		if ferr, ok := err.(*sqlite.Error); ok == true && ferr.Code() == errConstraintPrimaryKey {
 | |
| 			throw = false
 | |
| 		}
 | |
| 		if throw == true {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	stmt, err = DB.Prepare("INSERT INTO Share(id, related_backend, related_path, params, auth) VALUES($1, $2, $3, $4, $5) ON CONFLICT(id) DO UPDATE SET related_backend = $2, related_path = $3, params = $4")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	j, _ := json.Marshal(&struct {
 | |
| 		Password     *string `json:"password,omitempty"`
 | |
| 		Users        *string `json:"users,omitempty"`
 | |
| 		Expire       *int64  `json:"expire,omitempty"`
 | |
| 		Url          *string `json:"url,omitempty"`
 | |
| 		CanShare     bool    `json:"can_share"`
 | |
| 		CanManageOwn bool    `json:"can_manage_own"`
 | |
| 		CanRead      bool    `json:"can_read"`
 | |
| 		CanWrite     bool    `json:"can_write"`
 | |
| 		CanUpload    bool    `json:"can_upload"`
 | |
| 	}{
 | |
| 		Password:     p.Password,
 | |
| 		Users:        p.Users,
 | |
| 		Expire:       p.Expire,
 | |
| 		Url:          p.Url,
 | |
| 		CanShare:     p.CanShare,
 | |
| 		CanManageOwn: p.CanManageOwn,
 | |
| 		CanRead:      p.CanRead,
 | |
| 		CanWrite:     p.CanWrite,
 | |
| 		CanUpload:    p.CanUpload,
 | |
| 	})
 | |
| 	_, err = stmt.Exec(p.Id, p.Backend, p.Path, j, p.Auth)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func ShareDelete(id string) error {
 | |
| 	stmt, err := DB.Prepare("DELETE FROM Share WHERE id = ?")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	_, err = stmt.Exec(id)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func ShareProofVerifier(s Share, proof Proof) (Proof, error) {
 | |
| 	p := proof
 | |
| 
 | |
| 	if proof.Key == "password" {
 | |
| 		if s.Password == nil {
 | |
| 			return p, NewError("No password required", 400)
 | |
| 		}
 | |
| 
 | |
| 		v, ok := ShareProofVerifierPassword(*s.Password, proof.Value)
 | |
| 		if ok == false {
 | |
| 			time.Sleep(1000 * time.Millisecond)
 | |
| 			return p, ErrInvalidPassword
 | |
| 		}
 | |
| 		p.Value = v
 | |
| 	}
 | |
| 
 | |
| 	if proof.Key == "email" {
 | |
| 		// find out if user is authorized
 | |
| 		if s.Users == nil {
 | |
| 			return p, NewError("Authentication not required", 400)
 | |
| 		}
 | |
| 		v, ok := ShareProofVerifierEmail(*s.Users, proof.Value)
 | |
| 		if ok == false {
 | |
| 			time.Sleep(1000 * time.Millisecond)
 | |
| 			return p, ErrNotAuthorized
 | |
| 		}
 | |
| 		user := v
 | |
| 
 | |
| 		// prepare the verification code
 | |
| 		stmt, err := DB.Prepare("INSERT INTO Verification(key, code) VALUES(?, ?)")
 | |
| 		if err != nil {
 | |
| 			return p, err
 | |
| 		}
 | |
| 		code := RandomString(4)
 | |
| 		if _, err := stmt.Exec("email::"+user, code); err != nil {
 | |
| 			return p, err
 | |
| 		}
 | |
| 
 | |
| 		// Prepare message
 | |
| 		var b bytes.Buffer
 | |
| 		t := template.New("email")
 | |
| 		t.Parse(TmplEmailVerification())
 | |
| 		t.Execute(&b, struct {
 | |
| 			Code     string
 | |
| 			Username string
 | |
| 		}{code, networkDriveUsernameEnc(user)})
 | |
| 
 | |
| 		p.Key = "code"
 | |
| 		p.Value = ""
 | |
| 		p.Message = NewString("We've sent you a message with a verification code")
 | |
| 
 | |
| 		// Send email
 | |
| 		email := struct {
 | |
| 			Hostname string `json:"server"`
 | |
| 			Port     int    `json:"port"`
 | |
| 			Username string `json:"username"`
 | |
| 			Password string `json:"password"`
 | |
| 			From     string `json:"from"`
 | |
| 		}{
 | |
| 			Hostname: Config.Get("email.server").String(),
 | |
| 			Port:     Config.Get("email.port").Int(),
 | |
| 			Username: Config.Get("email.username").String(),
 | |
| 			Password: Config.Get("email.password").String(),
 | |
| 			From:     Config.Get("email.from").String(),
 | |
| 		}
 | |
| 
 | |
| 		m := gomail.NewMessage()
 | |
| 		m.SetHeader("From", email.From)
 | |
| 		m.SetHeader("To", proof.Value)
 | |
| 		m.SetHeader("Subject", "Your verification code")
 | |
| 		m.SetBody("text/html", b.String())
 | |
| 		d := gomail.NewDialer(email.Hostname, email.Port, email.Username, email.Password)
 | |
| 		d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
 | |
| 		if err := d.DialAndSend(m); err != nil {
 | |
| 			Log.Error("Sendmail error: %v", err)
 | |
| 			Log.Error("Verification code '%s'", code)
 | |
| 			return p, NewError("Couldn't send email", 500)
 | |
| 		}
 | |
| 		return p, nil
 | |
| 	}
 | |
| 
 | |
| 	if proof.Key == "code" {
 | |
| 		// find key for given code
 | |
| 		stmt, err := DB.Prepare("SELECT key FROM Verification WHERE code = ? AND expire > datetime('now')")
 | |
| 		if err != nil {
 | |
| 			return p, NewError("Not found", 404)
 | |
| 		}
 | |
| 		row := stmt.QueryRow(proof.Value)
 | |
| 		var key string
 | |
| 		if err = row.Scan(&key); err != nil {
 | |
| 			if err == sql.ErrNoRows {
 | |
| 				stmt.Close()
 | |
| 				p.Key = "email"
 | |
| 				p.Value = ""
 | |
| 				return p, NewError("Not found", 404)
 | |
| 			}
 | |
| 			stmt.Close()
 | |
| 			return p, err
 | |
| 		}
 | |
| 		stmt.Close()
 | |
| 
 | |
| 		// cleanup current attempt so that it isn't used for malicious purpose
 | |
| 		if stmt, err = DB.Prepare("DELETE FROM Verification WHERE code = ?"); err == nil {
 | |
| 			stmt.Exec(proof.Value)
 | |
| 			stmt.Close()
 | |
| 		}
 | |
| 		p.Key = "email"
 | |
| 		p.Value = strings.TrimPrefix(key, "email::")
 | |
| 	}
 | |
| 
 | |
| 	return p, nil
 | |
| }
 | |
| 
 | |
| func ShareProofVerifierPassword(hashed string, given string) (string, bool) {
 | |
| 	if err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(given)); err != nil {
 | |
| 		return "", false
 | |
| 	}
 | |
| 	return hashed, true
 | |
| }
 | |
| func ShareProofVerifierEmail(users string, wanted string) (string, bool) {
 | |
| 	s := strings.Split(users, ",")
 | |
| 	user := ""
 | |
| 	for _, possibleUser := range s {
 | |
| 		possibleUser := strings.Trim(possibleUser, " ")
 | |
| 		if wanted == possibleUser {
 | |
| 			user = possibleUser
 | |
| 			break
 | |
| 		} else if possibleUser[0:1] == "*" {
 | |
| 			if strings.HasSuffix(wanted, strings.TrimPrefix(possibleUser, "*")) {
 | |
| 				user = possibleUser
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if user == "" {
 | |
| 		return "", false
 | |
| 	}
 | |
| 	return user, true
 | |
| }
 | |
| 
 | |
| func ShareProofGetAlreadyVerified(req *http.Request) []Proof {
 | |
| 	var p []Proof
 | |
| 	var cookieValue string
 | |
| 
 | |
| 	c, _ := req.Cookie(COOKIE_NAME_PROOF)
 | |
| 	if c == nil {
 | |
| 		return p
 | |
| 	}
 | |
| 	cookieValue = c.Value
 | |
| 	if len(cookieValue) > 500 {
 | |
| 		return p
 | |
| 	}
 | |
| 	j, err := DecryptString(SECRET_KEY_DERIVATE_FOR_PROOF, cookieValue)
 | |
| 	if err != nil {
 | |
| 		return p
 | |
| 	}
 | |
| 	_ = json.Unmarshal([]byte(j), &p)
 | |
| 	return p
 | |
| }
 | |
| 
 | |
| func ShareProofGetRequired(s Share) []Proof {
 | |
| 	var p []Proof
 | |
| 	if s.Password != nil {
 | |
| 		p = append(p, Proof{Key: "password", Value: *s.Password})
 | |
| 	}
 | |
| 	if s.Users != nil {
 | |
| 		p = append(p, Proof{Key: "email", Value: *s.Users})
 | |
| 	}
 | |
| 	return p
 | |
| }
 | |
| 
 | |
| func ShareProofCalculateRemainings(ref []Proof, mem []Proof) []Proof {
 | |
| 	var remainingProof []Proof
 | |
| 
 | |
| 	for i := 0; i < len(ref); i++ {
 | |
| 		keep := true
 | |
| 		for j := 0; j < len(mem); j++ {
 | |
| 			if shareProofAreEquivalent(ref[i], mem[j]) {
 | |
| 				keep = false
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		if keep {
 | |
| 			remainingProof = append(remainingProof, ref[i])
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return remainingProof
 | |
| }
 | |
| 
 | |
| func shareProofAreEquivalent(ref Proof, p Proof) bool {
 | |
| 	if ref.Key != p.Key {
 | |
| 		return false
 | |
| 	} else if ref.Value != "" && ref.Value == p.Value {
 | |
| 		return true
 | |
| 	}
 | |
| 	for _, chunk := range strings.Split(ref.Value, ",") {
 | |
| 		chunk = strings.Trim(chunk, " ")
 | |
| 		if p.Id == Hash(ref.Key+"::"+chunk, 20) {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func TmplEmailVerification() string {
 | |
| 	return `
 | |
| <!doctype html>
 | |
| <html>
 | |
|   <head>
 | |
|     <meta name="viewport" content="width=device-width" />
 | |
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 | |
|     <title>Verification code</title>
 | |
|     <style>
 | |
|       /* -------------------------------------
 | |
|           GLOBAL RESETS
 | |
|       ------------------------------------- */
 | |
|       img {
 | |
|         border: none;
 | |
|         -ms-interpolation-mode: bicubic;
 | |
|         max-width: 100%; }
 | |
|       body {
 | |
|         background-color: #f6f6f6;
 | |
|         font-family: sans-serif;
 | |
|         -webkit-font-smoothing: antialiased;
 | |
|         font-size: 14px;
 | |
|         line-height: 1.4;
 | |
|         margin: 0;
 | |
|         padding: 0;
 | |
|         -ms-text-size-adjust: 100%;
 | |
|         -webkit-text-size-adjust: 100%; }
 | |
|       table {
 | |
|         border-collapse: separate;
 | |
|         mso-table-lspace: 0pt;
 | |
|         mso-table-rspace: 0pt;
 | |
|         width: 100%; }
 | |
|         table td {
 | |
|           font-family: sans-serif;
 | |
|           font-size: 14px;
 | |
|           vertical-align: top; }
 | |
|       /* -------------------------------------
 | |
|           BODY & CONTAINER
 | |
|       ------------------------------------- */
 | |
|       .body {
 | |
|         background-color: #f6f6f6;
 | |
|         width: 100%; }
 | |
|       /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
 | |
|       .container {
 | |
|         display: block;
 | |
|         Margin: 0 auto !important;
 | |
|         /* makes it centered */
 | |
|         max-width: 450px;
 | |
|         padding: 10px;
 | |
|         width: 580px; }
 | |
|       /* This should also be a block element, so that it will fill 100% of the .container */
 | |
|       .content {
 | |
|         box-sizing: border-box;
 | |
|         display: block;
 | |
|         Margin: 0 auto;
 | |
|         max-width: 450px;
 | |
|         padding: 10px; }
 | |
|       /* -------------------------------------
 | |
|           HEADER, FOOTER, MAIN
 | |
|       ------------------------------------- */
 | |
|       .main {
 | |
|         background: #ffffff;
 | |
|         border-radius: 3px;
 | |
|         width: 100%; }
 | |
|       .wrapper {
 | |
|         box-sizing: border-box;
 | |
|         padding: 20px; }
 | |
|       .content-block {
 | |
|         padding-bottom: 10px;
 | |
|         padding-top: 10px;
 | |
|       }
 | |
|       .footer {
 | |
|         clear: both;
 | |
|         Margin-top: 10px;
 | |
|         text-align: center;
 | |
|         width: 100%; }
 | |
|         .footer td,
 | |
|         .footer p,
 | |
|         .footer span,
 | |
|         .footer a {
 | |
|           color: #999999;
 | |
|           font-size: 12px;
 | |
|           text-align: center; }
 | |
|       /* -------------------------------------
 | |
|           TYPOGRAPHY
 | |
|       ------------------------------------- */
 | |
|       h1,
 | |
|       h2,
 | |
|       h3,
 | |
|       h4 {
 | |
|         color: #000000;
 | |
|         font-family: sans-serif;
 | |
|         font-weight: 400;
 | |
|         line-height: 1.4;
 | |
|         margin: 0;
 | |
|         margin-bottom: 30px; }
 | |
|       h1 {
 | |
|         font-size: 35px;
 | |
|         font-weight: 300;
 | |
|         text-align: center;
 | |
|         text-transform: capitalize; }
 | |
|       p,
 | |
|       ul,
 | |
|       ol {
 | |
|         font-family: sans-serif;
 | |
|         font-size: 14px;
 | |
|         font-weight: normal;
 | |
|         margin: 0;
 | |
|         margin-bottom: 15px; }
 | |
|         p li,
 | |
|         ul li,
 | |
|         ol li {
 | |
|           list-style-position: inside;
 | |
|           margin-left: 5px; }
 | |
|       a {
 | |
|         color: #3498db;
 | |
|         text-decoration: underline; }
 | |
|       /* -------------------------------------
 | |
|           BUTTONS
 | |
|       ------------------------------------- */
 | |
|       .btn {
 | |
|         box-sizing: border-box;
 | |
|         width: 100%; }
 | |
|         .btn > tbody > tr > td {
 | |
|           padding-bottom: 15px; }
 | |
|         .btn table {
 | |
|           width: auto; }
 | |
|         .btn table td {
 | |
|           background-color: #ffffff;
 | |
|           border-radius: 5px;
 | |
|           text-align: center; }
 | |
|         .btn a {
 | |
|           background-color: #ffffff;
 | |
|           border: solid 1px #3498db;
 | |
|           border-radius: 5px;
 | |
|           box-sizing: border-box;
 | |
|           color: #3498db;
 | |
|           cursor: pointer;
 | |
|           display: inline-block;
 | |
|           font-size: 14px;
 | |
|           font-weight: bold;
 | |
|           margin: 0;
 | |
|           padding: 12px 25px;
 | |
|           text-decoration: none;
 | |
|           text-transform: capitalize; }
 | |
|       .btn-primary table td {
 | |
|         background-color: #3498db; }
 | |
|       .btn-primary a {
 | |
|         background-color: #3498db;
 | |
|         border-color: #3498db;
 | |
|         color: #ffffff; }
 | |
|       /* -------------------------------------
 | |
|           OTHER STYLES THAT MIGHT BE USEFUL
 | |
|       ------------------------------------- */
 | |
|       .last {
 | |
|         margin-bottom: 0; }
 | |
|       .first {
 | |
|         margin-top: 0; }
 | |
|       .align-center {
 | |
|         text-align: center; }
 | |
|       .align-right {
 | |
|         text-align: right; }
 | |
|       .align-left {
 | |
|         text-align: left; }
 | |
|       .clear {
 | |
|         clear: both; }
 | |
|       .mt0 {
 | |
|         margin-top: 0; }
 | |
|       .mb0 {
 | |
|         margin-bottom: 0; }
 | |
|       .preheader {
 | |
|         color: transparent;
 | |
|         display: none;
 | |
|         height: 0;
 | |
|         max-height: 0;
 | |
|         max-width: 0;
 | |
|         opacity: 0;
 | |
|         overflow: hidden;
 | |
|         mso-hide: all;
 | |
|         visibility: hidden;
 | |
|         width: 0; }
 | |
|       .powered-by a {
 | |
|         text-decoration: none; }
 | |
|       hr {
 | |
|         border: 0;
 | |
|         border-bottom: 1px solid #f6f6f6;
 | |
|         Margin: 20px 0; }
 | |
|       /* -------------------------------------
 | |
|           RESPONSIVE AND MOBILE FRIENDLY STYLES
 | |
|       ------------------------------------- */
 | |
|       @media only screen and (max-width: 490px) {
 | |
|         table[class=body] h1 {
 | |
|           font-size: 28px !important;
 | |
|           margin-bottom: 10px !important; }
 | |
|         table[class=body] p,
 | |
|         table[class=body] ul,
 | |
|         table[class=body] ol,
 | |
|         table[class=body] td,
 | |
|         table[class=body] span,
 | |
|         table[class=body] a {
 | |
|           font-size: 16px !important; }
 | |
|         table[class=body] .wrapper,
 | |
|         table[class=body] .article {
 | |
|           padding: 10px !important; }
 | |
|         table[class=body] .content {
 | |
|           padding: 0 !important; }
 | |
|         table[class=body] .container {
 | |
|           padding: 0 !important;
 | |
|           width: 100% !important; }
 | |
|         table[class=body] .main {
 | |
|           border-left-width: 0 !important;
 | |
|           border-radius: 0 !important;
 | |
|           border-right-width: 0 !important; }
 | |
|         table[class=body] .btn table {
 | |
|           width: 100% !important; }
 | |
|         table[class=body] .btn a {
 | |
|           width: 100% !important; }
 | |
|         table[class=body] .img-responsive {
 | |
|           height: auto !important;
 | |
|           max-width: 100% !important;
 | |
|           width: auto !important; }}
 | |
|       /* -------------------------------------
 | |
|           PRESERVE THESE STYLES IN THE HEAD
 | |
|       ------------------------------------- */
 | |
|       @media all {
 | |
|         .ExternalClass {
 | |
|           width: 100%; }
 | |
|         .ExternalClass,
 | |
|         .ExternalClass p,
 | |
|         .ExternalClass span,
 | |
|         .ExternalClass font,
 | |
|         .ExternalClass td,
 | |
|         .ExternalClass div {
 | |
|           line-height: 100%; }
 | |
|         .apple-link a {
 | |
|           color: inherit !important;
 | |
|           font-family: inherit !important;
 | |
|           font-size: inherit !important;
 | |
|           font-weight: inherit !important;
 | |
|           line-height: inherit !important;
 | |
|           text-decoration: none !important; }
 | |
|         .btn-primary table td:hover {
 | |
|           background-color: #34495e !important; }
 | |
|         .btn-primary a:hover {
 | |
|           background-color: #34495e !important;
 | |
|           border-color: #34495e !important; } }
 | |
|     </style>
 | |
|   </head>
 | |
|   <body class="">
 | |
|     <table border="0" cellpadding="0" cellspacing="0" class="body">
 | |
|       <tr>
 | |
|         <td> </td>
 | |
|         <td class="container">
 | |
|           <div class="content">
 | |
| 
 | |
|             <!-- START CENTERED WHITE CONTAINER -->
 | |
|             <span class="preheader">Your code to login</span>
 | |
|             <table class="main">
 | |
| 
 | |
|               <!-- START MAIN CONTENT AREA -->
 | |
|               <tr>
 | |
|                 <td class="wrapper">
 | |
|                   <table border="0" cellpadding="0" cellspacing="0">
 | |
|                     <tr>
 | |
|                       <td>
 | |
|                         <h2 style="font-weight:100;margin:0">Your verification code is: <strong>{{.Code}}</strong></h2>
 | |
|                       </td>
 | |
|                     </tr>
 | |
|                   </table>
 | |
|                   <div style="margin-top:10px;font-style:italic;font-size:0.9em;">When mounted as a network drive, you can authenticate as: {{.Username}}</div>
 | |
|                 </td>
 | |
|               </tr>
 | |
| 
 | |
|             <!-- END MAIN CONTENT AREA -->
 | |
|             </table>
 | |
| 
 | |
|             <!-- START FOOTER -->
 | |
|             <div class="footer">
 | |
|               <table border="0" cellpadding="0" cellspacing="0">
 | |
|                 <tr>
 | |
|                   <td class="content-block powered-by">
 | |
|                     Powered by <a href="http://github.com/mickael-kerjean/filestash">Filestash</a>.
 | |
|                   </td>
 | |
|                 </tr>
 | |
|               </table>
 | |
|             </div>
 | |
|             <!-- END FOOTER -->
 | |
| 
 | |
|           <!-- END CENTERED WHITE CONTAINER -->
 | |
|           </div>
 | |
|         </td>
 | |
|         <td> </td>
 | |
|       </tr>
 | |
|     </table>
 | |
|   </body>
 | |
| </html>
 | |
| `
 | |
| }
 | |
| 
 | |
| func networkDriveUsernameEnc(email string) string {
 | |
| 	return email + "[" + Hash(email+SECRET_KEY_DERIVATE_FOR_HASH, 10) + "]"
 | |
| }
 | 
