More work
This commit is contained in:
parent
df4970c4c3
commit
3ba0b8dc26
14
Makefile
Normal file
14
Makefile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
JS_SOURCES = $(wildcard js/*.js) $(wildcard js/*/*.js)
|
||||||
|
|
||||||
|
all: static/bundle.js static/react-widgets
|
||||||
|
|
||||||
|
node_modules:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
static/bundle.js: $(JS_SOURCES) node_modules
|
||||||
|
browserify -t [ babelify --presets [ react ] ] js/main.js -o static/bundle.js
|
||||||
|
|
||||||
|
static/react-widgets: node_modules/react-widgets/dist node_modules
|
||||||
|
rsync -a node_modules/react-widgets/dist/ static/react-widgets/
|
||||||
|
|
||||||
|
.PHONY = all
|
28
db.go
Normal file
28
db.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"gopkg.in/gorp.v1"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorp.DbMap = initDB()
|
||||||
|
|
||||||
|
func initDB() *gorp.DbMap {
|
||||||
|
db, err := sql.Open("sqlite3", "file:lunch.sqlite?cache=shared&mode=rwc")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbmap := &gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
|
||||||
|
dbmap.AddTableWithName(User{}, "users").SetKeys(true, "UserId")
|
||||||
|
dbmap.AddTableWithName(Session{}, "sessions").SetKeys(true, "SessionId")
|
||||||
|
|
||||||
|
err = dbmap.CreateTablesIfNotExists()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbmap
|
||||||
|
}
|
36
errors.go
Normal file
36
errors.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
ErrorId int
|
||||||
|
ErrorString string
|
||||||
|
}
|
||||||
|
|
||||||
|
var error_codes = map[int]string{
|
||||||
|
1: "Not Signed In",
|
||||||
|
2: "Unauthorized Access",
|
||||||
|
3: "Invalid Request",
|
||||||
|
4: "User Exists",
|
||||||
|
// 5: "Connection Failed", //client-side error
|
||||||
|
999: "Internal Error",
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteError(w http.ResponseWriter, error_code int) {
|
||||||
|
msg, ok := error_codes[error_code]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("Error: WriteError received error code of %d", error_code)
|
||||||
|
msg = error_codes[999]
|
||||||
|
}
|
||||||
|
e := Error{error_code, msg}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
err := enc.Encode(e)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
84
main.go
Normal file
84
main.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate make
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"github.com/gorilla/context"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/fcgi"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serveFcgi bool
|
||||||
|
var baseDir string
|
||||||
|
var tmpDir string
|
||||||
|
var port int
|
||||||
|
var smtpServer string
|
||||||
|
var smtpPort int
|
||||||
|
var smtpUsername string
|
||||||
|
var smtpPassword string
|
||||||
|
var reminderEmail string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&baseDir, "base", "./", "Base directory for server")
|
||||||
|
flag.StringVar(&tmpDir, "tmp", "/tmp", "Directory to create temporary files in")
|
||||||
|
flag.IntVar(&port, "port", 80, "Port to serve API/files on")
|
||||||
|
flag.StringVar(&smtpServer, "smtp.server", "smtp.example.com", "SMTP server to send reminder emails from.")
|
||||||
|
flag.IntVar(&smtpPort, "smtp.port", 587, "SMTP server port to connect to")
|
||||||
|
flag.StringVar(&smtpUsername, "smtp.username", "moneygo", "SMTP username")
|
||||||
|
flag.StringVar(&smtpPassword, "smtp.password", "password", "SMTP password")
|
||||||
|
flag.StringVar(&reminderEmail, "email", "moneygo@example.com", "Email address to send reminder emails as.")
|
||||||
|
flag.BoolVar(&serveFcgi, "fcgi", false, "Serve via fcgi rather than http.")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
static_path := path.Join(baseDir, "static")
|
||||||
|
|
||||||
|
// Ensure base directory is valid
|
||||||
|
dir_err_str := "The base directory doesn't look like it contains the " +
|
||||||
|
"'static' directory. Check to make sure you're passing the right " +
|
||||||
|
"value to the -base argument."
|
||||||
|
static_dir, err := os.Stat(static_path)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
log.Fatal(dir_err_str)
|
||||||
|
}
|
||||||
|
if !static_dir.IsDir() {
|
||||||
|
log.Fatal(dir_err_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the logging flags to be printed
|
||||||
|
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, path.Join(baseDir, "static/index.html"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func staticHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, path.Join(baseDir, r.URL.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
servemux := http.NewServeMux()
|
||||||
|
servemux.HandleFunc("/", rootHandler)
|
||||||
|
servemux.HandleFunc("/static/", staticHandler)
|
||||||
|
servemux.HandleFunc("/session/", SessionHandler)
|
||||||
|
servemux.HandleFunc("/user/", UserHandler)
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", ":"+strconv.Itoa(port))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Serving on port %d out of directory: %s", port, baseDir)
|
||||||
|
if serveFcgi {
|
||||||
|
fcgi.Serve(listener, context.ClearHandler(servemux))
|
||||||
|
} else {
|
||||||
|
http.Serve(listener, context.ClearHandler(servemux))
|
||||||
|
}
|
||||||
|
}
|
36
package.json
Normal file
36
package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "lunch",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "A lunch management and tracking web application",
|
||||||
|
"main": "js/main.js",
|
||||||
|
"dependencies": {
|
||||||
|
"babel-preset-react": "^6.16.0",
|
||||||
|
"babelify": "^7.3.0",
|
||||||
|
"big.js": "^3.1.3",
|
||||||
|
"browserify": "^13.1.0",
|
||||||
|
"cldr-data": "^29.0.2",
|
||||||
|
"globalize": "^1.1.1",
|
||||||
|
"keymirror": "^0.1.1",
|
||||||
|
"react": "^15.3.2",
|
||||||
|
"react-addons-update": "^15.3.2",
|
||||||
|
"react-bootstrap": "^0.30.5",
|
||||||
|
"react-dom": "^15.3.2",
|
||||||
|
"react-redux": "^4.4.5",
|
||||||
|
"react-widgets": "^3.4.4",
|
||||||
|
"redux": "^3.6.0",
|
||||||
|
"redux-thunk": "^2.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.noughth.com/aclindsa/lunch.git"
|
||||||
|
},
|
||||||
|
"author": "Aaron Lindsay",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://git.noughth.com/aclindsa/lunch/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://git.noughth.com/aclindsa/lunch"
|
||||||
|
}
|
124
sessions.go
Normal file
124
sessions.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cookie_store = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
SessionId int64
|
||||||
|
SessionSecret string `json:"-"`
|
||||||
|
UserId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Write(w http.ResponseWriter) error {
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
return enc.Encode(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSession(r *http.Request) (*Session, error) {
|
||||||
|
var s Session
|
||||||
|
|
||||||
|
session, _ := cookie_store.Get(r, "moneygo")
|
||||||
|
_, ok := session.Values["session-secret"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("session-secret cookie not set")
|
||||||
|
}
|
||||||
|
s.SessionSecret = session.Values["session-secret"].(string)
|
||||||
|
|
||||||
|
err := DB.SelectOne(&s, "SELECT * from sessions where SessionSecret=?", s.SessionSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSessionIfExists(r *http.Request) {
|
||||||
|
session, err := GetSession(r)
|
||||||
|
if err == nil {
|
||||||
|
DB.Delete(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(w http.ResponseWriter, r *http.Request, userid int64) (*Session, error) {
|
||||||
|
s := Session{}
|
||||||
|
|
||||||
|
session, _ := cookie_store.Get(r, "moneygo")
|
||||||
|
|
||||||
|
session.Values["session-secret"] = string(securecookie.GenerateRandomKey(64))
|
||||||
|
s.SessionSecret = session.Values["session-secret"].(string)
|
||||||
|
s.UserId = userid
|
||||||
|
|
||||||
|
err := DB.Insert(&s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SessionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "POST" || r.Method == "PUT" {
|
||||||
|
user_json := r.PostFormValue("user")
|
||||||
|
if user_json == "" {
|
||||||
|
WriteError(w, 3 /*Invalid Request*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := User{}
|
||||||
|
err := user.Read(user_json)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 3 /*Invalid Request*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbuser, err := GetUserByUsername(user.Username)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 2 /*Unauthorized Access*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.HashPassword()
|
||||||
|
if user.PasswordHash != dbuser.PasswordHash {
|
||||||
|
WriteError(w, 2 /*Unauthorized Access*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteSessionIfExists(r)
|
||||||
|
|
||||||
|
session, err := NewSession(w, r, dbuser.UserId)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = session.Write(w)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if r.Method == "GET" {
|
||||||
|
s, err := GetSession(r)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 1 /*Not Signed In*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Write(w)
|
||||||
|
} else if r.Method == "DELETE" {
|
||||||
|
DeleteSessionIfExists(r)
|
||||||
|
WriteSuccess(w)
|
||||||
|
}
|
||||||
|
}
|
216
users.go
Normal file
216
users.go
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
UserId int64
|
||||||
|
Name string
|
||||||
|
Username string
|
||||||
|
Password string `db:"-"`
|
||||||
|
PasswordHash string `json:"-"`
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
const BogusPassword = "password"
|
||||||
|
|
||||||
|
type UserExistsError struct{}
|
||||||
|
|
||||||
|
func (ueu UserExistsError) Error() string {
|
||||||
|
return "User exists"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Write(w http.ResponseWriter) error {
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
return enc.Encode(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Read(json_str string) error {
|
||||||
|
dec := json.NewDecoder(strings.NewReader(json_str))
|
||||||
|
return dec.Decode(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) HashPassword() {
|
||||||
|
password_hasher := sha256.New()
|
||||||
|
io.WriteString(password_hasher, u.Password)
|
||||||
|
u.PasswordHash = fmt.Sprintf("%x", password_hasher.Sum(nil))
|
||||||
|
u.Password = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUser(userid int64) (*User, error) {
|
||||||
|
var u User
|
||||||
|
|
||||||
|
err := DB.SelectOne(&u, "SELECT * from users where UserId=?", userid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserByUsername(username string) (*User, error) {
|
||||||
|
var u User
|
||||||
|
|
||||||
|
err := DB.SelectOne(&u, "SELECT * from users where Username=?", username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertUser(u *User) error {
|
||||||
|
transaction, err := DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := transaction.SelectInt("SELECT count(*) from users where Username=?", u.Username)
|
||||||
|
if err != nil {
|
||||||
|
transaction.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existing > 0 {
|
||||||
|
transaction.Rollback()
|
||||||
|
return UserExistsError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = transaction.Insert(u)
|
||||||
|
if err != nil {
|
||||||
|
transaction.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = transaction.Commit()
|
||||||
|
if err != nil {
|
||||||
|
transaction.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserFromSession(r *http.Request) (*User, error) {
|
||||||
|
s, err := GetSession(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return GetUser(s.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "POST" {
|
||||||
|
user_json := r.PostFormValue("user")
|
||||||
|
if user_json == "" {
|
||||||
|
WriteError(w, 3 /*Invalid Request*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
err := user.Read(user_json)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 3 /*Invalid Request*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.UserId = -1
|
||||||
|
user.HashPassword()
|
||||||
|
|
||||||
|
err = InsertUser(&user)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(UserExistsError); ok {
|
||||||
|
WriteError(w, 4 /*User Exists*/)
|
||||||
|
} else {
|
||||||
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(201 /*Created*/)
|
||||||
|
err = user.Write(w)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user, err := GetUserFromSession(r)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 1 /*Not Signed In*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userid, err := GetURLID(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 3 /*Invalid Request*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userid != user.UserId {
|
||||||
|
WriteError(w, 2 /*Unauthorized Access*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "GET" {
|
||||||
|
err = user.Write(w)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if r.Method == "PUT" {
|
||||||
|
user_json := r.PostFormValue("user")
|
||||||
|
if user_json == "" {
|
||||||
|
WriteError(w, 3 /*Invalid Request*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save old PWHash in case the new password is bogus
|
||||||
|
old_pwhash := user.PasswordHash
|
||||||
|
|
||||||
|
err = user.Read(user_json)
|
||||||
|
if err != nil || user.UserId != userid {
|
||||||
|
WriteError(w, 3 /*Invalid Request*/)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user didn't create a new password, keep their old one
|
||||||
|
if user.Password != BogusPassword {
|
||||||
|
user.HashPassword()
|
||||||
|
} else {
|
||||||
|
user.Password = ""
|
||||||
|
user.PasswordHash = old_pwhash
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := DB.Update(user)
|
||||||
|
if count != 1 || err != nil {
|
||||||
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = user.Write(w)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if r.Method == "DELETE" {
|
||||||
|
//TODO delete everything else too?
|
||||||
|
//TODO how to handle making sure users really meant to delete everything?
|
||||||
|
count, err := DB.Delete(&user)
|
||||||
|
if count != 1 || err != nil {
|
||||||
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteSuccess(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
util.go
Normal file
23
util.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetURLID(url string) (int64, error) {
|
||||||
|
pieces := strings.Split(strings.Trim(url, "/"), "/")
|
||||||
|
return strconv.ParseInt(pieces[len(pieces)-1], 10, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetURLPieces(url string, format string, a ...interface{}) (int, error) {
|
||||||
|
url = strings.Replace(url, "/", " ", -1)
|
||||||
|
format = strings.Replace(format, "/", " ", -1)
|
||||||
|
return fmt.Sscanf(url, format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteSuccess(w http.ResponseWriter) {
|
||||||
|
fmt.Fprint(w, "{}")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user