You've already forked go-url-shortener
short url web page v1
This commit is contained in:
199
handler/account.go
Normal file
199
handler/account.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zhang2092/go-url-shortener/db"
|
||||
"github.com/zhang2092/go-url-shortener/pkg/cookie"
|
||||
pwd "github.com/zhang2092/go-url-shortener/pkg/password"
|
||||
)
|
||||
|
||||
type registerPageData struct {
|
||||
Summary string
|
||||
Email string
|
||||
EmailMsg string
|
||||
Username string
|
||||
UsernameMsg string
|
||||
Password string
|
||||
PasswordMsg string
|
||||
}
|
||||
|
||||
func RegisterView(templates fs.FS) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
renderRegister(w, r, templates, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func Register(templates fs.FS, store db.Store) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
email := r.PostFormValue("email")
|
||||
username := r.PostFormValue("username")
|
||||
password := r.PostFormValue("password")
|
||||
resp, ok := viladatorRegister(email, username, password)
|
||||
if !ok {
|
||||
renderRegister(w, r, templates, resp)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := pwd.BcryptHashPassword(password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
arg := &db.CreateUserParams{
|
||||
ID: genId(),
|
||||
Username: username,
|
||||
HashedPassword: hashedPassword,
|
||||
Email: email,
|
||||
}
|
||||
|
||||
_, err = store.CreateUser(r.Context(), arg)
|
||||
if err != nil {
|
||||
if store.IsUniqueViolation(err) {
|
||||
resp.Summary = "邮箱或名称已经存在"
|
||||
renderRegister(w, r, templates, resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Summary = "请求网络错误,请刷新重试"
|
||||
renderRegister(w, r, templates, resp)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
type loginPageData struct {
|
||||
Summary string
|
||||
Email string
|
||||
EmailMsg string
|
||||
Password string
|
||||
PasswordMsg string
|
||||
}
|
||||
|
||||
func LoginView(templates fs.FS) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
renderLogin(w, r, templates, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func Login(templates fs.FS, store db.Store) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
renderLogin(w, r, templates, registerPageData{Summary: "请求网络错误,请刷新重试"})
|
||||
return
|
||||
}
|
||||
|
||||
email := r.PostFormValue("email")
|
||||
password := r.PostFormValue("password")
|
||||
resp, ok := viladatorLogin(email, password)
|
||||
if !ok {
|
||||
renderLogin(w, r, templates, resp)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
user, err := store.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if store.IsNoRows(sql.ErrNoRows) {
|
||||
resp.Summary = "邮箱或密码错误"
|
||||
renderLogin(w, r, templates, resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Summary = "请求网络错误,请刷新重试"
|
||||
renderLogin(w, r, templates, resp)
|
||||
return
|
||||
}
|
||||
|
||||
err = pwd.BcryptComparePassword(user.HashedPassword, password)
|
||||
if err != nil {
|
||||
resp.Summary = "邮箱或密码错误"
|
||||
renderLogin(w, r, templates, resp)
|
||||
return
|
||||
}
|
||||
|
||||
encoded, err := secureCookie.Encode(AuthorizeCookie, &Authorize{ID: user.ID, Name: user.Username})
|
||||
if err != nil {
|
||||
resp.Summary = "请求网络错误,请刷新重试(cookie)"
|
||||
renderLogin(w, r, templates, resp)
|
||||
return
|
||||
}
|
||||
|
||||
c := cookie.NewCookie(cookie.AuthorizeName, encoded, time.Now().Add(time.Duration(7200)*time.Second))
|
||||
http.SetCookie(w, c)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func Logout(templates fs.FS) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie.DeleteCookie(w, cookie.AuthorizeName)
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func renderRegister(w http.ResponseWriter, r *http.Request, templates fs.FS, data any) {
|
||||
renderLayout(w, r, templates, data, "user/register.html.tmpl")
|
||||
}
|
||||
|
||||
func renderLogin(w http.ResponseWriter, r *http.Request, templates fs.FS, data any) {
|
||||
renderLayout(w, r, templates, data, "user/login.html.tmpl")
|
||||
}
|
||||
|
||||
func viladatorRegister(email, username, password string) (registerPageData, bool) {
|
||||
ok := true
|
||||
resp := registerPageData{
|
||||
Email: email,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if !ValidateRxEmail(email) {
|
||||
resp.EmailMsg = "请填写正确的邮箱地址"
|
||||
ok = false
|
||||
}
|
||||
if !ValidateRxUsername(username) {
|
||||
resp.UsernameMsg = "名称(6-20,字母,数字)"
|
||||
ok = false
|
||||
}
|
||||
if !ValidatePassword(password) {
|
||||
resp.PasswordMsg = "密码(8-20位)"
|
||||
ok = false
|
||||
}
|
||||
|
||||
return resp, ok
|
||||
}
|
||||
|
||||
func viladatorLogin(email, password string) (loginPageData, bool) {
|
||||
ok := true
|
||||
errs := loginPageData{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if !ValidateRxEmail(email) {
|
||||
errs.EmailMsg = "请填写正确的邮箱地址"
|
||||
ok = false
|
||||
}
|
||||
if len(password) == 0 {
|
||||
errs.PasswordMsg = "请填写正确的密码"
|
||||
ok = false
|
||||
}
|
||||
|
||||
return errs, ok
|
||||
}
|
||||
31
handler/base.go
Normal file
31
handler/base.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/securecookie"
|
||||
)
|
||||
|
||||
const (
|
||||
AuthorizeCookie = "authorize"
|
||||
ContextUser ctxKey = "context_user"
|
||||
)
|
||||
|
||||
var (
|
||||
secureCookie *securecookie.SecureCookie
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
|
||||
type Authorize struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func genId() string {
|
||||
id, _ := uuid.NewRandom()
|
||||
return id.String()
|
||||
}
|
||||
|
||||
func SetSecureCookie(sc *securecookie.SecureCookie) {
|
||||
secureCookie = sc
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/zhang2092/go-url-shortener/shortener"
|
||||
"github.com/zhang2092/go-url-shortener/store"
|
||||
)
|
||||
|
||||
type UrlCreationRequest struct {
|
||||
LongUrl string `json:"long_url"`
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
type UrlCreationResponse struct {
|
||||
Message string `json:"message"`
|
||||
ShortUrl string `json:"short_url"`
|
||||
}
|
||||
|
||||
func CreateShortUrl(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
var req UrlCreationRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid parameter", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
shortUrl, err := shortener.GenerateShortLink(req.LongUrl, req.UserId)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to generate short link", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = store.SaveUrlMapping(shortUrl, req.LongUrl, req.UserId)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to store url mapping", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
scheme := "http://"
|
||||
if r.TLS != nil {
|
||||
scheme = "https://"
|
||||
}
|
||||
|
||||
res := &UrlCreationResponse{
|
||||
Message: "short url created successfully",
|
||||
ShortUrl: scheme + r.Host + "/" + shortUrl,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func HandleShortUrlRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
shorUrl := vars["shortUrl"]
|
||||
link, err := store.RetrieveInitialUrl(shorUrl)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to get url", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(link) == 0 {
|
||||
http.Error(w, "short url get to long url is empty", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, link, http.StatusFound)
|
||||
}
|
||||
28
handler/home.go
Normal file
28
handler/home.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/zhang2092/go-url-shortener/db"
|
||||
)
|
||||
|
||||
func HomeView(templates fs.FS, store db.Store) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := withUser(ctx)
|
||||
result, err := store.ListUrlByUser(ctx, user.ID)
|
||||
if err != nil {
|
||||
renderLayout(w, r, templates, nil, "home.html.tmpl")
|
||||
}
|
||||
|
||||
scheme := "http://"
|
||||
if r.TLS != nil {
|
||||
scheme = "https://"
|
||||
}
|
||||
for _, item := range result {
|
||||
item.ShortUrl = scheme + r.Host + "/" + item.ShortUrl
|
||||
}
|
||||
renderLayout(w, r, templates, result, "home.html.tmpl")
|
||||
}
|
||||
}
|
||||
48
handler/middleware.go
Normal file
48
handler/middleware.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func MyAuthorize(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
u := withUser(r.Context())
|
||||
if u == nil {
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func SetUser(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(AuthorizeCookie)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
u := Authorize{}
|
||||
err = secureCookie.Decode(AuthorizeCookie, cookie.Value, &u)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, ContextUser, u)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func withUser(ctx context.Context) *Authorize {
|
||||
val := ctx.Value(ContextUser)
|
||||
if u, ok := val.(Authorize); ok {
|
||||
return &u
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
38
handler/render.go
Normal file
38
handler/render.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/zhang2092/go-url-shortener/pkg/logger"
|
||||
)
|
||||
|
||||
// renderLayout 渲染方法 带框架
|
||||
func renderLayout(w http.ResponseWriter, r *http.Request, templates fs.FS, data any, tmpl string) {
|
||||
t := template.New(filepath.Base(tmpl))
|
||||
t = t.Funcs(template.FuncMap{
|
||||
"csrfField": func() template.HTML {
|
||||
return csrf.TemplateField(r)
|
||||
},
|
||||
"currentUser": func() *Authorize {
|
||||
return withUser(r.Context())
|
||||
},
|
||||
})
|
||||
|
||||
tpl := template.Must(t.Clone())
|
||||
tpl, err := tpl.ParseFS(templates, tmpl, "base/header.html.tmpl", "base/footer.html.tmpl")
|
||||
if err != nil {
|
||||
logger.Logger.Errorf("template parse: %s, %v", tmpl, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tpl.Execute(w, data); err != nil {
|
||||
logger.Logger.Errorf("template execute: %s, %v", tmpl, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
80
handler/short_url.go
Normal file
80
handler/short_url.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/zhang2092/go-url-shortener/db"
|
||||
"github.com/zhang2092/go-url-shortener/service"
|
||||
"github.com/zhang2092/go-url-shortener/shortener"
|
||||
)
|
||||
|
||||
func CreateShortUrlView(templates fs.FS) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
renderLayout(w, r, templates, nil, "short_url/create.html.tmpl")
|
||||
}
|
||||
}
|
||||
|
||||
func CreateShortUrl(templates fs.FS, store db.Store) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
renderCreateShortUrl(w, r, templates, map[string]string{"Error": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
user := withUser(ctx)
|
||||
longUrl := r.PostFormValue("long_url")
|
||||
shortUrl, err := shortener.GenerateShortLink(longUrl, user.ID)
|
||||
if err != nil {
|
||||
renderCreateShortUrl(w, r, templates, map[string]string{"Error": "生成短路径错误"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Println(shortUrl)
|
||||
|
||||
_, err = store.CreateUserUrl(ctx, &db.CreateUserUrlParams{
|
||||
UserID: user.ID,
|
||||
ShortUrl: shortUrl,
|
||||
OriginUrl: longUrl,
|
||||
ExpireAt: time.Now().Add(time.Hour * 6),
|
||||
})
|
||||
if err != nil {
|
||||
renderCreateShortUrl(w, r, templates, map[string]string{"Error": "短路径存储错误"})
|
||||
return
|
||||
}
|
||||
|
||||
err = service.SaveUrlMapping(shortUrl, longUrl, user.ID)
|
||||
if err != nil {
|
||||
renderCreateShortUrl(w, r, templates, map[string]string{"Error": "短路径存储错误"})
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleShortUrlRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
shorUrl := vars["shortUrl"]
|
||||
link, err := service.RetrieveInitialUrl(shorUrl)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to get url", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(link) == 0 {
|
||||
http.Error(w, "short url get to long url is empty", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, link, http.StatusFound)
|
||||
}
|
||||
|
||||
func renderCreateShortUrl(w http.ResponseWriter, r *http.Request, templates fs.FS, data any) {
|
||||
renderLayout(w, r, templates, data, "short_url/create.html.tmpl")
|
||||
}
|
||||
38
handler/validator.go
Normal file
38
handler/validator.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
rxPhone = regexp.MustCompile(`^(13|14|15|16|17|18|19)\d{9}$`)
|
||||
rxEmail = regexp.MustCompile(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`)
|
||||
rxUsername = regexp.MustCompile(`^[a-z0-9A-Z]{6,20}$`) // 6到20位(字母,数字)
|
||||
//rxPassword = regexp.MustCompile(`^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9]{8,18}$`) // 最少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符
|
||||
)
|
||||
|
||||
func ValidateRxPhone(phone string) bool {
|
||||
phone = strings.TrimSpace(phone)
|
||||
return rxPhone.MatchString(phone)
|
||||
}
|
||||
|
||||
func ValidateRxEmail(email string) bool {
|
||||
email = strings.TrimSpace(email)
|
||||
return rxEmail.MatchString(email)
|
||||
}
|
||||
|
||||
func ValidateRxUsername(username string) bool {
|
||||
username = strings.TrimSpace(username)
|
||||
return rxUsername.MatchString(username)
|
||||
}
|
||||
|
||||
// func ValidateRxPassword(password string) bool {
|
||||
// password = strings.TrimSpace(password)
|
||||
// return rxPassword.MatchString(password)
|
||||
// }
|
||||
|
||||
func ValidatePassword(password string) bool {
|
||||
password = strings.TrimSpace(password)
|
||||
return len(password) >= 8 && len(password) <= 20
|
||||
}
|
||||
Reference in New Issue
Block a user