├── chitchat ├── views ├── en │ ├── error.html │ ├── new.thread.html │ ├── index.html │ ├── login.html │ ├── thread.html │ ├── signup.html │ ├── layout.html │ ├── auth.layout.html │ ├── navbar.html │ ├── auth.navbar.html │ └── auth.thread.html └── zh │ ├── error.html │ ├── new.thread.html │ ├── index.html │ ├── login.html │ ├── thread.html │ ├── signup.html │ ├── layout.html │ ├── auth.layout.html │ ├── navbar.html │ ├── auth.navbar.html │ └── auth.thread.html ├── public ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── css │ ├── login.css │ └── font-awesome.min.css └── js │ ├── bootstrap.min.js │ └── jquery-2.1.1.min.js ├── locales ├── active.zh.json └── active.en.json ├── logs └── chitchat.log ├── messages.go ├── config.json ├── go.mod ├── routes ├── router.go └── routes.go ├── models ├── post.go ├── db.go ├── session.go ├── thread.go └── user.go ├── README.md ├── main.go ├── handlers ├── index.go ├── post.go ├── thread.go ├── auth.go └── helper.go ├── config ├── viper.go └── config.go └── go.sum /chitchat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonfu/chitchat/HEAD/chitchat -------------------------------------------------------------------------------- /views/en/error.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |

{{ . }}

4 | 5 | {{ end }} -------------------------------------------------------------------------------- /views/zh/error.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |

{{ . }}

4 | 5 | {{ end }} -------------------------------------------------------------------------------- /public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonfu/chitchat/HEAD/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonfu/chitchat/HEAD/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonfu/chitchat/HEAD/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonfu/chitchat/HEAD/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /locales/active.zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "thread_not_found": { 3 | "description": "该群组不存在", 4 | "other": "该群组尚不存在,无法获取" 5 | } 6 | } -------------------------------------------------------------------------------- /locales/active.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "thread_not_found": { 3 | "description": "Thread not exists in db", 4 | "other": "Cannot read thread" 5 | } 6 | } -------------------------------------------------------------------------------- /logs/chitchat.log: -------------------------------------------------------------------------------- 1 | ERROR 2020/04/07 14:55:39 helper.go:71: sql: no rows in result set Cannot find user 2 | ERROR 2020/04/09 00:26:13 helper.go:71: sql: no rows in result set Cannot find user 3 | ERROR 2020/04/09 00:26:19 helper.go:71: sql: no rows in result set Cannot find user 4 | -------------------------------------------------------------------------------- /messages.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nicksnyder/go-i18n/v2/i18n" 4 | 5 | var messages = []i18n.Message{ 6 | i18n.Message{ 7 | ID: "thread_not_found", 8 | Description: "Thread not exists in db", 9 | Other: "Cannot read thread", 10 | }, 11 | } 12 | 13 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "Address": "0.0.0.0:8080", 4 | "Static": "public", 5 | "Log": "logs", 6 | "Locale": "locales", 7 | "Language": "en" 8 | }, 9 | "Db": { 10 | "Driver": "mysql", 11 | "Address": "localhost:3306", 12 | "Database": "chitchat", 13 | "User": "root", 14 | "Password": "root" 15 | } 16 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xueyuanjun/chitchat 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.7 7 | github.com/go-sql-driver/mysql v1.5.0 8 | github.com/gorilla/mux v1.7.4 9 | github.com/nicksnyder/go-i18n/v2 v2.0.3 10 | github.com/spf13/viper v1.6.3 11 | golang.org/x/text v0.3.2 12 | gopkg.in/yaml.v2 v2.2.8 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /views/zh/new.thread.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |
创建如下主题群组
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 | {{ end }} -------------------------------------------------------------------------------- /views/en/new.thread.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |
Start a new thread with the following topic
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 | {{ end }} -------------------------------------------------------------------------------- /routes/router.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import "github.com/gorilla/mux" 4 | 5 | // 返回一个 mux.Router 类型指针,从而可以当作处理器使用 6 | func NewRouter() *mux.Router { 7 | 8 | // 创建 mux.Router 路由器示例 9 | router := mux.NewRouter().StrictSlash(true) 10 | 11 | // 遍历 web.go 中定义的所有 webRoutes 12 | for _, route := range webRoutes { 13 | // 将每个 web 路由应用到路由器 14 | router.Methods(route.Method). 15 | Path(route.Pattern). 16 | Name(route.Name). 17 | Handler(route.HandlerFunc) 18 | } 19 | 20 | return router 21 | } -------------------------------------------------------------------------------- /views/zh/index.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |

3 | 创建新群组 或者加入下列群组! 4 |

5 | 6 | {{ range . }} 7 |
8 |
9 | {{ .Topic }} 10 |
11 |
12 | 由 {{ .User.Name }} 创建于 {{ .CreatedAt | fdate }} - 已有 {{ .NumReplies }} 个主题。 13 |
14 | 阅读更多 15 |
16 |
17 |
18 | {{ end }} 19 | 20 | {{ end }} -------------------------------------------------------------------------------- /views/en/index.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |

3 | Start a thread or join one below! 4 |

5 | 6 | {{ range . }} 7 |
8 |
9 | {{ .Topic }} 10 |
11 |
12 | Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }} posts. 13 |
14 | Read more 15 |
16 |
17 |
18 | {{ end }} 19 | 20 | {{ end }} -------------------------------------------------------------------------------- /views/zh/login.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |

