《Go Web 编程》之第2章 ChitChat论坛

第2章 ChitChat论坛

用户登录、发帖、回帖。

2.1 ChitChat简介

网上论坛相当于通过帖子(thread)进行对话的公告板。
由拥有特殊权限的版主(moderator)管理。
注册账号用户才能发帖和回帖,未注册账号用户只能查看帖子。

2.2 应用设计

Web应用的工作流程:
(1)客户端向服务器发送HTTP请求;
(2)服务器处理HTTP请求;
(3)返回HTTP响应。

ChitChat的请求格式:

http://<servername>/<handlername>?<parameters>
http://<服务器名><处理器名>?<参数>

处理器名按层级划分,/thread/read。
应用参数以URL查询形式传递。
多路复用器(multiplexer),检查请求,并重定向至各处理器处理。
处理器解析信息,处理,将数据传递给模板引擎。
模板引擎将模板和数据生成返回给客户端的HTML。

2.3 数据模型

ChitChat数据模型,包含4种数据结构,映射到关系型数据库。

  • User,论坛用户信息;
  • Session,当前登录会话;
  • Thread,论坛帖子,记录多个用户对话;
  • Post,用户在论坛里的回复。

2.4 请求的接收与处理

2.4.1 多路复用器

package main

import (
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	
	files := http.FileServer(http.Dir(config.Static))
	mux.Handle("/static/", http.StripPrefix("/static/", files))
	mux.HandleFunc("/", index)
	
	server := &http.Server{
		Addr:           "0.0.0.0:8080",
		Handler:        mux,
	}
	server.ListenAndServe()
}

2.4.2 服务静态文件

http://localhost/static/css/bootstrap.min.css
映射为
<application root>/public/css/bootstrap.min.css
files := http.FileServer(http.Dir("/public"))
mux.Handle("/static/", http.StripPrefix("/static/", files))

2.4.3 创建处理器函数

package main

import (
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	files := http.FileServer(http.Dir(config.Static))
	mux.Handle("/static/", http.StripPrefix("/static/", files))

	mux.HandleFunc("/", index)
	mux.HandleFunc("/err", err)

	mux.HandleFunc("/login", login)
	mux.HandleFunc("/logout", logout)
	mux.HandleFunc("/signup", signup)
	mux.HandleFunc("/signup_account", signupAccount)
	mux.HandleFunc("/authenticate", authenticate)

	mux.HandleFunc("/thread/new", newThread)
	mux.HandleFunc("/thread/create", createThread)
	mux.HandleFunc("/thread/post", postThread)
	mux.HandleFunc("/thread/read", readThread)

	server := &http.Server{
		Addr:           "0.0.0.0:8080",
		Handler:        mux,
	}
	server.ListenAndServe()
}

2.4.4 使用cookie进行访问控制

route_auth.go

// POST /authenticate
// Authenticate the user given the email and password
func authenticate(writer http.ResponseWriter, request *http.Request) {
	err := request.ParseForm()
	user, err := data.UserByEmail(request.PostFormValue("email"))
	if err != nil {
		danger(err, "Cannot find user")
	}
	if user.Password == data.Encrypt(request.PostFormValue("password")) {
		session, err := user.CreateSession()
		if err != nil {
			danger(err, "Cannot create session")
		}
		cookie := http.Cookie{
			Name:     "_cookie",
			Value:    session.Uuid,
			HttpOnly: true,
		}
		http.SetCookie(writer, &cookie)
		http.Redirect(writer, request, "/", 302)
	} else {
		http.Redirect(writer, request, "/login", 302)
	}
}

util.go

// Checks if the user is logged in and has a session, if not err is not nil
func session(writer http.ResponseWriter, request *http.Request) (sess data.Session, err error) {
	cookie, err := request.Cookie("_cookie")
	if err == nil {
		sess = data.Session{Uuid: cookie.Value}
		if ok, _ := sess.Check(); !ok {
			err = errors.New("Invalid session")
		}
	}
	return
}

2.5 使用模板生成HTML响应

util.go

