Compare commits

..

3 Commits
web ... main

Author SHA1 Message Date
79c4aa06d0 update go mod libs 2024-12-10 09:14:12 +08:00
kenneth
6d3dc79330
Update shortener_test.go 2023-12-25 17:03:29 +08:00
kenneth
8ca34713bd
Update shortener.go 2023-12-25 17:03:07 +08:00
44 changed files with 89 additions and 1781 deletions

View File

@ -1,39 +0,0 @@
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
test:
go test -v -cover ./...
server:
go run main.go
.PHONY: network redis postgres createdb dropdb psql migrateup migratedown sqlc test server

View File

@ -1,31 +0,0 @@
// 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,
}
}

View File

@ -1,27 +0,0 @@
// 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"`
}

View File

@ -1,24 +0,0 @@
// 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

@ -1,36 +0,0 @@
-- 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

@ -1,19 +0,0 @@
-- 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

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

View File

@ -1,27 +0,0 @@
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");

View File

@ -1,61 +0,0 @@
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
}

View File

@ -1,176 +0,0 @@
// 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

@ -1,112 +0,0 @@
// 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
}

13
go.mod
View File

@ -3,30 +3,17 @@ module github.com/zhang2092/go-url-shortener
go 1.23.3
require (
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/gorilla/securecookie v1.1.2
github.com/itchyny/base58-go v0.2.2
github.com/joho/godotenv v1.5.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/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
)

30
go.sum
View File

@ -1,5 +1,3 @@
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/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=
@ -10,47 +8,19 @@ 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/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/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.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,199 +0,0 @@
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
}

View File

@ -1,28 +0,0 @@
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 {
return uuid.Must(uuid.NewRandom()).String()
}
func SetSecureCookie(sc *securecookie.SecureCookie) {
secureCookie = sc
}

78
handler/handlers.go Normal file
View File

@ -0,0 +1,78 @@
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)
}

View File

@ -1,20 +0,0 @@
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")
}
renderLayout(w, r, templates, result, "home.html.tmpl")
}
}

View File

@ -1,48 +0,0 @@
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
}

View File

@ -1,45 +0,0 @@
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())
},
"genShortUrl": func(url string) string {
scheme := "http://"
if r.TLS != nil {
scheme = "https://"
}
return scheme + r.Host + "/" + url
},
})
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
}
}

View File

@ -1,55 +0,0 @@
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

@ -1,99 +0,0 @@
package handler
import (
"io/fs"
"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
}
_, 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 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 {
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")
}

View File

@ -1,38 +0,0 @@
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

@ -1,6 +0,0 @@
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

84
main.go
View File