5 | 6 | ChitChat 7 | 8 |

9 | 10 | 11 |
12 | 13 |
14 | 注册 15 |
16 | 17 | {{ end }} -------------------------------------------------------------------------------- /views/en/login.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |

5 | 6 | ChitChat 7 | 8 |

9 | 10 | 11 |
12 | 13 |
14 | Sign up 15 |
16 | 17 | {{ end }} -------------------------------------------------------------------------------- /views/en/thread.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |
5 | {{ .Topic }} 6 |
7 | Started by {{ .User.Name }} - {{ .CreatedAtDate }} 8 |
9 | 10 |
11 | 12 | {{ range .Posts }} 13 |
14 | {{ .Body }} 15 |
16 | {{ .User.Name }} - {{ .CreatedAtDate }} 17 |
18 |
19 | {{ end }} 20 | 21 |
22 | 23 | {{ end }} -------------------------------------------------------------------------------- /views/zh/thread.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |
5 | {{ .Topic }} 6 |
7 | 由 {{ .User.Name }} 创建于 {{ .CreatedAt | fdate }} 8 |
9 | 10 |
11 | 12 | {{ range .Posts }} 13 |
14 | {{ .Body }} 15 |
16 | {{ .User.Name }} - {{ .CreatedAt | fdate }} 17 |
18 |
19 | {{ end }} 20 | 21 |
22 | 23 | {{ end }} -------------------------------------------------------------------------------- /models/post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Post struct { 6 | Id int 7 | Uuid string 8 | Body string 9 | UserId int 10 | ThreadId int 11 | CreatedAt time.Time 12 | } 13 | 14 | func (post *Post) CreatedAtDate() string { 15 | return post.CreatedAt.Format("Jan 2, 2006 at 3:04pm") 16 | } 17 | 18 | // Get the user who wrote the post 19 | func (post *Post) User() (user User) { 20 | user = User{} 21 | Db.QueryRow("SELECT id, uuid, name, email, created_at FROM users WHERE id = ?", post.UserId). 22 | Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.CreatedAt) 23 | return 24 | } 25 | 26 | -------------------------------------------------------------------------------- /views/zh/signup.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |

5 | 6 | ChitChat 7 | 8 |

9 |
注册新账户
10 | 11 | 12 | 13 | 14 |
15 | 16 | {{ end }} -------------------------------------------------------------------------------- /views/en/signup.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |

5 | 6 | ChitChat 7 | 8 |

