Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79c4aa06d0 | |||
|
|
6d3dc79330 | ||
|
|
8ca34713bd |
39
Makefile
39
Makefile
@ -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
|
|
||||||
31
db/db.go
31
db/db.go
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
db/models.go
27
db/models.go
@ -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"`
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
DROP TABLE "user_relate_url";
|
|
||||||
DROP TABLE "users";
|
|
||||||
@ -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");
|
|
||||||
61
db/store.go
61
db/store.go
@ -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
|
|
||||||
}
|
|
||||||
176
db/user.sql.go
176
db/user.sql.go
@ -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
|
|
||||||
}
|
|
||||||
@ -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
13
go.mod
@ -3,30 +3,17 @@ module github.com/zhang2092/go-url-shortener
|
|||||||
go 1.23.3
|
go 1.23.3
|
||||||
|
|
||||||
require (
|
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/mux v1.8.1
|
||||||
github.com/gorilla/securecookie v1.1.2
|
|
||||||
github.com/itchyny/base58-go v0.2.2
|
github.com/itchyny/base58-go v0.2.2
|
||||||
github.com/joho/godotenv v1.5.1
|
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/redis/go-redis/v9 v9.7.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.uber.org/zap v1.27.0
|
|
||||||
golang.org/x/crypto v0.30.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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
|
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
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
30
go.sum
30
go.sum
@ -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 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
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 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/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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
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 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
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 h1:pswMT6rW2nRoELk5Mi8+xGLQPmDnlNnCwbfRCl2p7Mo=
|
||||||
github.com/itchyny/base58-go v0.2.2/go.mod h1:e7aEDHyQXm42jniwyoi+MaUeUdeWp58C5H20rTe52co=
|
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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
78
handler/handlers.go
Normal 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)
|
||||||
|
}
|
||||||
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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")
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
84
main.go
@ -2,10 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"embed"
|
|
||||||
"flag"
|
"flag"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -13,23 +10,12 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
|
||||||
hds "github.com/gorilla/handlers"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/zhang2092/go-url-shortener/db"
|
|
||||||
"github.com/zhang2092/go-url-shortener/handler"
|
"github.com/zhang2092/go-url-shortener/handler"
|
||||||
"github.com/zhang2092/go-url-shortener/pkg/logger"
|
"github.com/zhang2092/go-url-shortener/store"
|
||||||
"github.com/zhang2092/go-url-shortener/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed web/template
|
|
||||||
var templateFS embed.FS
|
|
||||||
|
|
||||||
//go:embed web/static
|
|
||||||
var staticFS embed.FS
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var local bool
|
var local bool
|
||||||
flag.BoolVar(&local, "debug", true, "server running in debug?")
|
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")
|
addr := os.Getenv("REDIS_ADDR")
|
||||||
password := os.Getenv("REDIS_PASSWORD")
|
password := os.Getenv("REDIS_PASSWORD")
|
||||||
redisDb, err := strconv.Atoi(os.Getenv("REDIS_DB"))
|
db, err := strconv.Atoi(os.Getenv("REDIS_DB"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to get redis db index: %v", err)
|
log.Fatalf("failed to get redis db index: %v", err)
|
||||||
}
|
}
|
||||||
service.InitializeStore(addr, password, redisDb)
|
store.InitializeStore(addr, password, db)
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
router.Use(mux.CORSMethodMiddleware(router))
|
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(statics))))
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Wecome to the URL Shortener API"))
|
||||||
csrfMiddleware := csrf.Protect(
|
}).Methods(http.MethodGet)
|
||||||
[]byte(securecookie.GenerateRandomKey(32)),
|
router.HandleFunc("/create-short-url", handler.CreateShortUrl).Methods(http.MethodPost)
|
||||||
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("/{shortUrl}", handler.HandleShortUrlRedirect).Methods(http.MethodGet)
|
router.HandleFunc("/{shortUrl}", handler.HandleShortUrlRedirect).Methods(http.MethodGet)
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
@ -127,8 +62,7 @@ func main() {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
service.CloseStoreRedisConn()
|
store.CloseStoreRedisConn()
|
||||||
conn.Close()
|
|
||||||
|
|
||||||
srv.Shutdown(ctx)
|
srv.Shutdown(ctx)
|
||||||
log.Println("shutting down")
|
log.Println("shutting down")
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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))
|
|
||||||
}
|
|
||||||
@ -15,8 +15,7 @@ func sha256Of(input string) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func base58Encoded(bytes []byte) (string, error) {
|
func base58Encoded(bytes []byte) (string, error) {
|
||||||
encoding := base58.BitcoinEncoding
|
encoded, err := base58.BitcoinEncoding.Encode(bytes)
|
||||||
encoded, err := encoding.Encode(bytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
17
sqlc.yaml
17
sqlc.yaml
@ -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
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package service
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -52,7 +52,3 @@ func RetrieveInitialUrl(shortUrl string) (string, error) {
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteShortUrl(shortUrl string) error {
|
|
||||||
return storeService.redisClient.Set(ctx, shortUrl, "", time.Second).Err()
|
|
||||||
}
|
|
||||||
7
web/static/css/bootstrap.min.css
vendored
7
web/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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;
|
|
||||||
}
|
|
||||||
7
web/static/js/bootstrap.bundle.min.js
vendored
7
web/static/js/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7
web/static/js/bootstrap.min.js
vendored
7
web/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
web/static/js/jquery.min.js
vendored
2
web/static/js/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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}}
|
|
||||||
@ -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> -->
|
|
||||||
@ -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" .}}
|
|
||||||
@ -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" .}}
|
|
||||||
@ -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" .}}
|
|
||||||
@ -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" .}}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user