@ -2,10 +2,7 @@ package main
import (
"context"
"database/sql"
"embed"
"flag"
"io/fs"
"log"
"net/http"
"os"
@ -13,23 +10,12 @@ 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/db"
"github.com/zhang2092/go-url-shortener/handler"
"github.com/zhang2092/go-url-shortener/pkg/logger"
"github.com/zhang2092/go-url-shortener/service"
"github.com/zhang2092/go-url-shortener/store"
)
//go:embed web/template
var templateFS embed.FS
//go:embed web/static
var staticFS embed.FS
func main() {
var local bool
flag.BoolVar(&local, "debug", true, "server running in debug?")
@ -41,71 +27,20 @@ func main() {
}
}
logger.NewLogger()
// Set up templates
templates, err := fs.Sub(templateFS, "web/template")
if err != nil {
log.Fatal(err)
}
// Set up statics
statics, err := fs.Sub(staticFS, "web/static")
if err != nil {
log.Fatal(err)
}
addr := os.Getenv("REDIS_ADDR")
password := os.Getenv("REDIS_PASSWORD")
redisDb, err := strconv.Atoi(os.Getenv("REDIS_DB"))
db, err := strconv.Atoi(os.Getenv("REDIS_DB"))
if err != nil {
log.Fatalf("failed to get redis db index: %v", err)
}
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)
handler.SetSecureCookie(securecookie.New(hashKey, blockKey))
store.InitializeStore(addr, password, db)
router := mux.NewRouter()
router.Use(mux.CORSMethodMiddleware(router))
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(statics))))
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(handler.SetUser)
router.Handle("/register", hds.MethodHandler{
http.MethodGet: http.Handler(handler.RegisterView(templates)),
http.MethodPost: http.Handler(handler.Register(templates, store)),
})
router.Handle("/login", hds.MethodHandler{
http.MethodGet: http.Handler(handler.LoginView(templates)),
http.MethodPost: http.Handler(handler.Login(templates, store)),
})
router.Handle("/logout", handler.Logout(templates)).Methods(http.MethodGet)
subRouter := router.PathPrefix("/").Subrouter()
subRouter.Use(handler.MyAuthorize)
subRouter.Handle("/", handler.HomeView(templates, store)).Methods(http.MethodGet)
subRouter.Handle("/create-short-url", hds.MethodHandler{
http.MethodGet: http.Handler(handler.CreateShortUrlView(templates)),
http.MethodPost: http.Handler(handler.CreateShortUrl(templates, store)),
})
subRouter.Handle("/delete-short-url/{shortUrl}", handler.DeleteShortUrl(store)).Methods(http.MethodPost)
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.HandleFunc("/{shortUrl}", handler.HandleShortUrlRedirect).Methods(http.MethodGet)
srv := &http.Server{
@ -127,8 +62,7 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
service.CloseStoreRedisConn()
conn.Close()
store.CloseStoreRedisConn()
srv.Shutdown(ctx)
log.Println("shutting down")

View File

@ -1,41 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -1,79 +0,0 @@
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

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

View File

@ -1,17 +0,0 @@
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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,100 +0,0 @@
@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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
<!-- <!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="stylesheet" href="/statics/css/bootstrap.min.css" />
<title>HLS流媒体</title>
{{block "css" .}}{{end}}
</head>
<body>
<div class="wrapper"> -->
{{define "footer"}}
</div>
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/bootstrap.bundle.min.js"></script>
{{block "js" .}}{{end}}
</body>
</html>
{{end}}

View File

@ -1,44 +0,0 @@
{{define "header"}}
<!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="/statics/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/index.css" />
{{block "css" .}}{{end}}
<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 currentUser}}
<li style="font-size: 12px;">
欢迎您: {{ currentUser.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>
{{end}}
</ul>
</nav>
{{end}}
<!-- </div>
<script src="/statics/js/jquery.slim.min.js"></script>
<script src="/statics/js/bootstrap.bundle.min.js"></script>
</body>
</html> -->

View File

@ -1,83 +0,0 @@
{{template "header" .}}
<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>
{{range .}}
<tr>
<td width="600px">{{.OriginUrl}}</td>
<td width="320px"><a target="_blank" href="{{genShortUrl .ShortUrl}}">{{genShortUrl .ShortUrl}}</a></td>
<td width="80px">
{{if eq .Status 0}}
<code>YES</code>
{{else}}
<code>NO</code>
{{end}}
</td>
<td width="80px">
{{if eq .Status 0}}
<button data-short-url="{{.ShortUrl}}" class="btn btn-danger deleteShortUrl">删除</button>
{{end}}
</td>
</tr>
{{end}}
</table>
{{ csrfField }}
</div>
</div>
{{define "css"}}
<style>
.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;
}
</style>
{{end}}
{{define "js"}}
<script>
$('.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('删除失败');
}
}
})
});
</script>
{{end}}
{{template "footer" .}}

View File

@ -1,25 +0,0 @@
{{template "header" .}}
<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">
{{ csrfField }}
<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 .}}
<div class="py-md-5" style="color: #f44336;">
</div>
{{end}}
</div>
</div>
</div>
{{template "footer" .}}

View File

@ -1,42 +0,0 @@
{{template "header" .}}
<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">
{{ csrfField }}
<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="{{.Email}}"
aria-describedby="emailValid">
</div>
{{if .EmailMsg}}
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailMsg}}</small>
{{end}}
</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="{{.Password}}" aria-describedby="passwordValid">
</div>
{{if .PasswordMsg}}
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordMsg}}</small>
{{end}}
</div>
<button type="submit" class="btn btn-primary btn-block">提交</button>
</form>
{{if .Summary}}
<div class="py-md-5" style="color: #f44336;">
{{.Summary}}
</div>
{{end}}
</div>
</div>
</div>
{{template "footer" .}}

View File

@ -1,54 +0,0 @@
{{template "header" .}}
<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">
{{ csrfField }}
<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="{{.Email}}"
aria-describedby="emailValid">
</div>
{{if .EmailMsg}}
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailMsg}}</small>
{{end}}
</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="{{.Username}}" aria-describedby="usernameValid">
</div>
{{if .UsernameMsg}}
<small id="usernameValid" style="color: #f44336;" class="form-text">{{.UsernameMsg}}</small>
{{end}}
</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="{{.Password}}" aria-describedby="passwordValid">
</div>
{{if .PasswordMsg}}
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordMsg}}</small>
{{end}}
</div>
<button type="submit" class="btn btn-primary btn-block">提交</button>
</form>
{{if .Summary}}
<div class="py-md-5" style="color: #f44336;">
{{.Summary}}
</div>
{{end}}
</div>
</div>
</div>
{{template "footer" .}}