func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
	var files []string
	for _, file := range filenames {
		files = append(files, fmt.Sprintf("templates/%s.html", file))
	}

	templates := template.Must(template.ParseFiles(files...))
	templates.ExecuteTemplate(writer, "layout", data)
}

2.6 安装PostgreSQL

2.6.1 Ubuntu上安装

sudo apt-get install postgresql postgresql-contrib
//登入Postgres账号
sudo su postgres
//创建用户
createuser -interactive
//创建用户名命名的数据库
createdb USER_NAME

2.6.2 Mac上安装

Postgres应用压缩包解压,Postgres.app文件放入Applications文件夹。

~/.profile或~/.bashrc文件添加路径
export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/9.4/bin

2.6.3 Windows上安装

https://www.enterprisedb.com/downloads/postgres-postgresql-downloads

2.7 连接数据库

setup.sql

drop table if exists posts;
drop table if exists threads;
drop table if exists sessions;
drop table if exists users;

create table users (
  id         serial primary key,
  uuid       varchar(64) not null unique,
  name       varchar(255),
  email      varchar(255) not null unique,
  password   varchar(255) not null,
  created_at timestamp not null   
);

create table sessions (
  id         serial primary key,
  uuid       varchar(64) not null unique,
  email      varchar(255),
  user_id    integer references users(id),
  created_at timestamp not null   
);

create table threads (
  id         serial primary key,
  uuid       varchar(64) not null unique,
  topic      text,
  user_id    integer references users(id),
  created_at timestamp not null       
);

create table posts (
  id         serial primary key,
  uuid       varchar(64) not null unique,
  body       text,
  user_id    integer references users(id),
  thread_id  integer references threads(id),
  created_at timestamp not null  
);
createdb -p 5433 -U postgres chitchat

psql -p 5433 -U postgres -d chitchat -f F:\study\go\gwp-master\Chapter_2_Go_ChitChat\chitchat\data\setup.sql

thread.go

type Thread struct {
	Id        int
	Uuid      string
	Topic     string
	UserId    int
	CreatedAt time.Time
}

data.go

var Db *sql.DB

func init() {
	var err error
	Db, err = sql.Open("postgres", "host=localhost port=5433 user=postgres dbname=chitchat password=postgres sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	err = Db.Ping()
	if err != nil {
		panic(err)
	}
}

2.8 启动服务器

// starting up the server
	server := &http.Server{
		Addr:           config.Address,
		Handler:        mux,
	}
	server.ListenAndServe()

2.9 Web应用运作流程回顾

(1)客户端向服务器发送请求;
(2)多路复用器收到请求,重定向至正确的处理器;
(3)处理器对请求进行处理;
(4)需要访问数据库的情况下,处理器使用一或多个数据结构(根据数据库中数据建模);
(5)处理器调用与数据结构有关函数或方法时,数据结构背后的模型与数据库连接并进行相应操作;
(6)请求处理完毕时,处理器调用模板引擎,有时会向模板引擎传递一些通过模型获取到的数据;
(7)模板引擎语法分析模板文件列表创建模板结构,与处理器传递数据合并生成最终HTML;
(8)生成HTML作为响应回传至客户端。

2.10 各组成部分

2.10.1 模板

template/error.html

{{ define "content" }}
<p>{{ . }}</p>
{{ end }}

template/index.html

{{ define "content" }}
<h2><a href="/thread/new">Start a thread</a> or join one below!</h2>

{{ range . }}
<p><i>{{ .Topic }}</i> Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }} posts.</p>
<p><a href="/thread/read?id={{ .Uuid }}">Read more</a></p>
<br/>
{{ end }}
{{ end }}

template/layout.html

{{ define "layout" }}
<!DOCTYPE html>
<html>
	<head>
		<title>ChitChat</title>
	</head>
	<body>
		{{ template "navbar" . }}
		{{ template "content" . }}
	</body>
</html>
{{ end }}

template/login.html

{{ define "content" }}
<form action="/authenticate" method="post">
	<h2><i>ChitChat</i></h2>
	<input type="email" name="email" placeholder="Email address" required autofocus>
	<br/>
	<input type="password" name="password" placeholder="Password" required>
	<br/>
	<button type="submit">Sign in</button>
	<br/>
	<a href="/signup">Sign up</a>
