Compare commits

...

12 Commits
main ... templ

Author SHA1 Message Date
200c48a22c Update 2024-12-19 14:01:14 +08:00
afbbf1ee31 fix: update html title 2024-12-11 17:45:28 +08:00
d005553dc7 rename templ.go to render.go 2024-12-11 17:42:24 +08:00
af08d9befc update assets use 2024-12-11 17:37:58 +08:00
f6c961adb6 remove unuse file 2024-12-11 17:13:51 +08:00
e9876b0c6f render html change templ 2024-12-11 17:11:53 +08:00
cff64975d7 remove print log and uuid generate 2024-12-10 09:44:17 +08:00
8a0cc68a22 update go mod libs 2024-12-10 09:17:23 +08:00
kenneth
de0cf3f4e6 fix:update response err 2023-12-25 18:01:39 +08:00
kenneth
b7c593c827 shutdown server before close db 2023-12-25 17:32:10 +08:00
kenneth
ae060a2a37 delete short url 2023-12-25 17:31:07 +08:00
kenneth
c1f4731669 short url web page v1 2023-12-25 17:04:22 +08:00
58 changed files with 2774 additions and 104 deletions

42
Makefile Normal file
View File

@ -0,0 +1,42 @@
DB_URL=postgresql://root:secret@localhost:5432/short_url?sslmode=disable
network:
docker network create url-short-network
redis:
docker run --name rd -d -p 6379:6379 redis:7.2.3 --requirepass "secret"
postgres:
docker run --name postgres --network url-short-network -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:16-alpine
createdb:
docker exec -it postgres createdb --username=root --owner=root short_url
dropdb:
docker exec -it postgres dropdb short_url
psql:
docker exec -it postgres psql -U root -d short_url
migrateinit:
migrate create -ext sql -dir db/schema -seq init_schema
migrateup:
migrate -path db/schema -database "$(DB_URL)" -verbose up
migratedown:
migrate -path db/schema -database "$(DB_URL)" -verbose down
sqlc:
sqlc generate
templ:
templ generate
test:
go test -v -cover ./...
server:
go run main.go
.PHONY: network redis postgres createdb dropdb psql migrateup migratedown sqlc templ test server

7
assets/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

17
assets/css/home.css Normal file
View File

@ -0,0 +1,17 @@
.my_table {
display: block;
max-width: 1280px;
}
.my_table tr {
display: inline-block;
width: 100%;
border: 1px solid #eee;
border-collapse: collapse;
}
.my_table tr td {
display: inline-block;
word-wrap: break-word;
padding: 2px 5px;
}

100
assets/css/index.css Normal file
View File

@ -0,0 +1,100 @@
@charset "utf-8";
html,
body {
/* 禁用空格键的滚动 */
scroll-behavior: auto;
}
.flex {
display: flex;
}
.flex-row {
flex-direction: row;
}
.flex-column {
flex-direction: column;
}
.justify-content {
justify-content: center;
}
.align-items {
align-items: center;
}
.navbar-wh {
padding: 0.2rem 1.6rem;
border-bottom: 1px solid rgba(0, 0, 0, .12);
}
.navbar-brand-fs {
font-size: 1rem;
}
.logo {
width: 2.2rem;
height: 2.2rem;
fill: currentColor;
/* color: #f44336; */
color: #007bff;
}
.oauth {
align-items: center;
list-style-type: none;
height: 40px;
margin-bottom: 0;
}
.oauth li {
margin-left: 10px;
}
.main {
max-width: 1280px;
width: 100%;
padding-bottom: 60px;
}
.main .title {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 30px;
}
.main .title a {
height: 35px;
}
.tip-box {
width: 100%;
background-color: rgb(229, 246, 253);
padding: 1rem;
border-radius: 3px;
display: flex;
margin-top: 10px;
}
.tip-box .tip-icon {
width: 24px;
margin-right: 12px;
}
.tip-box .tip-icon svg {
fill: currentColor;
color: #007bff;
}
.tip-box .tip-info p {
letter-spacing: 0.00938em;
font-size: 13px;
font-weight: 400;
color: rgb(1, 67, 97);
margin-bottom: 0;
line-height: 24px;
}

7
assets/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

7
assets/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
assets/js/home.js Normal file
View File

@ -0,0 +1,22 @@
$('.deleteShortUrl').click(function () {
let csrfToken = $('input[name="csrf_token"]').val()
let u = $(this).attr('data-short-url')
$.ajax({
url: '/delete-short-url/' + u,
type: 'POST',
cache: false,
processData: false,
contentType: false,
headers: {
"X-CSRF-Token": csrfToken
},
success: function (res) {
if (res.success) {
alert('删除成功');
window.location.reload();
} else {
alert('删除失败');
}
}
})
});

2
assets/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

22
go.mod
View File

@ -1,19 +1,33 @@
module github.com/zhang2092/go-url-shortener
go 1.21.5
go 1.23.4
require (
github.com/a-h/templ v0.2.793
github.com/google/uuid v1.6.0
github.com/gorilla/csrf v1.7.2
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
github.com/itchyny/base58-go v0.2.1
github.com/gorilla/securecookie v1.1.2
github.com/itchyny/base58-go v0.2.2
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.3.1
github.com/lib/pq v1.10.9
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/redis/go-redis/v9 v9.7.0
github.com/stretchr/testify v1.8.4
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.30.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

46
go.sum
View File

@ -1,26 +1,60 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/itchyny/base58-go v0.2.1 h1:wtnhAVdOcW3WuHEASmGHMms4juOB8yEpj/KJxlB57+k=
github.com/itchyny/base58-go v0.2.1/go.mod h1:BNvrKeAtWNSca1GohNbyhfff9/v0IrZjzWCAGeAvZZE=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/itchyny/base58-go v0.2.2 h1:pswMT6rW2nRoELk5Mi8+xGLQPmDnlNnCwbfRCl2p7Mo=
github.com/itchyny/base58-go v0.2.2/go.mod h1:e7aEDHyQXm42jniwyoi+MaUeUdeWp58C5H20rTe52co=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -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)
}

BIN
internal/.DS_Store vendored Normal file

Binary file not shown.

31
internal/db/db.go Normal file
View File

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

27
internal/db/models.go Normal file
View File