9 |
Sign up for an account below
10 | 11 | 12 | 13 | 14 |
15 | 16 | {{ end }} -------------------------------------------------------------------------------- /views/en/layout.html: -------------------------------------------------------------------------------- 1 | {{ define "layout" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ChitChat 10 | 11 | 12 | 13 | 14 | {{ template "navbar" . }} 15 | 16 |
17 | 18 | {{ template "content" . }} 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | {{ end }} -------------------------------------------------------------------------------- /views/zh/layout.html: -------------------------------------------------------------------------------- 1 | {{ define "layout" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ChitChat 10 | 11 | 12 | 13 | 14 | {{ template "navbar" . }} 15 | 16 |
17 | 18 | {{ template "content" . }} 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | {{ end }} -------------------------------------------------------------------------------- /views/en/auth.layout.html: -------------------------------------------------------------------------------- 1 | {{ define "layout" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ChitChat 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | {{ template "content" . }} 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | {{ end }} -------------------------------------------------------------------------------- /views/zh/auth.layout.html: -------------------------------------------------------------------------------- 1 | {{ define "layout" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ChitChat 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | {{ template "content" . }} 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | {{ end }} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Web 编程入门项目:基于 Go 语言开发在线论坛 2 | 3 | - [构建在线论坛项目(一):整体设计与数据模型](https://xueyuanjun.com/post/21519) 4 | - [构建在线论坛项目(二):模型类与 MySQL 数据库交互](https://xueyuanjun.com/post/21534) 5 | - [构建在线论坛项目(三):访问论坛首页](https://xueyuanjun.com/post/21545) 6 | - [构建在线论坛项目(四):用户认证实现(基于 Cookie + Session)](https://xueyuanjun.com/post/21555) 7 | - [构建在线论坛项目(五):创建群组和主题](https://xueyuanjun.com/post/21567) 8 | - [构建在线论坛项目(六):日志与错误处理](https://xueyuanjun.com/post/21573) 9 | - [构建在线论坛项目(七):通过单例获取全局配置](https://xueyuanjun.com/post/21575) 10 | - [构建在线论坛项目(八):消息、视图和日期时间本地化](https://xueyuanjun.com/post/21583) 11 | - [构建在线论坛项目(九):部署 Go Web 应用](https://xueyuanjun.com/post/21591) 12 | - [增补篇:通过 Viper 读取配置文件并实现热加载](https://xueyuanjun.com/post/21601) 13 | 14 | > 注:本项目基于 [chitchat](https://github.com/sausheong/gwp/tree/master/Chapter_2_Go_ChitChat/chitchat) 项目做的二次开发,将数据库调整为了 MySQL、路由器调整为了 gorilla/mux、新增了配置文件单例模式获取、本地化编程以及应用部署流程。 -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/xueyuanjun/chitchat/config" 5 | . "github.com/xueyuanjun/chitchat/routes" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func main() { 11 | startWebServer() 12 | } 13 | 14 | // 通过指定端口启动 Web 服务器 15 | func startWebServer() { 16 | r := NewRouter() // 通过 router.go 中定义的路由器来分发请求 17 | 18 | // 处理静态资源文件 19 | assets := http.FileServer(http.Dir(ViperConfig.App.Static)) 20 | r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", assets)) 21 | 22 | http.Handle("/", r) 23 | 24 | log.Println("Starting HTTP service at " + ViperConfig.App.Address) 25 | err := http.ListenAndServe(ViperConfig.App.Address, nil) 26 | 27 | if err != nil { 28 | log.Println("An error occured starting HTTP listener at " + ViperConfig.App.Address) 29 | log.Println("Error: " + err.Error()) 30 | } 31 | } -------------------------------------------------------------------------------- /handlers/index.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/xueyuanjun/chitchat/models" 5 | "net/http" 6 | ) 7 | 8 | func Index(writer http.ResponseWriter, request *http.Request) { 9 | threads, err := models.Threads(); 10 | if err == nil { 11 | _, err := session(writer, request) 12 | if err != nil { 13 | generateHTML(writer, threads, "layout", "navbar", "index") 14 | } else { 15 | generateHTML(writer, threads, "layout", "auth.navbar", "index") 16 | } 17 | } 18 | } 19 | 20 | func Err(writer http.ResponseWriter, request *http.Request) { 21 | vals := request.URL.Query() 22 | _, err := session(writer, request) 23 | if err != nil { 24 | generateHTML(writer, vals.Get("msg"), "layout", "navbar", "error") 25 | } else { 26 | generateHTML(writer, vals.Get("msg"), "layout", "auth.navbar", "error") 27 | } 28 | } -------------------------------------------------------------------------------- /views/zh/navbar.html: -------------------------------------------------------------------------------- 1 | {{ define "navbar" }} 2 | 26 | {{ end }} -------------------------------------------------------------------------------- /views/en/navbar.html: -------------------------------------------------------------------------------- 1 | {{ define "navbar" }} 2 | 26 | {{ end }} -------------------------------------------------------------------------------- /public/css/login.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 40px; 3 | padding-bottom: 40px; 4 | } 5 | 6 | .form-signin { 7 | max-width: 330px; 8 | padding: 15px; 9 | margin: 0 auto; 10 | } 11 | .form-signin .form-signin-heading, 12 | .form-signin .checkbox { 13 | margin-bottom: 10px; 14 | } 15 | .form-signin .checkbox { 16 | font-weight: normal; 17 | } 18 | .form-signin .form-control { 19 | position: relative; 20 | height: auto; 21 | -webkit-box-sizing: border-box; 22 | -moz-box-sizing: border-box; 23 | box-sizing: border-box; 24 | padding: 10px; 25 | font-size: 16px; 26 | } 27 | .form-signin .form-control:focus { 28 | z-index: 2; 29 | } 30 | .form-signin input[type="email"] { 31 | margin-bottom: -1px; 32 | border-bottom-right-radius: 0; 33 | border-bottom-left-radius: 0; 34 | } 35 | .form-signin input[type="password"] { 36 | margin-bottom: 10px; 37 | border-top-left-radius: 0; 38 | border-top-right-radius: 0; 39 | } 40 | -------------------------------------------------------------------------------- /views/zh/auth.navbar.html: -------------------------------------------------------------------------------- 1 | {{ define "navbar" }} 2 | 26 | {{ end }} -------------------------------------------------------------------------------- /views/en/auth.navbar.html: -------------------------------------------------------------------------------- 1 | {{ define "navbar" }} 2 | 26 | {{ end }} -------------------------------------------------------------------------------- /views/zh/auth.thread.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |
5 | {{ .Topic }} 6 |
7 | 由 {{ .User.Name }} 创建于 {{ .CreatedAtDate }} 8 |
9 | 10 |
11 | 12 | {{ range .Posts }} 13 |
14 | {{ .Body }} 15 |
16 | {{ .User.Name }} - {{ .CreatedAtDate }} 17 |
18 |
19 | {{ end }} 20 | 21 |
22 | 23 | 24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 | 39 | {{ end }} -------------------------------------------------------------------------------- /views/en/auth.thread.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 | 3 |
4 |
5 | {{ .Topic }} 6 |
7 | Started by {{ .User.Name }} - {{ .CreatedAtDate }} 8 |
9 | 10 |
11 | 12 | {{ range .Posts }} 13 |
14 | {{ .Body }} 15 |
16 | {{ .User.Name }} - {{ .CreatedAtDate }} 17 |
18 |
19 | {{ end }} 20 | 21 |
22 | 23 | 24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 | 39 | {{ end }} -------------------------------------------------------------------------------- /config/viper.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/fsnotify/fsnotify" 7 | "github.com/nicksnyder/go-i18n/v2/i18n" 8 | "github.com/spf13/viper" 9 | "golang.org/x/text/language" 10 | ) 11 | 12 | var ViperConfig Configuration 13 | 14 | func init() { 15 | runtimeViper := viper.New() 16 | runtimeViper.AddConfigPath(".") 17 | runtimeViper.SetConfigName("config") 18 | runtimeViper.SetConfigType("json") 19 | err := runtimeViper.ReadInConfig() 20 | if err != nil { 21 | panic(fmt.Errorf("Fatal error config file: %s \n", err)) 22 | } 23 | runtimeViper.Unmarshal(&ViperConfig) 24 | 25 | // 本地化初始设置 26 | bundle := i18n.NewBundle(language.English) 27 | bundle.RegisterUnmarshalFunc("json", json.Unmarshal) 28 | bundle.MustLoadMessageFile(ViperConfig.App.Locale + "/active.en.json") 29 | bundle.MustLoadMessageFile(ViperConfig.App.Locale + "/active." + ViperConfig.App.Language + ".json") 30 | ViperConfig.LocaleBundle = bundle 31 | 32 | // 监听配置文件变更 33 | runtimeViper.WatchConfig() 34 | runtimeViper.OnConfigChange(func(e fsnotify.Event) { 35 | runtimeViper.Unmarshal(&ViperConfig) 36 | ViperConfig.LocaleBundle.MustLoadMessageFile(ViperConfig.App.Locale + "/active." + ViperConfig.App.Language + ".json") 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /handlers/post.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/nicksnyder/go-i18n/v2/i18n" 6 | "github.com/xueyuanjun/chitchat/models" 7 | "net/http" 8 | ) 9 | 10 | // POST /thread/post 11 | // 在指定群组下创建新主题 12 | func PostThread(writer http.ResponseWriter, request *http.Request) { 13 | sess, err := session(writer, request) 14 | if err != nil { 15 | http.Redirect(writer, request, "/login", 302) 16 | } else { 17 | err = request.ParseForm() 18 | if err != nil { 19 | danger(err, "Cannot parse form") 20 | } 21 | user, err := sess.User() 22 | if err != nil { 23 | danger(err, "Cannot get user from session") 24 | } 25 | body := request.PostFormValue("body") 26 | uuid := request.PostFormValue("uuid") 27 | thread, err := models.ThreadByUUID(uuid) 28 | if err != nil { 29 | msg := localizer.MustLocalize(&i18n.LocalizeConfig{ 30 | MessageID: "thread_not_found", 31 | }) 32 | errorMessage(writer, request, msg) 33 | } 34 | if _, err := user.CreatePost(thread, body); err != nil { 35 | danger(err, "Cannot create post") 36 | } 37 | url := fmt.Sprint("/thread/read?id=", uuid) 38 | http.Redirect(writer, request, url, 302) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /models/db.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha1" 6 | "database/sql" 7 | "fmt" 8 | _ "github.com/go-sql-driver/mysql" 9 | . "github.com/xueyuanjun/chitchat/config" 10 | "log" 11 | ) 12 | 13 | var Db *sql.DB 14 | 15 | func init() { 16 | var err error 17 | driver := ViperConfig.Db.Driver 18 | source := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8&parseTime=true", ViperConfig.Db.User, ViperConfig.Db.Password, 19 | ViperConfig.Db.Address, ViperConfig.Db.Database) 20 | Db, err = sql.Open(driver, source) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | return 25 | } 26 | 27 | // create a random UUID with from RFC 4122 28 | // adapted from http://github.com/nu7hatch/gouuid 29 | func createUUID() (uuid string) { 30 | u := new([16]byte) 31 | _, err := rand.Read(u[:]) 32 | if err != nil { 33 | log.Fatalln("Cannot generate UUID", err) 34 | } 35 | 36 | // 0x40 is reserved variant from RFC 4122 37 | u[8] = (u[8] | 0x40) & 0x7F 38 | // Set the four most significant bits (bits 12 through 15) of the 39 | // time_hi_and_version field to the 4-bit version number. 40 | u[6] = (u[6] & 0xF) | (0x4 << 4) 41 | uuid = fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:]) 42 | return 43 | } 44 | 45 | // hash plaintext with SHA-1 46 | func Encrypt(plaintext string) (cryptext string) { 47 | cryptext = fmt.Sprintf("%x", sha1.Sum([]byte(plaintext))) 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /models/session.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Session struct { 6 | Id int 7 | Uuid string 8 | Email string 9 | UserId int 10 | CreatedAt time.Time 11 | } 12 | 13 | // Check if session is valid in the database 14 | func (session *Session) Check() (valid bool, err error) { 15 | err = Db.QueryRow("SELECT id, uuid, email, user_id, created_at FROM sessions WHERE uuid = ?", session.Uuid). 16 | Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt) 17 | if err != nil { 18 | valid = false 19 | return 20 | } 21 | if session.Id != 0 { 22 | valid = true 23 | } 24 | return 25 | } 26 | 27 | // Delete session from database 28 | func (session *Session) DeleteByUUID() (err error) { 29 | statement := "delete from sessions where uuid = ?" 30 | stmt, err := Db.Prepare(statement) 31 | if err != nil { 32 | return 33 | } 34 | defer stmt.Close() 35 | 36 | _, err = stmt.Exec(session.Uuid) 37 | return 38 | } 39 | 40 | // Get the user from the session 41 | func (session *Session) User() (user User, err error) { 42 | user = User{} 43 | err = Db.QueryRow("SELECT id, uuid, name, email, created_at FROM users WHERE id = ?", session.UserId). 44 | Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.CreatedAt) 45 | return 46 | } 47 | 48 | // Delete all sessions from database 49 | func SessionDeleteAll() (err error) { 50 | statement := "delete from sessions" 51 | _, err = Db.Exec(statement) 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/nicksnyder/go-i18n/v2/i18n" 6 | "golang.org/x/text/language" 7 | "log" 8 | "os" 9 | "sync" 10 | ) 11 | 12 | type App struct { 13 | Address string 14 | Static string 15 | Log string 16 | Locale string 17 | Language string 18 | } 19 | 20 | type Database struct { 21 | Driver string 22 | Address string 23 | Database string 24 | User string 25 | Password string 26 | } 27 | 28 | type Configuration struct { 29 | App App 30 | Db Database 31 | LocaleBundle *i18n.Bundle 32 | } 33 | 34 | var config *Configuration 35 | var once sync.Once 36 | 37 | // 通过单例模式初始化全局配置 38 | func LoadConfig() *Configuration { 39 | once.Do(func() { 40 | file, err := os.Open("config.json") 41 | if err != nil { 42 | log.Fatalln("Cannot open config file", err) 43 | } 44 | decoder := json.NewDecoder(file) 45 | config = &Configuration{} 46 | err = decoder.Decode(config) 47 | if err != nil { 48 | log.Fatalln("Cannot get configuration from file", err) 49 | } 50 | // 本地化初始设置 51 | bundle := i18n.NewBundle(language.English) 52 | bundle.RegisterUnmarshalFunc("json", json.Unmarshal) 53 | bundle.MustLoadMessageFile(config.App.Locale + "/active.en.json") 54 | bundle.MustLoadMessageFile(config.App.Locale + "/active." + config.App.Language + ".json") 55 | config.LocaleBundle = bundle 56 | }) 57 | return config 58 | } 59 | -------------------------------------------------------------------------------- /routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/xueyuanjun/chitchat/handlers" 5 | "net/http" 6 | ) 7 | 8 | // 定义一个 WebRoute 结构体用于存放单个路由 9 | type WebRoute struct { 10 | Name string 11 | Method string 12 | Pattern string 13 | HandlerFunc http.HandlerFunc 14 | } 15 | 16 | // 声明 WebRoutes 切片存放所有 Web 路由 17 | type WebRoutes []WebRoute 18 | 19 | // 定义所有 Web 路由 20 | var webRoutes = WebRoutes{ 21 | { 22 | "home", 23 | "GET", 24 | "/", 25 | handlers.Index, 26 | }, 27 | { 28 | "signup", 29 | "GET", 30 | "/signup", 31 | handlers.Signup, 32 | }, 33 | { 34 | "signupAccount", 35 | "POST", 36 | "/signup_account", 37 | handlers.SignupAccount, 38 | }, 39 | { 40 | "login", 41 | "GET", 42 | "/login", 43 | handlers.Login, 44 | }, 45 | { 46 | "auth", 47 | "POST", 48 | "/authenticate", 49 | handlers.Authenticate, 50 | }, 51 | { 52 | "logout", 53 | "GET", 54 | "/logout", 55 | handlers.Logout, 56 | }, 57 | { 58 | "newThread", 59 | "GET", 60 | "/thread/new", 61 | handlers.NewThread, 62 | }, 63 | { 64 | "createThread", 65 | "POST", 66 | "/thread/create", 67 | handlers.CreateThread, 68 | }, 69 | { 70 | "readThread", 71 | "GET", 72 | "/thread/read", 73 | handlers.ReadThread, 74 | }, 75 | { 76 | "postThread", 77 | "POST", 78 | "/thread/post", 79 | handlers.PostThread, 80 | }, 81 | { 82 | "error", 83 | "GET", 84 | "/err", 85 | handlers.Err, 86 | }, 87 | } 88 | -------------------------------------------------------------------------------- /handlers/thread.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | "github.com/xueyuanjun/chitchat/models" 6 | "net/http" 7 | ) 8 | 9 | // GET /threads/new 10 | // 创建群组页面 11 | func NewThread(writer http.ResponseWriter, request *http.Request) { 12 | _, err := session(writer, request) 13 | if err != nil { 14 | http.Redirect(writer, request, "/login", 302) 15 | } else { 16 | generateHTML(writer, nil, "layout", "auth.navbar", "new.thread") 17 | } 18 | } 19 | 20 | // POST /thread/create 21 | // 执行群组创建逻辑 22 | func CreateThread(writer http.ResponseWriter, request *http.Request) { 23 | sess, err := session(writer, request) 24 | if err != nil { 25 | http.Redirect(writer, request, "/login", 302) 26 | } else { 27 | err = request.ParseForm() 28 | if err != nil { 29 | danger(err, "Cannot parse form") 30 | } 31 | user, err := sess.User() 32 | if err != nil { 33 | danger(err, "Cannot get user from session") 34 | } 35 | topic := request.PostFormValue("topic") 36 | if _, err := user.CreateThread(topic); err != nil { 37 | danger(err, "Cannot create thread") 38 | } 39 | http.Redirect(writer, request, "/", 302) 40 | } 41 | } 42 | 43 | // GET /thread/read 44 | // 通过 ID 渲染指定群组页面 45 | func ReadThread(writer http.ResponseWriter, request *http.Request) { 46 | vals := request.URL.Query() 47 | uuid := vals.Get("id") 48 | thread, err := models.ThreadByUUID(uuid) 49 | if err != nil { 50 | msg := localizer.MustLocalize(&i18n.LocalizeConfig{ 51 | MessageID: "thread_not_found", 52 | }) 53 | errorMessage(writer, request, msg) 54 | } else { 55 | _, err := session(writer, request) 56 | if err != nil { 57 | generateHTML(writer, &thread, "layout", "navbar", "thread") 58 | } else { 59 | generateHTML(writer, &thread, "layout", "auth.navbar", "auth.thread") 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/xueyuanjun/chitchat/models" 5 | "net/http" 6 | ) 7 | 8 | // GET /login 9 | // 登录页面 10 | func Login(writer http.ResponseWriter, request *http.Request) { 11 | generateHTML(writer, nil, "auth.layout", "navbar", "login") 12 | } 13 | 14 | // GET /signup 15 | // 注册页面 16 | func Signup(writer http.ResponseWriter, request *http.Request) { 17 | generateHTML(writer, nil, "auth.layout", "navbar", "signup") 18 | } 19 | 20 | // POST /signup 21 | // 注册新用户 22 | func SignupAccount(writer http.ResponseWriter, request *http.Request) { 23 | err := request.ParseForm() 24 | if err != nil { 25 | danger(err, "Cannot parse form") 26 | } 27 | user := models.User{ 28 | Name: request.PostFormValue("name"), 29 | Email: request.PostFormValue("email"), 30 | Password: request.PostFormValue("password"), 31 | } 32 | if err := user.Create(); err != nil { 33 | danger(err, "Cannot create user") 34 | } 35 | http.Redirect(writer, request, "/login", 302) 36 | } 37 | 38 | // POST /authenticate 39 | // 通过邮箱和密码字段对用户进行认证 40 | func Authenticate(writer http.ResponseWriter, request *http.Request) { 41 | err := request.ParseForm() 42 | user, err := models.UserByEmail(request.PostFormValue("email")) 43 | if err != nil { 44 | danger(err, "Cannot find user") 45 | } 46 | if user.Password == models.Encrypt(request.PostFormValue("password")) { 47 | session, err := user.CreateSession() 48 | if err != nil { 49 | danger(err, "Cannot create session") 50 | } 51 | cookie := http.Cookie{ 52 | Name: "_cookie", 53 | Value: session.Uuid, 54 | HttpOnly: true, 55 | } 56 | http.SetCookie(writer, &cookie) 57 | http.Redirect(writer, request, "/", 302) 58 | } else { 59 | http.Redirect(writer, request, "/login", 302) 60 | } 61 | } 62 | 63 | // GET /logout 64 | // 用户退出 65 | func Logout(writer http.ResponseWriter, request *http.Request) { 66 | cookie, err := request.Cookie("_cookie") 67 | if err != http.ErrNoCookie { 68 | warning(err, "Failed to get cookie") 69 | session := models.Session{Uuid: cookie.Value} 70 | session.DeleteByUUID() 71 | } 72 | http.Redirect(writer, request, "/", 302) 73 | } 74 | -------------------------------------------------------------------------------- /handlers/helper.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/nicksnyder/go-i18n/v2/i18n" 7 | . "github.com/xueyuanjun/chitchat/config" 8 | "github.com/xueyuanjun/chitchat/models" 9 | "html/template" 10 | "log" 11 | "net/http" 12 | "os" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | var logger *log.Logger 18 | var localizer *i18n.Localizer 19 | 20 | func init() { 21 | // 获取本地化实例 22 | localizer = i18n.NewLocalizer(ViperConfig.LocaleBundle, ViperConfig.App.Language) 23 | file, err := os.OpenFile(ViperConfig.App.Log + "/chitchat.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 24 | if err != nil { 25 | log.Fatalln("Failed to open log file", err) 26 | } 27 | logger = log.New(file, "INFO ", log.Ldate|log.Ltime|log.Lshortfile) 28 | } 29 | 30 | // Checks if the user is logged in and has a session, if not err is not nil 31 | func session(writer http.ResponseWriter, request *http.Request) (sess models.Session, err error) { 32 | cookie, err := request.Cookie("_cookie") 33 | if err == nil { 34 | sess = models.Session{Uuid: cookie.Value} 35 | if ok, _ := sess.Check(); !ok { 36 | err = errors.New("Invalid session") 37 | } 38 | } 39 | return 40 | } 41 | 42 | // 生成 HTML 模板 43 | func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) { 44 | var files []string 45 | for _, file := range filenames { 46 | files = append(files, fmt.Sprintf("views/%s/%s.html", ViperConfig.App.Language, file)) 47 | } 48 | funcMap := template.FuncMap{"fdate": formatDate} 49 | t := template.New("layout").Funcs(funcMap) 50 | templates := template.Must(t.ParseFiles(files...)) 51 | templates.ExecuteTemplate(writer, "layout", data) 52 | } 53 | 54 | // version 55 | func Version() string { 56 | return "0.1" 57 | } 58 | 59 | // 记录日志信息 60 | func info(args ...interface{}) { 61 | logger.SetPrefix("INFO ") 62 | logger.Println(args...) 63 | } 64 | 65 | func danger(args ...interface{}) { 66 | logger.SetPrefix("ERROR ") 67 | logger.Println(args...) 68 | } 69 | 70 | func warning(args ...interface{}) { 71 | logger.SetPrefix("WARNING ") 72 | logger.Println(args...) 73 | } 74 | 75 | // 异常处理统一重定向到错误页面 76 | func errorMessage(writer http.ResponseWriter, request *http.Request, msg string) { 77 | url := []string{"/err?msg=", msg} 78 | http.Redirect(writer, request, strings.Join(url, ""), 302) 79 | } 80 | 81 | // 日期格式化 82 | func formatDate(t time.Time) string { 83 | datetime := "2006-01-02 15:04:05" 84 | return t.Format(datetime) 85 | } -------------------------------------------------------------------------------- /models/thread.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Thread struct { 6 | Id int 7 | Uuid string 8 | Topic string 9 | UserId int 10 | CreatedAt time.Time 11 | } 12 | 13 | // format the CreatedAt date to display nicely on the screen 14 | func (thread *Thread) CreatedAtDate() string { 15 | return thread.CreatedAt.Format("Jan 2, 2006 at 3:04pm") 16 | } 17 | 18 | // get the number of posts in a thread 19 | func (thread *Thread) NumReplies() (count int) { 20 | rows, err := Db.Query("SELECT count(*) FROM posts where thread_id = ?", thread.Id) 21 | if err != nil { 22 | return 23 | } 24 | for rows.Next() { 25 | if err = rows.Scan(&count); err != nil { 26 | return 27 | } 28 | } 29 | rows.Close() 30 | return 31 | } 32 | 33 | // get posts to a thread 34 | func (thread *Thread) Posts() (posts []Post, err error) { 35 | rows, err := Db.Query("SELECT id, uuid, body, user_id, thread_id, created_at FROM posts where thread_id = ?", thread.Id) 36 | if err != nil { 37 | return 38 | } 39 | for rows.Next() { 40 | post := Post{} 41 | if err = rows.Scan(&post.Id, &post.Uuid, &post.Body, &post.UserId, &post.ThreadId, &post.CreatedAt); err != nil { 42 | return 43 | } 44 | posts = append(posts, post) 45 | } 46 | rows.Close() 47 | return 48 | } 49 | 50 | // Get all threads in the database and returns it 51 | func Threads() (threads []Thread, err error) { 52 | rows, err := Db.Query("SELECT id, uuid, topic, user_id, created_at FROM threads ORDER BY created_at DESC") 53 | if err != nil { 54 | return 55 | } 56 | for rows.Next() { 57 | conv := Thread{} 58 | if err = rows.Scan(&conv.Id, &conv.Uuid, &conv.Topic, &conv.UserId, &conv.CreatedAt); err != nil { 59 | return 60 | } 61 | threads = append(threads, conv) 62 | } 63 | rows.Close() 64 | return 65 | } 66 | 67 | // Get a thread by the UUID 68 | func ThreadByUUID(uuid string) (conv Thread, err error) { 69 | conv = Thread{} 70 | err = Db.QueryRow("SELECT id, uuid, topic, user_id, created_at FROM threads WHERE uuid = ?", uuid). 71 | Scan(&conv.Id, &conv.Uuid, &conv.Topic, &conv.UserId, &conv.CreatedAt) 72 | return 73 | } 74 | 75 | // Get the user who started this thread 76 | func (thread *Thread) User() (user User) { 77 | user = User{} 78 | Db.QueryRow("SELECT id, uuid, name, email, created_at FROM users WHERE id = ?", thread.UserId). 79 | Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.CreatedAt) 80 | return 81 | } -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type User struct { 6 | Id int 7 | Uuid string 8 | Name string 9 | Email string 10 | Password string 11 | CreatedAt time.Time 12 | } 13 | 14 | // Create a new session for an existing user 15 | func (user *User) CreateSession() (session Session, err error) { 16 | statement := "insert into sessions (uuid, email, user_id, created_at) values (?, ?, ?, ?)" 17 | stmtin, err := Db.Prepare(statement) 18 | if err != nil { 19 | return 20 | } 21 | defer stmtin.Close() 22 | 23 | uuid := createUUID() 24 | stmtin.Exec(uuid, user.Email, user.Id, time.Now()) 25 | 26 | stmtout, err := Db.Prepare("select id, uuid, email, user_id, created_at from sessions where uuid = ?") 27 | if err != nil { 28 | return 29 | } 30 | defer stmtout.Close() 31 | // use QueryRow to return a row and scan the returned id into the Session struct 32 | err = stmtout.QueryRow(uuid).Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt) 33 | return 34 | } 35 | 36 | // Get the session for an existing user 37 | func (user *User) Session() (session Session, err error) { 38 | session = Session{} 39 | err = Db.QueryRow("SELECT id, uuid, email, user_id, created_at FROM sessions WHERE user_id = ?", user.Id). 40 | Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt) 41 | return 42 | } 43 | 44 | // Create a new user, save user info into the database 45 | func (user *User) Create() (err error) { 46 | // Postgres does not automatically return the last insert id, because it would be wrong to assume 47 | // you're always using a sequence.You need to use the RETURNING keyword in your insert to get this 48 | // information from postgres. 49 | statement := "insert into users (uuid, name, email, password, created_at) values (?, ?, ?, ?, ?)" 50 | stmtin, err := Db.Prepare(statement) 51 | if err != nil { 52 | return 53 | } 54 | defer stmtin.Close() 55 | 56 | uuid := createUUID() 57 | stmtin.Exec(uuid, user.Name, user.Email, Encrypt(user.Password), time.Now()) 58 | 59 | stmtout, err := Db.Prepare("select id, uuid, created_at from users where uuid = ?") 60 | if err != nil { 61 | return 62 | } 63 | defer stmtout.Close() 64 | // use QueryRow to return a row and scan the returned id into the User struct 65 | err = stmtout.QueryRow(uuid).Scan(&user.Id, &user.Uuid, &user.CreatedAt) 66 | return 67 | } 68 | 69 | // Delete user from database 70 | func (user *User) Delete() (err error) { 71 | statement := "delete from users where id = ?" 72 | stmt, err := Db.Prepare(statement) 73 | if err != nil { 74 | return 75 | } 76 | defer stmt.Close() 77 | 78 | _, err = stmt.Exec(user.Id) 79 | return 80 | } 81 | 82 | // Update user information in the database 83 | func (user *User) Update() (err error) { 84 | statement := "update users set name = ?, email = ? where id = ?" 85 | stmt, err := Db.Prepare(statement) 86 | if err != nil { 87 | return 88 | } 89 | defer stmt.Close() 90 | 91 | _, err = stmt.Exec(user.Name, user.Email, user.Id) 92 | return 93 | } 94 | 95 | // Delete all users from database 96 | func UserDeleteAll() (err error) { 97 | statement := "delete from users" 98 | _, err = Db.Exec(statement) 99 | return 100 | } 101 | 102 | // Get all users in the database and returns it 103 | func Users() (users []User, err error) { 104 | rows, err := Db.Query("SELECT id, uuid, name, email, password, created_at FROM users") 105 | if err != nil { 106 | return 107 | } 108 | for rows.Next() { 109 | user := User{} 110 | if err = rows.Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt); err != nil { 111 | return 112 | } 113 | users = append(users, user) 114 | } 115 | rows.Close() 116 | return 117 | } 118 | 119 | // Get a single user given the email 120 | func UserByEmail(email string) (user User, err error) { 121 | user = User{} 122 | err = Db.QueryRow("SELECT id, uuid, name, email, password, created_at FROM users WHERE email = ?", email). 123 | Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt) 124 | return 125 | } 126 | 127 | // Get a single user given the UUID 128 | func UserByUUID(uuid string) (user User, err error) { 129 | user = User{} 130 | err = Db.QueryRow("SELECT id, uuid, name, email, password, created_at FROM users WHERE uuid = ?", uuid). 131 | Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt) 132 | return 133 | } 134 | 135 | // Create a new thread 136 | func (user *User) CreateThread(topic string) (conv Thread, err error) { 137 | statement := "insert into threads (uuid, topic, user_id, created_at) values (?, ?, ?, ?)" 138 | stmtin, err := Db.Prepare(statement) 139 | if err != nil { 140 | return 141 | } 142 | defer stmtin.Close() 143 | 144 | uuid := createUUID() 145 | stmtin.Exec(uuid, topic, user.Id, time.Now()) 146 | 147 | stmtout, err := Db.Prepare("select id, uuid, topic, user_id, created_at from threads where uuid = ?") 148 | if err != nil { 149 | return 150 | } 151 | defer stmtout.Close() 152 | 153 | // use QueryRow to return a row and scan the returned id into the Session struct 154 | err = stmtout.QueryRow(uuid).Scan(&conv.Id, &conv.Uuid, &conv.Topic, &conv.UserId, &conv.CreatedAt) 155 | return 156 | } 157 | 158 | // Create a new post to a thread 159 | func (user *User) CreatePost(conv Thread, body string) (post Post, err error) { 160 | statement := "insert into posts (uuid, body, user_id, thread_id, created_at) values (?, ?, ?, ?, ?)" 161 | stmtin, err := Db.Prepare(statement) 162 | if err != nil { 163 | return 164 | } 165 | defer stmtin.Close() 166 | 167 | uuid := createUUID() 168 | stmtin.Exec(uuid, body, user.Id, conv.Id, time.Now()) 169 | 170 | stmtout, err := Db.Prepare("select id, uuid, body, user_id, thread_id, created_at from posts where uuid = ?") 171 | if err != nil { 172 | return 173 | } 174 | defer stmtout.Close() 175 | 176 | // use QueryRow to return a row and scan the returned id into the Session struct 177 | err = stmtout.QueryRow(uuid).Scan(&post.Id, &post.Uuid, &post.Body, &post.UserId, &post.ThreadId, &post.CreatedAt) 178 | return 179 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= 3 | github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 10 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 11 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 12 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 13 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 14 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 15 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 16 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 17 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 18 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 22 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 23 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 26 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 27 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 28 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 29 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 30 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 31 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 32 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 33 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 36 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 37 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 40 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 41 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 42 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 43 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 44 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 45 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 46 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 47 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 48 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 49 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 50 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 51 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 52 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 53 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 54 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 55 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 56 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 57 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 58 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 59 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 60 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 61 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 62 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 63 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 64 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 65 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 66 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 67 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 68 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 69 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 70 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 71 | github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M= 72 | github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo= 73 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 74 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 75 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 76 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 79 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 80 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 81 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 82 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 83 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 84 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 85 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 86 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 87 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 88 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 89 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 90 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 91 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 92 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 93 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 94 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 95 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 96 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 97 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 98 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 99 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 100 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 101 | github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= 102 | github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= 103 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 104 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 105 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 106 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 107 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 108 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 109 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 110 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 111 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 112 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 113 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 114 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 115 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 116 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 117 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 118 | golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 119 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 120 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 121 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 122 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 123 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 124 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 125 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 126 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 127 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 128 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 129 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 131 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 134 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 135 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 137 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= 140 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 142 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 143 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 144 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 145 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 146 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= 147 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 148 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 149 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 150 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 151 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 152 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 153 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 154 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 155 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 156 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 157 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 158 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 159 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 160 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 161 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 162 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 163 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 164 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 165 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 166 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 167 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 168 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 169 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 170 | -------------------------------------------------------------------------------- /public/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.2.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"} -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('