</form>
{{ end }}

template/login.layout.html

{{ define "layout" }}
<!DOCTYPE html>
<html>
  <head>
    <title>ChitChat</title>
  </head>
  <body>
	{{ template "content" . }} 
  </body>
</html>
{{ end }}

template/new.thread.html

{{ define "content" }}
<form action="/thread/create" method="post">
	<p>Start a new thread with the following topic</p>
    <textarea name="topic" id="topic" placeholder="Thread topic here" rows="4"></textarea>
    <br/>
    <br/>
    <button type="submit">Start this thread</button>
</form>
{{ end }}

template/private.navbar.html

{{ define "navbar" }}
<a href="/"><i>ChitChat</i></a>
<ul>
	<li><a href="/">Home</a></li>
	<li><a href="/logout">Logout</a></li>
</ul>
{{ end }}

template/private.thread.html

{{ define "content" }}
<h2><i>{{ .Topic }}</i> Started by {{ .User.Name }} - {{ .CreatedAtDate }}</h2>

{{ range .Posts }}
<p>{{ .Body }}</p>
<p>{{ .User.Name }} - {{ .CreatedAtDate }}</p>
<br/>
{{ end }}

<form action="/thread/post" method="post">
	<textarea name="body" id="body" placeholder="Write your reply here" rows="3"></textarea>
	<input type="hidden" name="uuid" value="{{ .Uuid }}">
	<br/>
	<button type="submit">Reply</button>
</form>
{{ end }}

template/public.navbar.html

{{ define "navbar" }}
<a href="/"><i>ChitChat</i></a>
<ul>
	<li><a href="/">Home</a></li>
	<li><a href="/login">Login</a></li>
</ul>
{{ end }}

template/public.thread.html

{{ define "content" }}
<h2><i>{{ .Topic }}</i> Started by {{ .User.Name }} - {{ .CreatedAtDate }}</h2>

{{ range .Posts }}
<p>{{ .Body }}</p>
<p>{{ .User.Name }} - {{ .CreatedAtDate }}</p>
<br/>
{{ end }}
{{ end }}

template/signup.html

{{ define "content" }}
<form action="/signup_account" method="post">
  <h2><i>ChitChat</i></h2>
  <p>Sign up for an account below</p>
  <input id="name" type="text" name="name" placeholder="Name" required autofocus>
  <br/>
  <input type="email" name="email" placeholder="Email address" required>
  <br/>
  <input type="password" name="password" placeholder="Password" required>
  <br/>
  <button type="submit">Sign up</button>
</form>
{{ end }}

2.10.2 数据库

data/setup.sql

drop table if exists posts;
drop table if exists threads;
drop table if exists sessions;
drop table if exists users;

create table users (
  id         serial primary key,
  uuid       varchar(64) not null unique,
  name       varchar(255),
  email      varchar(255) not null unique,
  password   varchar(255) not null,
  created_at timestamp not null   
);

create table sessions (
  id         serial primary key,
  uuid       varchar(64) not null unique,
  email      varchar(255),
  user_id    integer references users(id),
  created_at timestamp not null   
);

create table threads (
  id         serial primary key,
  uuid       varchar(64) not null unique,
  topic      text,
  user_id    integer references users(id),
  created_at timestamp not null       
);

create table posts (
  id         serial primary key,
  uuid       varchar(64) not null unique,
  body       text,
  user_id    integer references users(id),
  thread_id  integer references threads(id),
  created_at timestamp not null  
);

创建数据库和表

createdb -p 5433 -U postgres chitchat

psql -p 5433 -U postgres -d chitchat -f F:\study\go\chitchat\data\setup.sql

data/data.go

package data

import (
	"crypto/rand"
	"crypto/sha1"
	"database/sql"
	"fmt"
	"log"

	_ "github.com/lib/pq"
)

//数据库连接池句柄
var Db *sql.DB