@ -0,0 +1,27 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
package db
import (
"time"
)
type User struct {
ID string `json:"id"`
Username string `json:"username"`
HashedPassword string `json:"hashed_password"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type UserRelateUrl struct {
ID int64 `json:"id"`
UserID string `json:"user_id"`
ShortUrl string `json:"short_url"`
OriginUrl string `json:"origin_url"`
Status int32 `json:"status"`
ExpireAt time.Time `json:"expire_at"`
CreatedAt time.Time `json:"created_at"`
}

24
internal/db/querier.go Normal file
View File

@ -0,0 +1,24 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
package db
import (
"context"
)
type Querier interface {
CreateUser(ctx context.Context, arg *CreateUserParams) (*User, error)
CreateUserUrl(ctx context.Context, arg *CreateUserUrlParams) (*UserRelateUrl, error)
DeleteUser(ctx context.Context, id string) error
GetUser(ctx context.Context, id string) (*User, error)
GetUserByEmail(ctx context.Context, email string) (*User, error)
GetUserByName(ctx context.Context, username string) (*User, error)
ListUrlByUser(ctx context.Context, userID string) ([]*UserRelateUrl, error)
ListUsers(ctx context.Context, arg *ListUsersParams) ([]*User, error)
UpdateStatus(ctx context.Context, arg *UpdateStatusParams) (*UserRelateUrl, error)
UpdateUser(ctx context.Context, arg *UpdateUserParams) (*User, error)
}
var _ Querier = (*Queries)(nil)

View File

@ -0,0 +1,36 @@
-- name: CreateUser :one
INSERT INTO users (
id, username, hashed_password, email
) VALUES (
$1, $2, $3, $4
)
RETURNING *;
-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;
-- name: UpdateUser :one
UPDATE users
SET hashed_password = $2,
email = $3
WHERE id = $1
RETURNING *;
-- name: GetUser :one
SELECT * FROM users
WHERE id = $1 LIMIT 1;
-- name: GetUserByName :one
SELECT * FROM users
WHERE username = $1 LIMIT 1;
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1 LIMIT 1;
-- name: ListUsers :many
SELECT * FROM users
ORDER BY id
LIMIT $1
OFFSET $2;

View File

@ -0,0 +1,19 @@
-- name: CreateUserUrl :one
INSERT INTO user_relate_url (
user_id, short_url, origin_url, status, expire_at
) VALUES (
$1, $2, $3, 0, $4
)
RETURNING *;
-- name: UpdateStatus :one
UPDATE user_relate_url
SET status = $2
WHERE short_url = $1
RETURNING *;
-- name: ListUrlByUser :many
SELECT *
FROM user_relate_url
WHERE user_id = $1
ORDER BY id DESC;

View File

@ -0,0 +1,2 @@
DROP TABLE "user_relate_url";
DROP TABLE "users";

View File

@ -0,0 +1,27 @@
CREATE TABLE "users" (
"id" varchar NOT NULL PRIMARY KEY,
"username" varchar NOT NULL,
"hashed_password" varchar NOT NULL,
"email" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
ALTER TABLE "users" ADD CONSTRAINT "username_key" UNIQUE ("username");
ALTER TABLE "users" ADD CONSTRAINT "email_key" UNIQUE ("email");
CREATE INDEX ON "users" ("username");
CREATE INDEX ON "users" ("email");
CREATE TABLE "user_relate_url" (
"id" bigserial NOT NULL PRIMARY KEY,
"user_id" varchar NOT NULL,
"short_url" varchar NOT NULL,
"origin_url" varchar NOT NULL,
"status" int NOT NULL DEFAULT 0,
"expire_at" timestamptz NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
ALTER TABLE "user_relate_url" ADD CONSTRAINT "short_url_key" UNIQUE ("short_url");
CREATE INDEX ON "user_relate_url" ("user_id");
CREATE INDEX ON "user_relate_url" ("short_url");

61
internal/db/store.go Normal file
View File

@ -0,0 +1,61 @@
package db
import (
"context"
"database/sql"
"fmt"
"github.com/lib/pq"
)
type Store interface {
Querier
ExecTx(ctx context.Context, fn func(*Queries) error) error
IsUniqueViolation(err error) bool
IsForeignKeyViolation(err error) bool
IsNoRows(err error) bool
}
type SQLStore struct {
db *sql.DB
*Queries
}
func NewStore(db *sql.DB) Store {
return &SQLStore{
db: db,
Queries: New(db),
}
}
func (store *SQLStore) ExecTx(ctx context.Context, fn func(*Queries) error) error {
tx, err := store.db.BeginTx(ctx, nil)
if err != nil {
return err
}
q := New(tx)
err = fn(q)
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return fmt.Errorf("tx err: %v, rb err: %v", err, rbErr)
}
return err
}
return tx.Commit()
}
func (store *SQLStore) IsUniqueViolation(err error) bool {
pqErr, ok := err.(*pq.Error)
return ok && pqErr.Code == "23505"
}
func (store *SQLStore) IsForeignKeyViolation(err error) bool {
pqErr, ok := err.(*pq.Error)
return ok && pqErr.Code == "23503"
}
func (store *SQLStore) IsNoRows(err error) bool {
return err == sql.ErrNoRows
}

176
internal/db/user.sql.go Normal file
View File

@ -0,0 +1,176 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
// source: user.sql
package db
import (
"context"
)
const createUser = `-- name: CreateUser :one
INSERT INTO users (
id, username, hashed_password, email
) VALUES (
$1, $2, $3, $4
)
RETURNING id, username, hashed_password, email, created_at
`
type CreateUserParams struct {
ID string `json:"id"`
Username string `json:"username"`
HashedPassword string `json:"hashed_password"`
Email string `json:"email"`
}
func (q *Queries) CreateUser(ctx context.Context, arg *CreateUserParams) (*User, error) {
row := q.db.QueryRowContext(ctx, createUser,
arg.ID,
arg.Username,
arg.HashedPassword,
arg.Email,
)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return &i, err
}
const deleteUser = `-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, id string) error {
_, err := q.db.ExecContext(ctx, deleteUser, id)
return err
}
const getUser = `-- name: GetUser :one
SELECT id, username, hashed_password, email, created_at FROM users
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetUser(ctx context.Context, id string) (*User, error) {
row := q.db.QueryRowContext(ctx, getUser, id)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return &i, err
}
const getUserByEmail = `-- name: GetUserByEmail :one
SELECT id, username, hashed_password, email, created_at FROM users
WHERE email = $1 LIMIT 1
`
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (*User, error) {
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return &i, err
}
const getUserByName = `-- name: GetUserByName :one
SELECT id, username, hashed_password, email, created_at FROM users
WHERE username = $1 LIMIT 1
`
func (q *Queries) GetUserByName(ctx context.Context, username string) (*User, error) {
row := q.db.QueryRowContext(ctx, getUserByName, username)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return &i, err
}
const listUsers = `-- name: ListUsers :many
SELECT id, username, hashed_password, email, created_at FROM users
ORDER BY id
LIMIT $1
OFFSET $2
`
type ListUsersParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListUsers(ctx context.Context, arg *ListUsersParams) ([]*User, error) {
rows, err := q.db.QueryContext(ctx, listUsers, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []*User{}
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, &i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET hashed_password = $2,
email = $3
WHERE id = $1
RETURNING id, username, hashed_password, email, created_at
`
type UpdateUserParams struct {
ID string `json:"id"`
HashedPassword string `json:"hashed_password"`
Email string `json:"email"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg *UpdateUserParams) (*User, error) {
row := q.db.QueryRowContext(ctx, updateUser, arg.ID, arg.HashedPassword, arg.Email)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return &i, err
}

View File

@ -0,0 +1,112 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
// source: user_relate_url.sql
package db
import (
"context"
"time"
)
const createUserUrl = `-- name: CreateUserUrl :one
INSERT INTO user_relate_url (
user_id, short_url, origin_url, status, expire_at
) VALUES (
$1, $2, $3, 0, $4
)
RETURNING id, user_id, short_url, origin_url, status, expire_at, created_at
`
type CreateUserUrlParams struct {
UserID string `json:"user_id"`
ShortUrl string `json:"short_url"`
OriginUrl string `json:"origin_url"`
ExpireAt time.Time `json:"expire_at"`
}
func (q *Queries) CreateUserUrl(ctx context.Context, arg *CreateUserUrlParams) (*UserRelateUrl, error) {
row := q.db.QueryRowContext(ctx, createUserUrl,
arg.UserID,
arg.ShortUrl,
arg.OriginUrl,
arg.ExpireAt,
)
var i UserRelateUrl
err := row.Scan(
&i.ID,
&i.UserID,
&i.ShortUrl,
&i.OriginUrl,
&i.Status,
&i.ExpireAt,
&i.CreatedAt,
)
return &i, err
}
const listUrlByUser = `-- name: ListUrlByUser :many
SELECT id, user_id, short_url, origin_url, status, expire_at, created_at
FROM user_relate_url
WHERE user_id = $1
ORDER BY id DESC
`
func (q *Queries) ListUrlByUser(ctx context.Context, userID string) ([]*UserRelateUrl, error) {
rows, err := q.db.QueryContext(ctx, listUrlByUser, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []*UserRelateUrl{}
for rows.Next() {
var i UserRelateUrl
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.ShortUrl,
&i.OriginUrl,
&i.Status,
&i.ExpireAt,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, &i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateStatus = `-- name: UpdateStatus :one
UPDATE user_relate_url
SET status = $2
WHERE short_url = $1
RETURNING id, user_id, short_url, origin_url, status, expire_at, created_at
`
type UpdateStatusParams struct {
ShortUrl string `json:"short_url"`
Status int32 `json:"status"`
}
func (q *Queries) UpdateStatus(ctx context.Context, arg *UpdateStatusParams) (*UserRelateUrl, error) {
row := q.db.QueryRowContext(ctx, updateStatus, arg.ShortUrl, arg.Status)
var i UserRelateUrl
err := row.Scan(
&i.ID,
&i.UserID,
&i.ShortUrl,
&i.OriginUrl,
&i.Status,
&i.ExpireAt,
&i.CreatedAt,
)
return &i, err
}

172
internal/handler/account.go Normal file
View File

@ -0,0 +1,172 @@
package handler
import (
"database/sql"
"net/http"
"time"
"github.com/google/uuid"
"github.com/zhang2092/go-url-shortener/internal/db"
"github.com/zhang2092/go-url-shortener/internal/middleware"
"github.com/zhang2092/go-url-shortener/internal/pkg/cookie"
pwd "github.com/zhang2092/go-url-shortener/internal/pkg/password"
"github.com/zhang2092/go-url-shortener/internal/templ"
"github.com/zhang2092/go-url-shortener/internal/templ/models"
)
func RegisterView(w http.ResponseWriter, r *http.Request) {
templ.Register(w, r, &models.RegisterPageData{})
}
func Register(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 {
templ.Register(w, r, resp)
return
}
hashedPassword, err := pwd.BcryptHashPassword(password)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
arg := &db.CreateUserParams{
ID: uuid.Must(uuid.NewRandom()).String(),
Username: username,
HashedPassword: hashedPassword,
Email: email,
}
_, err = store.CreateUser(r.Context(), arg)
if err != nil {
if store.IsUniqueViolation(err) {
resp.Summary = "邮箱或名称已经存在"
templ.Register(w, r, resp)
return
}
resp.Summary = "请求网络错误,请刷新重试"
templ.Register(w, r, resp)
return
}
http.Redirect(w, r, "/login", http.StatusFound)
}
}
func LoginView(w http.ResponseWriter, r *http.Request) {
templ.Login(w, r, &models.LoginPageData{})
}
func Login(store db.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if err := r.ParseForm(); err != nil {
templ.Login(w, r, &models.LoginPageData{Summary: "请求网络错误,请刷新重试"})
return
}
email := r.PostFormValue("email")
password := r.PostFormValue("password")
resp, ok := viladatorLogin(email, password)
if !ok {
templ.Login(w, r, resp)
return
}
ctx := r.Context()
user, err := store.GetUserByEmail(ctx, email)
if err != nil {
if store.IsNoRows(sql.ErrNoRows) {
resp.Summary = "邮箱或密码错误"
templ.Login(w, r, resp)
return
}
resp.Summary = "请求网络错误,请刷新重试"
templ.Login(w, r, resp)
return
}
err = pwd.BcryptComparePassword(user.HashedPassword, password)
if err != nil {
resp.Summary = "邮箱或密码错误"
templ.Login(w, r, resp)
return
}
encoded, err := middleware.Encode(middleware.AuthorizeCookie, &middleware.Authorize{ID: user.ID, Name: user.Username})
if err != nil {
resp.Summary = "请求网络错误,请刷新重试(cookie)"
templ.Login(w, r, 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() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie.DeleteCookie(w, cookie.AuthorizeName)
http.Redirect(w, r, "/login", http.StatusFound)
}
}
func viladatorRegister(email, username, password string) (*models.RegisterPageData, bool) {
ok := true
resp := &models.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) (*models.LoginPageData, bool) {
ok := true
errs := &models.LoginPageData{
Email: email,
Password: password,
}
if !ValidateRxEmail(email) {
errs.EmailMsg = "请填写正确的邮箱地址"
ok = false
}
if len(password) == 0 {
errs.PasswordMsg = "请填写正确的密码"
ok = false
}
return errs, ok
}

22
internal/handler/home.go Normal file
View File

@ -0,0 +1,22 @@
package handler
import (
"net/http"
"github.com/zhang2092/go-url-shortener/internal/db"
"github.com/zhang2092/go-url-shortener/internal/middleware"
"github.com/zhang2092/go-url-shortener/internal/templ"
)
func HomeView(store db.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := middleware.GetUser(ctx)
result, err := store.ListUrlByUser(ctx, user.ID)
if err != nil {
templ.Home(w, r, nil)
return
}
templ.Home(w, r, result)
}
}

55
internal/handler/resp.go Normal file
View File

@ -0,0 +1,55 @@
package handler
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
)
type response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data any `json:"data"`
}
func respond(w http.ResponseWriter, message string, v any, statusCode int) {
rsp := response{
Success: true,
Message: message,
Data: v,
}
b, err := json.Marshal(rsp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(statusCode)
_, err = w.Write(b)
if err != nil && !errors.Is(err, context.Canceled) {
log.Printf("could not write http response: %v\n", err)
}
}
func RespondErr(w http.ResponseWriter, message string, v any) {
rsp := response{
Success: false,
Message: message,
Data: v,
}
b, err := json.Marshal(rsp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write(b)
if err != nil && !errors.Is(err, context.Canceled) {
log.Printf("could not write http response: %v\n", err)
}
}

View File

@ -0,0 +1,90 @@
package handler
import (
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/zhang2092/go-url-shortener/internal/db"
"github.com/zhang2092/go-url-shortener/internal/middleware"
"github.com/zhang2092/go-url-shortener/internal/service"
"github.com/zhang2092/go-url-shortener/internal/shortener"
"github.com/zhang2092/go-url-shortener/internal/templ"
)
func CreateShortUrlView(w http.ResponseWriter, r *http.Request) {
templ.CreateUrl(w, r, "")
}
func CreateShortUrl(store db.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if err := r.ParseForm(); err != nil {
templ.CreateUrl(w, r, "请求参数错误")
return
}
ctx := r.Context()
user := middleware.GetUser(ctx)
longUrl := r.PostFormValue("long_url")
shortUrl, err := shortener.GenerateShortLink(longUrl, user.ID)
if err != nil {
templ.CreateUrl(w, r, "生成短路径错误")
return
}
_, err = store.CreateUserUrl(ctx, &db.CreateUserUrlParams{
UserID: user.ID,
ShortUrl: shortUrl,
OriginUrl: longUrl,
ExpireAt: time.Now().Add(time.Hour * 6),
})
if err != nil {
templ.CreateUrl(w, r, "短路径存储错误")
return
}
err = service.SaveUrlMapping(shortUrl, longUrl, user.ID)
if err != nil {
templ.CreateUrl(w, r, "短路径存储错误")
return
}
http.Redirect(w, r, "/", http.StatusFound)
}
}
func DeleteShortUrl(store db.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
shorUrl := vars["shortUrl"]
_, err := store.UpdateStatus(r.Context(), &db.UpdateStatusParams{
ShortUrl: shorUrl,
Status: -1,
})
if err != nil {
RespondErr(w, "删除错误", nil)
return
}
err = service.DeleteShortUrl(shorUrl)
if err != nil {
RespondErr(w, "删除错误", nil)
return
}
respond(w, "删除成功", nil, http.StatusOK)
}
}
func HandleShortUrlRedirect(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
shorUrl := vars["shortUrl"]
link, err := service.RetrieveInitialUrl(shorUrl)
if err != nil || len(link) == 0 {
RespondErr(w, "短链已经失效", nil)
return
}
http.Redirect(w, r, link, http.StatusFound)
}

View 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
}

View File

@ -0,0 +1,77 @@
package middleware
import (
"context"
"net/http"
"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 SetSecureCookie(sc *securecookie.SecureCookie) {
secureCookie = sc
}
func Encode(name string, value any) (string, error) {
return secureCookie.Encode(name, value)
}
func MyAuthorize(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := GetUser(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
}
if cookie == nil || len(cookie.Value) == 0 {
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 GetUser(ctx context.Context) *Authorize {
val := ctx.Value(ContextUser)
if u, ok := val.(Authorize); ok {
return &u
}
return nil
}

View File

@ -0,0 +1,41 @@
package cookie
import (
"net/http"
"time"
)
const (
AuthorizeName = "authorize"
)
func NewCookie(name, value string, expired time.Time) *http.Cookie {
return &http.Cookie{
Name: name,
Value: value,
Path: "/",
Secure: false, // true->只能https站点操作
HttpOnly: true, // true->js不能捕获
Expires: expired,
}
}
func SetCookie(w http.ResponseWriter, name, value string, expired time.Time) {
cookie := NewCookie(name, value, expired)
http.SetCookie(w, cookie)
}
func ReadCookie(r *http.Request, name string) (string, error) {
cookie, err := r.Cookie(name)
if err != nil {
return "", err
}
return cookie.Value, nil
}
func DeleteCookie(w http.ResponseWriter, name string) {
cookie := NewCookie(name, "", time.Now().Add(time.Duration(-10)*time.Second))
cookie.MaxAge = -1
http.SetCookie(w, cookie)
}

View File

@ -0,0 +1,33 @@
package logger
import (
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Logger *zap.SugaredLogger
func NewLogger() {
core := zapcore.NewCore(getEncoder(), getLogWriter(), zapcore.DebugLevel)
logger := zap.New(core, zap.AddCaller())
Logger = logger.Sugar()
}
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "./log/run.log", // 日志文件的位置
MaxSize: 10, // 在进行切割之前日志文件的最大大小以MB为单位
MaxBackups: 100, // 保留旧文件的最大个数
MaxAge: 365, // 保留旧文件的最大天数
Compress: false, // 是否压缩/归档旧文件
}
return zapcore.AddSync(lumberJackLogger)
}

View File

@ -0,0 +1,79 @@
package password
import (
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt"
)
// ******************** scrypt ********************
// ScryptHashPassword scrypt 加密
// password 原始密码
func ScryptHashPassword(password string) (string, error) {
// example for making salt - https://play.golang.org/p/_Aw6WeWC42I
salt := make([]byte, 32)
_, err := rand.Read(salt)
if err != nil {
return "", err
}
// using recommended cost parameters from - https://godoc.org/golang.org/x/crypto/scrypt
shash, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
if err != nil {
return "", err
}
// return hex-encoded string with salt appended to password
hashedPW := fmt.Sprintf("%s.%s", hex.EncodeToString(shash), hex.EncodeToString(salt))
return hashedPW, nil
}
// ScryptComparePassword 判断密码是否正确
// storedPassword 加密密码
// suppliedPassword 原始密码
func ScryptComparePassword(storedPassword string, suppliedPassword string) error {
pwsalt := strings.Split(storedPassword, ".")
// check supplied password salted with hash
salt, err := hex.DecodeString(pwsalt[1])
if err != nil {
return fmt.Errorf("unable to verify user password")
}
shash, err := scrypt.Key([]byte(suppliedPassword), salt, 32768, 8, 1, 32)
if err != nil {
return err
}
if hex.EncodeToString(shash) == pwsalt[0] {
return nil
}
return fmt.Errorf("password error")
}
// ******************** bcrypt ********************
// BcryptHashPassword bcrypt 加密
// password 原始密码
func BcryptHashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hashedPassword), nil
}
// BcryptComparePassword 判断密码是否正确
// hashedPassword 加密密码
// password 原始密码
func BcryptComparePassword(hashedPassword string, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

View File

@ -1,4 +1,4 @@
package store
package service
import (
"context"
@ -52,3 +52,7 @@ func RetrieveInitialUrl(shortUrl string) (string, error) {
return result, nil
}
func DeleteShortUrl(shortUrl string) error {
return storeService.redisClient.Set(ctx, shortUrl, "", time.Second).Err()
}

View File

@ -15,7 +15,8 @@ func sha256Of(input string) []byte {
}
func base58Encoded(bytes []byte) (string, error) {
encoded, err := base58.BitcoinEncoding.Encode(bytes)
encoding := base58.BitcoinEncoding
encoded, err := encoding.Encode(bytes)
if err != nil {
return "", err
}
@ -31,5 +32,5 @@ func GenerateShortLink(originUrl string, userId string) (string, error) {
return "", err
}
return result, nil
return result[:8], nil
}

View File

@ -12,15 +12,15 @@ func TestGenerateShortLink(t *testing.T) {
link1 := "https://www.baidu.com/"
short1, err := GenerateShortLink(link1, userId)
assert.NoError(t, err)
assert.Equal(t, short1, "egtq236P5f3")
assert.Equal(t, short1, "egtq236P")
link2 := "https://www.163.com/"
short2, err := GenerateShortLink(link2, userId)
assert.NoError(t, err)
assert.Equal(t, short2, "DiCqg9YpV89")
assert.Equal(t, short2, "DiCqg9Yp")
link3 := "https://www.qq.com/"
short3, err := GenerateShortLink(link3, userId)
assert.NoError(t, err)
assert.Equal(t, short3, "4QhQ62cZem1")
assert.Equal(t, short3, "4QhQ62cZ")
}

BIN
internal/templ/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,67 @@
package auth
import (
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"github.com/zhang2092/go-url-shortener/internal/templ/models"
"github.com/zhang2092/go-url-shortener/internal/templ/util"
"net/http"
)
templ Login(r *http.Request, form *models.LoginPageData) {
@base.Base(r, nil, nil) {
<div class="container">
<div class="flex flex-column align-items row py-md-5 mt-md-5">
<h1>登录</h1>
<div class="col-sm-4 py-md-5">
<form action="/login" method="post">
@templ.Raw(util.CsrfField(r))
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">邮箱</span>
</div>
<input
type="email"
name="email"
class="form-control"
required
id="email"
value={ form.Email }
aria-describedby="emailValid"
/>
</div>
if form.EmailMsg != "" {
<small id="emailValid" style="color: #f44336;" class="form-text">{ form.EmailMsg }</small>
}
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">密码</span>
</div>
<input
type="password"
name="password"
class="form-control"
required
id="password"
value={ form.Password }
aria-describedby="passwordValid"
/>
</div>
if form.PasswordMsg!= "" {
<small id="passwordValid" style="color: #f44336;" class="form-text">{ form.PasswordMsg }</small>
}
</div>
<button type="submit" class="btn btn-primary btn-block">提交</button>
</form>
if form.Summary!= "" {
<div class="py-md-5" style="color: #f44336;">
{ form.Summary }
</div>
}
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,168 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package auth
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"github.com/zhang2092/go-url-shortener/internal/templ/models"
"github.com/zhang2092/go-url-shortener/internal/templ/util"
"net/http"
)
func Login(r *http.Request, form *models.LoginPageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><div class=\"flex flex-column align-items row py-md-5 mt-md-5\"><h1>登录</h1><div class=\"col-sm-4 py-md-5\"><form action=\"/login\" method=\"post\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(util.CsrfField(r)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"form-group\"><div class=\"input-group\"><div class=\"input-group-prepend\"><span class=\"input-group-text\">邮箱</span></div><input type=\"email\" name=\"email\" class=\"form-control\" required id=\"email\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/login.templ`, Line: 29, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-describedby=\"emailValid\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if form.EmailMsg != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<small id=\"emailValid\" style=\"color: #f44336;\" class=\"form-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(form.EmailMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/login.templ`, Line: 34, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</small>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"form-group\"><div class=\"input-group\"><div class=\"input-group-prepend\"><span class=\"input-group-text\">密码</span></div><input type=\"password\" name=\"password\" class=\"form-control\" required id=\"password\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(form.Password)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/login.templ`, Line: 48, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-describedby=\"passwordValid\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if form.PasswordMsg != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<small id=\"passwordValid\" style=\"color: #f44336;\" class=\"form-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(form.PasswordMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/login.templ`, Line: 53, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</small>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><button type=\"submit\" class=\"btn btn-primary btn-block\">提交</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if form.Summary != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"py-md-5\" style=\"color: #f44336;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(form.Summary)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/login.templ`, Line: 60, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base.Base(r, nil, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,86 @@
package auth
import (
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"github.com/zhang2092/go-url-shortener/internal/templ/models"
"github.com/zhang2092/go-url-shortener/internal/templ/util"
"net/http"
)
templ Register(r *http.Request, form *models.RegisterPageData) {
@base.Base(r, nil, nil) {
<div class="container">
<div class="flex flex-column align-items row py-md-5 mt-md-5">
<h1>注册</h1>
<div class="col-sm-4 py-md-5">
<form action="/register" method="post">
@templ.Raw(util.CsrfField(r))
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">邮箱</span>
</div>
<input
type="email"
name="email"
class="form-control"
required
id="email"
value={ form.Email }
aria-describedby="emailValid"
/>
</div>
if form.Email!="" {
<small id="emailValid" style="color: #f44336;" class="form-text">{ form.EmailMsg }</small>
}
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">名称</span>
</div>
<input
type="text"
name="username"
class="form-control"
required
id="username"
value={ form.Username }
aria-describedby="usernameValid"
/>
</div>
if form.Username!="" {
<small id="usernameValid" style="color: #f44336;" class="form-text">{ form.UsernameMsg }</small>
}
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">密码</span>
</div>
<input
type="password"
name="password"
class="form-control"
required
id="password"
value={ form.Password }
aria-describedby="passwordValid"
/>
</div>
if form.Password!="" {
<small id="passwordValid" style="color: #f44336;" class="form-text">{ form.PasswordMsg }</small>
}
</div>
<button type="submit" class="btn btn-primary btn-block">提交</button>
</form>
if form.Summary != "" {
<div class="py-md-5" style="color: #f44336;">
{ form.Summary }
</div>
}
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,204 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package auth
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"github.com/zhang2092/go-url-shortener/internal/templ/models"
"github.com/zhang2092/go-url-shortener/internal/templ/util"
"net/http"
)
func Register(r *http.Request, form *models.RegisterPageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><div class=\"flex flex-column align-items row py-md-5 mt-md-5\"><h1>注册</h1><div class=\"col-sm-4 py-md-5\"><form action=\"/register\" method=\"post\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(util.CsrfField(r)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"form-group\"><div class=\"input-group\"><div class=\"input-group-prepend\"><span class=\"input-group-text\">邮箱</span></div><input type=\"email\" name=\"email\" class=\"form-control\" required id=\"email\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/register.templ`, Line: 29, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-describedby=\"emailValid\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if form.Email != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<small id=\"emailValid\" style=\"color: #f44336;\" class=\"form-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(form.EmailMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/register.templ`, Line: 34, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</small>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"form-group\"><div class=\"input-group\"><div class=\"input-group-prepend\"><span class=\"input-group-text\">名称</span></div><input type=\"text\" name=\"username\" class=\"form-control\" required id=\"username\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(form.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/register.templ`, Line: 48, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-describedby=\"usernameValid\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if form.Username != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<small id=\"usernameValid\" style=\"color: #f44336;\" class=\"form-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(form.UsernameMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/register.templ`, Line: 53, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</small>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"form-group\"><div class=\"input-group\"><div class=\"input-group-prepend\"><span class=\"input-group-text\">密码</span></div><input type=\"password\" name=\"password\" class=\"form-control\" required id=\"password\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(form.Password)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/register.templ`, Line: 67, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-describedby=\"passwordValid\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if form.Password != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<small id=\"passwordValid\" style=\"color: #f44336;\" class=\"form-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(form.PasswordMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/register.templ`, Line: 72, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</small>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><button type=\"submit\" class=\"btn btn-primary btn-block\">提交</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if form.Summary != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"py-md-5\" style=\"color: #f44336;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(form.Summary)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/auth/register.templ`, Line: 79, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base.Base(r, nil, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,53 @@
package base
import (
"github.com/zhang2092/go-url-shortener/internal/templ/util"
"net/http"
)
templ Base(r *http.Request, css templ.Component, js templ.Component) {
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<link rel="shortcut icon" href="/assets/favicon.ico" type="image/x-icon"/>
<link rel="stylesheet" href="/assets/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/assets/css/index.css"/>
@css
<title>URL短地址服务</title>
</head>
<body>
<div class="wrapper">
<nav class="navbar navbar-light bg-light navbar-wh">
<a class="navbar-brand navbar-brand-fs" href="/">
URL短地址服务
</a>
<ul class="flex oauth">
{{ auth := util.GetAuthorize(ctx) }}
if auth != nil {
<li style="font-size: 12px;">
欢迎您: { auth.Name }
</li>
<li style="font-size: 12px;">
<a href="/logout" class="btn btn-primary btn-sm">退出</a>
</li>
} else {
<li>
<a href="/login" class="btn btn-outline-primary btn-sm">登录</a>
</li>
<li>
<a href="/register" class="btn btn-primary btn-sm">注册</a>
</li>
}
</ul>
</nav>
</div>
{ children... }
<input type="hidden" id="csrf_token" value={ util.CsrfToken(r) }/>
<script src="/assets/js/jquery.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
@js
</body>
</html>
}

View File

@ -0,0 +1,111 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package base
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/zhang2092/go-url-shortener/internal/templ/util"
"net/http"
)
func Base(r *http.Request, css templ.Component, js templ.Component) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"><link rel=\"shortcut icon\" href=\"/assets/favicon.ico\" type=\"image/x-icon\"><link rel=\"stylesheet\" href=\"/assets/css/bootstrap.min.css\"><link rel=\"stylesheet\" href=\"/assets/css/index.css\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = css.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<title>URL短地址服务</title></head><body><div class=\"wrapper\"><nav class=\"navbar navbar-light bg-light navbar-wh\"><a class=\"navbar-brand navbar-brand-fs\" href=\"/\">URL短地址服务</a><ul class=\"flex oauth\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
auth := util.GetAuthorize(ctx)
if auth != nil {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li style=\"font-size: 12px;\">欢迎您: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(auth.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/base/base.templ`, Line: 30, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li><li style=\"font-size: 12px;\"><a href=\"/logout\" class=\"btn btn-primary btn-sm\">退出</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a href=\"/login\" class=\"btn btn-outline-primary btn-sm\">登录</a></li><li><a href=\"/register\" class=\"btn btn-primary btn-sm\">注册</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></nav></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<input type=\"hidden\" id=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(util.CsrfToken(r))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/base/base.templ`, Line: 47, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><script src=\"/assets/js/jquery.min.js\"></script><script src=\"/assets/js/bootstrap.bundle.min.js\"></script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = js.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,13 @@
package err
import (
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"net/http"
)
templ Error404(r *http.Request) {
@base.Base(r, nil, nil) {
<h1>404</h1>
<p>当前短路径已经失效</p>
}
}

View File

@ -0,0 +1,63 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package err
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"net/http"
)
func Error404(r *http.Request) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h1>404</h1><p>当前短路径已经失效</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base.Base(r, nil, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,57 @@
package home
import (
"github.com/zhang2092/go-url-shortener/internal/db"
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"net/http"
)
templ css() {
<link rel="stylesheet" href="/assets/css/home.css"/>
}
templ js() {
<script href="/assets/js/home.js"></script>
}
templ Home(r *http.Request, data []*db.UserRelateUrl) {
@base.Base(r, css(), js()) {
<div class="container-fluid flex justify-content">
<div class="main">
<h3 style="margin-top: 20px;margin-bottom: 10px;">
短地址列表
<a
class="btn btn-primary"
href="/create-short-url"
>添加</a>
</h3>
<table class="my_table" style="display: block;">
<tr>
<th width="600px">原地址</th>
<th width="320px">短地址</th>
<th width="80px">是否有效</th>
<th width="80px">删除</th>
</tr>
for _, item := range data {
<tr>
<td width="600px">{ item.OriginUrl }</td>
<td width="320px"><a target="_blank" href={ templ.URL(item.ShortUrl) }>{ item.ShortUrl }</a></td>
<td width="80px">
if item.Status == 0 {
<code>YES</code>
} else {
<code>NO</code>
}
</td>
<td width="80px">
if item.Status == 0 {
<button data-short-url={ item.ShortUrl } class="btn btn-danger deleteShortUrl">删除</button>
}
</td>
</tr>
}
</table>
</div>
</div>
}
}

View File

@ -0,0 +1,205 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package home
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/zhang2092/go-url-shortener/internal/db"
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"net/http"
)
func css() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<link rel=\"stylesheet\" href=\"/assets/css/home.css\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func js() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script href=\"/assets/js/home.js\"></script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func Home(r *http.Request, data []*db.UserRelateUrl) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container-fluid flex justify-content\"><div class=\"main\"><h3 style=\"margin-top: 20px;margin-bottom: 10px;\">短地址列表 <a class=\"btn btn-primary\" href=\"/create-short-url\">添加</a></h3><table class=\"my_table\" style=\"display: block;\"><tr><th width=\"600px\">原地址</th><th width=\"320px\">短地址</th><th width=\"80px\">是否有效</th><th width=\"80px\">删除</th></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range data {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td width=\"600px\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.OriginUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/home/home.templ`, Line: 37, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td width=\"320px\"><a target=\"_blank\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 templ.SafeURL = templ.URL(item.ShortUrl)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var6)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.ShortUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/home/home.templ`, Line: 38, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></td><td width=\"80px\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Status == 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<code>YES</code>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<code>NO</code>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td width=\"80px\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Status == 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button data-short-url=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.ShortUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templ/home/home.templ`, Line: 48, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"btn btn-danger deleteShortUrl\">删除</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</table></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base.Base(r, css(), js()).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,19 @@
package models
type LoginPageData struct {
Summary string
Email string
EmailMsg string
Password string
PasswordMsg string
}
type RegisterPageData struct {
Summary string
Email string
EmailMsg string
Username string
UsernameMsg string
Password string
PasswordMsg string
}

51
internal/templ/render.go Normal file
View File

@ -0,0 +1,51 @@
package templ
import (
"net/http"
"github.com/gorilla/csrf"
"github.com/zhang2092/go-url-shortener/internal/db"
"github.com/zhang2092/go-url-shortener/internal/middleware"
"github.com/zhang2092/go-url-shortener/internal/templ/auth"
"github.com/zhang2092/go-url-shortener/internal/templ/err"
"github.com/zhang2092/go-url-shortener/internal/templ/home"
"github.com/zhang2092/go-url-shortener/internal/templ/models"
"github.com/zhang2092/go-url-shortener/internal/templ/url"
)
func Login(w http.ResponseWriter, r *http.Request, form *models.LoginPageData) {
checkErr(w, auth.Login(r, form).Render(r.Context(), w))
}
func Register(w http.ResponseWriter, r *http.Request, form *models.RegisterPageData) {
checkErr(w, auth.Register(r, form).Render(r.Context(), w))
}
func Home(w http.ResponseWriter, r *http.Request, data []*db.UserRelateUrl) {
checkErr(w, home.Home(r, data).Render(r.Context(), w))
}
func CreateUrl(w http.ResponseWriter, r *http.Request, errorMsg string) {
checkErr(w, url.CreateUrl(r, errorMsg).Render(r.Context(), w))
}
func Error404(w http.ResponseWriter, r *http.Request) {
checkErr(w, err.Error404(r).Render(r.Context(), w))
}
func defaultData(r *http.Request, data map[string]any) map[string]any {
if data == nil {
data = make(map[string]any, 3)
}
data["AuthUser"] = middleware.GetUser(r.Context())
data["CsrfToken"] = csrf.Token(r)
data["CsrfTokenField"] = csrf.TemplateField(r)
return data
}
func checkErr(w http.ResponseWriter, err error) {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View File

@ -0,0 +1,34 @@
package url
import (
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"github.com/zhang2092/go-url-shortener/internal/templ/util"
"net/http"
)
templ CreateUrl(r *http.Request, errorMsg string) {
@base.Base(r, nil, nil) {
<div class="container">
<div class="flex flex-column align-items row py-md-5 mt-md-5">
<h1>创建短路径</h1>
<div class="col-sm-4 py-md-5">
<form action="/create-short-url" method="post">
@templ.Raw(util.CsrfField(r))
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">原路径</span>
</div>
<input type="text" name="long_url" class="form-control" required id="long_url"/>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">创建</button>
</form>
if errorMsg != "" {
<div class="py-md-5" style="color: #f44336;"></div>
}
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,82 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package url
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/zhang2092/go-url-shortener/internal/templ/base"
"github.com/zhang2092/go-url-shortener/internal/templ/util"
"net/http"
)
func CreateUrl(r *http.Request, errorMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><div class=\"flex flex-column align-items row py-md-5 mt-md-5\"><h1>创建短路径</h1><div class=\"col-sm-4 py-md-5\"><form action=\"/create-short-url\" method=\"post\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(util.CsrfField(r)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"form-group\"><div class=\"input-group\"><div class=\"input-group-prepend\"><span class=\"input-group-text\">原路径</span></div><input type=\"text\" name=\"long_url\" class=\"form-control\" required id=\"long_url\"></div></div><button type=\"submit\" class=\"btn btn-primary btn-block\">创建</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errorMsg != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"py-md-5\" style=\"color: #f44336;\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base.Base(r, nil, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,11 @@
package util
import (
"context"
"github.com/zhang2092/go-url-shortener/internal/middleware"
)
func GetAuthorize(ctx context.Context) *middleware.Authorize {
return middleware.GetUser(ctx)
}

View File

@ -0,0 +1,16 @@
package util
import (
"html/template"
"net/http"
"github.com/gorilla/csrf"
)
func CsrfField(r *http.Request) template.HTML {
return csrf.TemplateField(r)
}
func CsrfToken(r *http.Request) string {
return csrf.Token(r)
}

View File

@ -0,0 +1,45 @@
package util
import "strings"
func GetResourcesFile(path ...string) ([]string, []string) {
var csses []string
var jses []string
if len(path) > 0 {
for _, p := range path {
if strings.HasSuffix(p, ".css") {
csses = append(csses, p)
} else if strings.HasSuffix(p, ".js") {
jses = append(jses, p)
}
}
}
return csses, jses
}
func GetCssFile(path ...string) []string {
var res []string
if len(path) > 0 {
for _, p := range path {
if strings.HasSuffix(p, ".css") {
res = append(res, p)
}
}
}
return res
}
func GetJsFile(path ...string) []string {
var res []string
if len(path) > 0 {
for _, p := range path {
if strings.HasSuffix(p, ".js") {
res = append(res, p)
}
}
}
return res
}

6
log/run.log Normal file
View File

@ -0,0 +1,6 @@
2023-12-25T15:32:56.864+0800 ERROR handler/render.go:28 template parse: user/login.html.tmpl, template: header.html.tmpl:22: function "currentUser" not defined
2023-12-25T15:32:57.541+0800 ERROR handler/render.go:28 template parse: user/login.html.tmpl, template: header.html.tmpl:22: function "currentUser" not defined
2023-12-25T15:33:21.637+0800 ERROR handler/render.go:28 template parse: user/login.html.tmpl, template: header.html.tmpl:22: function "currentUser" not defined
2023-12-25T15:33:56.806+0800 ERROR handler/render.go:28 template parse: user/register.html.tmpl, template: header.html.tmpl:22: function "currentUser" not defined
2023-12-25T15:34:12.152+0800 ERROR handler/render.go:28 template parse: user/login.html.tmpl, template: header.html.tmpl:22: function "currentUser" not defined
2023-12-25T15:34:59.057+0800 ERROR handler/render.go:31 template parse: user/register.html.tmpl, template: header.html.tmpl:22: function "currentUser" not defined

78
main.go
View File

@ -2,7 +2,10 @@ package main
import (
"context"
"database/sql"
"embed"
"flag"
"io/fs"
"log"
"net/http"
"os"
@ -10,12 +13,21 @@ import (
"strconv"
"time"
"github.com/gorilla/csrf"
hds "github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/joho/godotenv"
"github.com/zhang2092/go-url-shortener/handler"
"github.com/zhang2092/go-url-shortener/store"
"github.com/zhang2092/go-url-shortener/internal/db"
"github.com/zhang2092/go-url-shortener/internal/handler"
"github.com/zhang2092/go-url-shortener/internal/middleware"
"github.com/zhang2092/go-url-shortener/internal/pkg/logger"
"github.com/zhang2092/go-url-shortener/internal/service"
)
//go:embed assets
var assetsFS embed.FS
func main() {
var local bool
flag.BoolVar(&local, "debug", true, "server running in debug?")
@ -27,20 +39,65 @@ func main() {
}
}
logger.NewLogger()
// Set up assets
assets, err := fs.Sub(assetsFS, "assets")
if err != nil {
log.Fatal(err)
}
addr := os.Getenv("REDIS_ADDR")
password := os.Getenv("REDIS_PASSWORD")
db, err := strconv.Atoi(os.Getenv("REDIS_DB"))
redisDb, err := strconv.Atoi(os.Getenv("REDIS_DB"))
if err != nil {
log.Fatalf("failed to get redis db index: %v", err)
}
store.InitializeStore(addr, password, db)
service.InitializeStore(addr, password, redisDb)
conn, err := sql.Open(os.Getenv("DB_DRIVER"), os.Getenv("DB_SOURCE"))
if err != nil {
log.Fatal("cannot connect to db: ", err)
}
store := db.NewStore(conn)
hashKey := securecookie.GenerateRandomKey(32)
blockKey := securecookie.GenerateRandomKey(32)
middleware.SetSecureCookie(securecookie.New(hashKey, blockKey))
router := mux.NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Wecome to the URL Shortener API"))
}).Methods(http.MethodGet)
router.HandleFunc("/create-short-url", handler.CreateShortUrl).Methods(http.MethodPost)
router.Use(mux.CORSMethodMiddleware(router))
router.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.FS(assets))))
csrfMiddleware := csrf.Protect(
[]byte(securecookie.GenerateRandomKey(32)),
csrf.Secure(false),
csrf.HttpOnly(true),
csrf.FieldName("csrf_token"),
csrf.CookieName("authorize_csrf"),
)
router.Use(csrfMiddleware)
router.Use(middleware.SetUser)
router.Handle("/register", hds.MethodHandler{
http.MethodGet: http.HandlerFunc(handler.RegisterView),
http.MethodPost: http.Handler(handler.Register(store)),
})
router.Handle("/login", hds.MethodHandler{
http.MethodGet: http.HandlerFunc(handler.LoginView),
http.MethodPost: http.Handler(handler.Login(store)),
})
router.Handle("/logout", handler.Logout()).Methods(http.MethodGet)
subRouter := router.PathPrefix("/").Subrouter()
subRouter.Use(middleware.MyAuthorize)
subRouter.Handle("/", handler.HomeView(store)).Methods(http.MethodGet)
subRouter.Handle("/create-short-url", hds.MethodHandler{
http.MethodGet: http.HandlerFunc(handler.CreateShortUrlView),
http.MethodPost: http.Handler(handler.CreateShortUrl(store)),
})
subRouter.Handle("/delete-short-url/{shortUrl}", handler.DeleteShortUrl(store)).Methods(http.MethodPost)
router.HandleFunc("/{shortUrl}", handler.HandleShortUrlRedirect).Methods(http.MethodGet)
srv := &http.Server{
@ -62,7 +119,8 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
store.CloseStoreRedisConn()
service.CloseStoreRedisConn()
conn.Close()
srv.Shutdown(ctx)
log.Println("shutting down")

17
sqlc.yaml Normal file
View File

@ -0,0 +1,17 @@
version: "2"
sql:
- engine: "postgresql"
queries: "./db/query/"
schema: "./db/schema/"
gen:
go:
package: "db"
out: "./db/"
emit_json_tags: true
emit_prepared_queries: false
emit_interface: true
emit_exact_table_names: false
emit_empty_slices: true
emit_result_struct_pointers: true
emit_params_struct_pointers: true