//数据库连接
func init() {
	var err error
	Db, err = sql.Open("postgres", "host=localhost port=5433 user=postgres dbname=chitchat password=postgres sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	err = Db.Ping()
	if err != nil {
		panic(err)
	}
}

//RFC 4122随机UUID(Universally Unique IDentifier
func createUUID() (uuid string) {
	u := new([16]byte)
	if _, err := rand.Read(u[:]); err != nil {
		log.Fatalln("Cannot generate UUID", err)
	}

	u[8] = (u[8] | 0x40) & 0x7F
	u[6] = (u[6] & 0xF) | (0x4 << 4)
	uuid = fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:])
	return
}

//SHA-1散列明文
func Encrypt(plaintext string) (cryptext string) {
	cryptext = fmt.Sprintf("%x", sha1.Sum([]byte(plaintext)))
	return
}

data/user.go

package data

import (
	"time"
)

//用户
type User struct {
	Id        int
	Uuid      string
	Name      string
	Email     string
	Password  string
	CreatedAt time.Time
}

// Create a new session for an existing user
func (user *User) CreateSession() (session Session, err error) {
	statement := "insert into sessions (uuid, email, user_id, created_at) values ($1, $2, $3, $4) returning id, uuid, email, user_id, created_at"
	stmt, err := Db.Prepare(statement)
	if err != nil {
		return
	}
	defer stmt.Close()

	// use QueryRow to return a row and scan the returned id into the Session struct
	err = stmt.QueryRow(createUUID(), user.Email, user.Id, time.Now()).
		Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt)
	return
}

// Get the session for an existing user
func (user *User) Session() (session Session, err error) {
	err = Db.QueryRow("SELECT id, uuid, email, user_id, created_at FROM sessions WHERE user_id = $1", user.Id).
		Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt)
	return
}

// Create a new user, save user info into the database
func (user *User) Create() (err error) {
	// Postgres does not automatically return the last insert id, because it would be wrong to assume
	// you're always using a sequence.You need to use the RETURNING keyword in your insert to get this
	// information from postgres.
	statement := "insert into users (uuid, name, email, password, created_at) values ($1, $2, $3, $4, $5) returning id, uuid, created_at"
	stmt, err := Db.Prepare(statement)
	if err != nil {
		return
	}
	defer stmt.Close()

	// use QueryRow to return a row and scan the returned id into the User struct
	err = stmt.QueryRow(createUUID(), user.Name, user.Email, Encrypt(user.Password), time.Now()).
		Scan(&user.Id, &user.Uuid, &user.CreatedAt)
	return
}

// Delete user from database
func (user *User) Delete() (err error) {
	stmt, err := Db.Prepare("delete from users where id = $1")
	if err != nil {
		return
	}
	defer stmt.Close()

	_, err = stmt.Exec(user.Id)
	return
}

// Update user information in the database
func (user *User) Update() (err error) {
	stmt, err := Db.Prepare("update users set name = $2, email = $3 where id = $1")
	if err != nil {
		return
	}
	defer stmt.Close()

	_, err = stmt.Exec(user.Id, user.Name, user.Email)
	return
}

// Delete all users from database
func UserDeleteAll() (err error) {
	_, err = Db.Exec("delete from users")
	return
}

// Get all users in the database and returns it
func Users() (users []User, err error) {
	rows, err := Db.Query("SELECT id, uuid, name, email, password, created_at FROM users")
	if err != nil {
		return
	}
	defer rows.Close()

	for rows.Next() {
		user := User{}
		if err = rows.Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt); err != nil {
			return
		}
		users = append(users, user)
	}
	return
}

// Get a single user given the UUID
func UserNoPasswordByID(id int) (user User, err error) {
	err = Db.QueryRow("SELECT id, uuid, name, email, created_at FROM users WHERE id = $1", id).
		Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.CreatedAt)
	return
}

// Get a single user given the UUID
func UserByID(id int) (user User, err error) {
	err = Db.QueryRow("SELECT id, uuid, name, email, password, created_at FROM users WHERE id = $1", id).
		Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt)
	return
}

// Get a single user given the UUID
func UserByUUID(uuid string) (user User, err error) {
	err = Db.QueryRow("SELECT id, uuid, name, email, password, created_at FROM users WHERE uuid = $1", uuid).
		Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt)
	return
}

// Get a single user given the email
func UserByEmail(email string) (user User, err error) {
	err = Db.QueryRow("SELECT id, uuid, name, email, password, created_at FROM users WHERE email = $1", email).
		Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt)
	return
}

// Create a new thread
func (user *User) CreateThread(topic string) (thread Thread, err error) {
	statement := "insert into threads (uuid, topic, user_id, created_at) values ($1, $2, $3, $4) returning id, uuid, topic, user_id, created_at"
	stmt, err := Db.Prepare(statement)
	if err != nil {
		return
	}
	defer stmt.Close()

	// use QueryRow to return a row and scan the returned id into the Session struct
	err = stmt.QueryRow(createUUID(), topic, user.Id, time.Now()).
		Scan(&thread.Id, &thread.Uuid, &thread.Topic, &thread.UserId, &thread.CreatedAt)
	return
}

// Create a new post to a thread
func (user *User) CreatePost(thread Thread, body string) (post Post, err error) {
	statement := "insert into posts (uuid, body, user_id, thread_id, created_at) values ($1, $2, $3, $4, $5) returning id, uuid, body, user_id, thread_id, created_at"
	stmt, err := Db.Prepare(statement)
	if err != nil {
		return
	}
	defer stmt.Close()

	// use QueryRow to return a row and scan the returned id into the Session struct
	err = stmt.QueryRow(createUUID(), body, user.Id, thread.Id, time.Now()).Scan(&post.Id, &post.Uuid, &post.Body, &post.UserId, &post.ThreadId, &post.CreatedAt)
	return
}

data/thread.go

package data

import (
	"time"
)

//主线
type Thread struct {
	Id        int
	Uuid      string
	Topic     string //话题
	UserId    int
	CreatedAt time.Time
}

// format the CreatedAt date to display nicely on the screen
func (thread *Thread) CreatedAtDate() string {
	return thread.CreatedAt.Format("Jan 2, 2006 at 3:04pm")
}

// get the number of posts in a thread
func (thread *Thread) NumReplies() (count int) {
	rows, err := Db.Query("SELECT count(*) FROM posts where thread_id = $1", thread.Id)
	if err != nil {
		return
	}
	defer rows.Close()

	for rows.Next() {
		if err = rows.Scan(&count); err != nil {
			return
		}
	}
	return
}

// get posts to a thread
func (thread *Thread) Posts() (posts []Post, err error) {
	rows, err := Db.Query("SELECT id, uuid, body, user_id, thread_id, created_at FROM posts where thread_id = $1", thread.Id)
	if err != nil {
		return
	}
	defer rows.Close()

	for rows.Next() {
		post := Post{}
		if err = rows.Scan(&post.Id, &post.Uuid, &post.Body, &post.UserId, &post.ThreadId, &post.CreatedAt); err != nil {
			return
		}
		posts = append(posts, post)
	}
	return
}

// Get all threads in the database and returns it
func Threads() (threads []Thread, err error) {
	rows, err := Db.Query("SELECT id, uuid, topic, user_id, created_at FROM threads ORDER BY created_at DESC")
	if err != nil {
		return
	}
	defer rows.Close()

	for rows.Next() {
		thread := Thread{}
		if err = rows.Scan(&thread.Id, &thread.Uuid, &thread.Topic, &thread.UserId, &thread.CreatedAt); err != nil {
			return
		}
		threads = append(threads, thread)
	}
	return
}

// Get a thread by the UUID
func ThreadByUUID(uuid string) (thread Thread, err error) {
	err = Db.QueryRow("SELECT id, uuid, topic, user_id, created_at FROM threads WHERE uuid = $1", uuid).
		Scan(&thread.Id, &thread.Uuid, &thread.Topic, &thread.UserId, &thread.CreatedAt)
	return
}

// Get the user who started this thread
func (thread *Thread) User() (user User) {
	user, _ = UserNoPasswordByID(thread.UserId)
	return
}

// Delete all threads from database
func ThreadDeleteAll() (err error) {
	_, err = Db.Exec("delete from threads")
	return
}

data/post.go

package data

import (
	"time"
)

//帖子
type Post struct {
	Id        int
	Uuid      string
	Body      string //内容
	UserId    int
	ThreadId  int
	CreatedAt time.Time
}

func (post *Post) CreatedAtDate() string {
	return post.CreatedAt.Format("Jan 2, 2006 at 3:04pm")
}

// Get the user who wrote the post
func (post *Post) User() (user User) {
	user, _ = UserNoPasswordByID(post.UserId)
	return
}

data/session.go

package data

import (
	"time"
)

//会话
type Session struct {
	Id        int
	Uuid      string
	Email     string
	UserId    int
	CreatedAt time.Time
}

// Check if session is valid in the database
func (session *Session) Check() (valid bool, err error) {
	err = Db.QueryRow("SELECT id, uuid, email, user_id, created_at FROM sessions WHERE uuid = $1", session.Uuid).
		Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt)
	if err != nil {
		valid = false
		return
	}
	if session.Id != 0 {
		valid = true
	}
	return
}

// Check if session is valid in the database
func CheckSeeeionByUUID(uuid string) bool {
	count := 0
	Db.QueryRow("SELECT count(*) FROM sessions WHERE uuid = $1 LIMIT 1", uuid).Scan(&count)
	return count == 1
}

// Check if session is valid in the database
func SessionByUUID(uuid string) (session Session, err error) {
	err = Db.QueryRow("SELECT id, uuid, email, user_id, created_at FROM sessions WHERE uuid = $1", uuid).
		Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt)
	return
}

func SessionDeleteByUUID(uuid string) (err error) {
	stmt, err := Db.Prepare("delete from sessions where uuid = $1")
	if err != nil {
		return
	}
	defer stmt.Close()

	_, err = stmt.Exec(uuid)
	return
}

// Delete session from database
func (session *Session) DeleteByUUID() (err error) {
	stmt, err := Db.Prepare("delete from sessions where uuid = $1")
	if err != nil {
		return
	}
	defer stmt.Close()

	_, err = stmt.Exec(session.Uuid)
	return
}

// Get the user from the session
func (session *Session) User() (user User, err error) {
	user, _ = UserNoPasswordByID(session.UserId)
	return
}

// Delete all sessions from database
func SessionDeleteAll() (err error) {
	_, err = Db.Exec("delete from sessions")
	return
}

2.10.3 主模块

config.json

{
  "Address"        : "127.0.0.1:8080",
  "ReadTimeout"    : 10,
  "WriteTimeout"   : 600
}

util.go

package main

import (
	"chitchat/data"
	"encoding/json"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
)

type Configuration struct {
	Address      string
	ReadTimeout  int64
	WriteTimeout int64
}

var config Configuration
var logger *log.Logger

func loadConfig() {
	fp, err := os.Open("config.json")
	if err != nil {
		log.Fatalln("Cannot open config file", err)
	}
	defer fp.Close()

	decoder := json.NewDecoder(fp)
	err = decoder.Decode(&config)
	if err != nil {
		log.Fatalln("Cannot get configuration from file", err)
	}
}

func init() {
	loadConfig()
	fp, err := os.OpenFile("chitchat.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalln("Failed to open log file", err)
	}
	logger = log.New(fp, "INFO ", log.Ldate|log.Ltime|log.Lshortfile)
}

// Convenience function to redirect to the error message page
func error_message(writer http.ResponseWriter, request *http.Request, msg string) {
	http.Redirect(writer, request, "/err?msg="+msg, http.StatusFound)
}

// Checks if the user is logged in and has a session, if not err is not nil
func session(request *http.Request) bool {
	if cookie, err := request.Cookie("_cookie"); err == nil {
		return data.CheckSeeeionByUUID(cookie.Value)
	}
	return false
}

func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
	var files []string
	for _, file := range filenames {
		files = append(files, fmt.Sprintf("templates/%s.html", file))
	}
	templates := template.Must(template.ParseFiles(files...))
	templates.ExecuteTemplate(writer, "layout", data)
}

func logError(args ...interface{}) {
	logger.SetPrefix("ERROR ")
	logger.Println(args...)
}

route.go

package main

import (
	"chitchat/data"
	"fmt"
	"net/http"
)

func index(writer http.ResponseWriter, request *http.Request) {
	if threads, err := data.Threads(); err != nil {
		error_message(writer, request, "Cannot get threads")
	} else {
		if !session(request) {
			generateHTML(writer, threads, "layout", "public.navbar", "index")
		} else {
			generateHTML(writer, threads, "layout", "private.navbar", "index")
		}
	}
}

// GET /err?msg=
// shows the error message page
func err(writer http.ResponseWriter, request *http.Request) {
	if vals := request.URL.Query(); !session(request) {
		generateHTML(writer, vals.Get("msg"), "layout", "public.navbar", "error")
	} else {
		generateHTML(writer, vals.Get("msg"), "layout", "private.navbar", "error")
	}
}

// GET /login
// Show the login page
func login(writer http.ResponseWriter, request *http.Request) {
	generateHTML(writer, nil, "login.layout", "login")
}

// GET /logout
// Logs the user out
func logout(writer http.ResponseWriter, request *http.Request) {
	if cookie, err := request.Cookie("_cookie"); err == nil {
		data.SessionDeleteByUUID(cookie.Value)
	}
	http.Redirect(writer, request, "/", http.StatusFound)
}

// GET /signup
// Show the signup page
func signup(writer http.ResponseWriter, request *http.Request) {
	generateHTML(writer, nil, "login.layout", "signup")
}

// POST /signup
// Create the user account
func signupAccount(writer http.ResponseWriter, request *http.Request) {
	user := data.User{
		Name:     request.PostFormValue("name"),
		Email:    request.PostFormValue("email"),
		Password: request.PostFormValue("password"),
	}
	if err := user.Create(); err != nil {
		logError(err, "Cannot create user")
	}
	http.Redirect(writer, request, "/login", http.StatusFound)
}

// POST /authenticate
// Authenticate the user given the email and password
func authenticate(writer http.ResponseWriter, request *http.Request) {
	user, err := data.UserByEmail(request.PostFormValue("email"))
	if err != nil {
		logError(err, "Cannot find user")
	}
	if user.Password == data.Encrypt(request.PostFormValue("password")) {
		session, err := user.CreateSession()
		if err != nil {
			logError(err, "Cannot create session")
		}

		cookie := http.Cookie{
			Name:     "_cookie",
			Value:    session.Uuid,
			HttpOnly: true,
		}
		http.SetCookie(writer, &cookie)
		http.Redirect(writer, request, "/", http.StatusFound)
	} else {
		http.Redirect(writer, request, "/login", http.StatusFound)
	}
}

// GET /thread/new
// Show the new thread form page
func newThread(writer http.ResponseWriter, request *http.Request) {
	if !session(request) {
		http.Redirect(writer, request, "/login", http.StatusFound)
	} else {
		generateHTML(writer, nil, "layout", "private.navbar", "new.thread")
	}
}

// POST /thread/create
// Create the user account
func createThread(writer http.ResponseWriter, request *http.Request) {
	if !session(request) {
		http.Redirect(writer, request, "/login", http.StatusFound)
	} else {
		cookie, _ := request.Cookie("_cookie")
		session, _ := data.SessionByUUID(cookie.Value)
		user, err := data.UserNoPasswordByID(session.UserId)
		if err != nil {
			logError(err, "Cannot get user from session")
		}
		topic := request.PostFormValue("topic")
		if _, err := user.CreateThread(topic); err != nil {
			logError(err, "Cannot create thread")
		}
		http.Redirect(writer, request, "/", http.StatusFound)
	}
}

// POST /thread/post
// Create the post
func postThread(writer http.ResponseWriter, request *http.Request) {
	if !session(request) {
		http.Redirect(writer, request, "/login", http.StatusFound)
	} else {
		cookie, _ := request.Cookie("_cookie")
		session, _ := data.SessionByUUID(cookie.Value)
		user, err := data.UserByID(session.UserId)
		if err != nil {
			logError(err, "Cannot get user from session")
		}
		body := request.PostFormValue("body")
		uuid := request.PostFormValue("uuid")
		thread, err := data.ThreadByUUID(uuid)
		if err != nil {
			error_message(writer, request, "Cannot read thread")
		}
		if _, err := user.CreatePost(thread, body); err != nil {
			logError(err, "Cannot create post")
		}
		url := fmt.Sprint("/thread/read?id=", uuid)
		http.Redirect(writer, request, url, http.StatusFound)
	}
}

// GET /thread/read
// Show the details of the thread, including the posts and the form to write a post
func readThread(writer http.ResponseWriter, request *http.Request) {
	vals := request.URL.Query()
	uuid := vals.Get("id")
	thread, err := data.ThreadByUUID(uuid)
	if err != nil {
		error_message(writer, request, "Cannot read thread")
	} else {
		if !session(request) {
			generateHTML(writer, &thread, "layout", "public.navbar", "public.thread")
		} else {
			generateHTML(writer, &thread, "layout", "private.navbar", "private.thread")
		}
	}
}

main.go

package main

import (
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()

	// index
	mux.HandleFunc("/", index)

	// error
	mux.HandleFunc("/err", err)

	// defined in route_auth.go
	mux.HandleFunc("/login", login)
	mux.HandleFunc("/logout", logout)
	mux.HandleFunc("/signup", signup)
	mux.HandleFunc("/signup_account", signupAccount)
	mux.HandleFunc("/authenticate", authenticate)

	// defined in route_thread.go
	mux.HandleFunc("/thread/new", newThread)
	mux.HandleFunc("/thread/create", createThread)
	mux.HandleFunc("/thread/post", postThread)
	mux.HandleFunc("/thread/read", readThread)

	// starting up the server
	server := &http.Server{
		Addr:           config.Address,
		Handler:        mux,
		ReadTimeout:    time.Duration(config.ReadTimeout * int64(time.Second)),
		WriteTimeout:   time.Duration(config.WriteTimeout * int64(time.Second)),
		MaxHeaderBytes: 1 << 20,
	}
	server.ListenAndServe()
}

运行及测试

go mod init chitchat
go get github.com/lib/pq

go run main.go route.go utils.go

curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/err?msg=test

curl -i http://127.0.0.1:8080/login
curl -i http://127.0.0.1:8080/logout
curl -i http://127.0.0.1:8080/signup

//创建用户
curl -v -d"name=test111&email=test111@136.com&password=test111" http://127.0.0.1:8080/signup_account

//用户验证,获取cookie并保存
curl -v -d"name=test111&email=test111@136.com&password=test111" -c cookie.txt http://127.0.0.1:8080/authenticate

curl -i -b cookie.txt http://127.0.0.1:8080/thread/new

//创建主题
curl -i -b cookie.txt -d"topic=mytest" http://127.0.0.1:8080/thread/create

//发布帖子
//uuid指thread的uuid,curl -i http://127.0.0.1:8080/可以查看
curl -i -b cookie.txt -d"body=contents&uuid=31115747-5b29-4a9f-55c4-2cf92c6e6150" http://127.0.0.1:8080/thread/post

//查看主题下的所有帖子
curl -i -b cookie.txt http://127.0.0.1:8080/thread/read?id=31115747-5b29-4a9f-55c4-2cf92c6e6150

//查看数据表
psql -p 5433 -U postgres -d chitchat -c "select * from users;"
psql -p 5433 -U postgres -d chitchat -c "select * from threads;"
psql -p 5433 -U postgres -d chitchat -c "select * from posts;"
psql -p 5433 -U postgres -d chitchat -c "select * from sessions;"

//sessions查看会话是否存在
psql -p 5433 -U postgres -d chitchat -c "SELECT count(*) FROM sessions WHERE uuid = '84cb28ba-2af4-466a-5ba1-cc0f6471d6dc' LIMIT 1;"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值