initial commit on github

This commit is contained in:
2021-05-02 16:04:45 +03:00
commit 7babade475
37 changed files with 3271 additions and 0 deletions
+666
View File
@@ -0,0 +1,666 @@
package v1
import (
"bytes"
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/getsentry/sentry-go"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
)
type Login struct {
Login string `json:"login"`
Password string `json:"password"`
Recaptcha string `json:"recaptcha"`
}
type Register struct {
Login string `json:"login"`
Email string `json:"email"`
Password string `json:"password"`
Recaptcha string `json:"recaptcha"`
}
type Activate struct {
Token string `json:"token"`
}
func RegisterAuthRoutes(r *mux.Router) {
r.HandleFunc("/auth/login", resolveGenerateNewToken).Methods("OPTIONS", "POST")
r.HandleFunc("/auth/logout", resolveDeleteToken).Methods("OPTIONS", "POST")
r.HandleFunc("/auth/register", resolveRegisterUser).Methods("OPTIONS", "POST")
r.HandleFunc("/auth/register/vk", resolveRegisterUserVK).Methods("OPTIONS", "POST")
r.HandleFunc("/auth/activate", resolveActivateUser).Methods("OPTIONS", "POST")
r.HandleFunc("/auth/activateToken", resolveGenerateActivateToken).Methods("OPTIONS", "POST")
r.HandleFunc("/auth/restore", resolveRestorePassword).Methods("OPTIONS", "POST")
r.HandleFunc("/auth/restoreToken", resolveGenerateRestorePassword).Methods("OPTIONS", "POST")
}
var (
loginAttempts = make(map[string][]time.Time)
restoreGenerations = make(map[int64]time.Time)
activationGenerations = make(map[int]time.Time)
)
func resolveRegisterUserVK(w http.ResponseWriter, r *http.Request) {
type Body struct {
Code string `json:"code"`
}
body := new(Body)
reqBody, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
err = json.Unmarshal(reqBody, body)
if err != nil {
Error(w, err)
return
}
req, err := http.Get("https://oauth.vk.com/access_token?client_id=7836090&client_secret=HqlMicDyJyq4E3SvsPba&redirect_uri=http://localhost:3000/vk_auth&code="+body.Code)
if err != nil {
Error(w, err)
return
}
b, err := ioutil.ReadAll(req.Body)
if err != nil {
Error(w, err)
return
}
log.Println(string(b))
}
func resolveRestorePassword(w http.ResponseWriter, r *http.Request) {
type Restore struct {
Token string `json:"token"`
NewPassword string `json:"new_password"`
}
restore := new(Restore)
body, err := ReadBody(r)
if err != nil {
w.WriteHeader(400)
Error(w, err)
}
err = json.Unmarshal(body, restore)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
tokenString, err := base64.StdEncoding.DecodeString(restore.Token)
if err != nil {
Error(w, err)
return
}
token, err := jwt.Parse(string(tokenString), func(token *jwt.Token) (i interface{}, e error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(privateConfig.ActivateTokenSecret), nil
})
if err != nil {
Error(w, err)
return
}
if !token.Valid {
RawError(w, "token not valid")
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
RawError(w, "token not valid")
return
}
t, ok := claims["time"].(float64)
if !ok {
RawError(w, "token not valid")
return
}
id, ok := claims["id"].(float64)
if !ok {
RawError(w, "token not valid")
return
}
genTime := time.Unix(int64(t), 0)
if time.Since(genTime) > time.Hour*1 {
TokenExpired.Ferror(w)
return
}
user, err := FetchUser(int(id))
if err != nil {
Error(w, err)
return
}
samePassword := comparePasswords(restore.NewPassword, user.Password)
if samePassword {
SamePassword.Ferror(w)
return
}
hashedPassword := hashAndSaltPassword([]byte(restore.NewPassword))
err = UpdatePassword(int(id), hashedPassword)
Response(w, err, "ok")
}
func resolveGenerateRestorePassword(w http.ResponseWriter, r *http.Request) {
type Restore struct {
Email string `json:"email"`
Recaptcha string `json:"recaptcha"`
}
var restore = &Restore{}
body, err := ReadBody(r)
if err != nil {
w.WriteHeader(400)
Error(w, err)
}
err = json.Unmarshal(body, restore)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
valid, err := validateRecaptcha(restore.Recaptcha, r.RemoteAddr)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
if !valid {
RecaptchaFailed.Ferror(w)
return
}
user, err := FetchUserByLogin(restore.Email)
if err == sql.ErrNoRows {
NotFound.Ferror(w)
return
}
if err != nil {
Error(w, err)
return
}
t, ok := restoreGenerations[user.ID]
if !ok {
restoreGenerations[user.ID] = time.Now()
} else {
if t.Add(time.Minute * 5).After(time.Now()) {
TooOftenGeneration.Ferror(w)
return
}
}
restoreToken := createActivateToken(user)
var mailData = struct {
Name string
URL string
}{Name: user.Username, URL: fmt.Sprintf("%s/restore/%s", config.FrontendUrl, restoreToken)}
SendMailTemplate(user.Email, "Восстановление пароля", "restore_password.html", mailData)
Response(w, nil, "ok")
}
func resolveActivateUser(w http.ResponseWriter, r *http.Request) {
var activate Activate
body, err := ReadBody(r)
if err != nil {
w.WriteHeader(400)
Error(w, err)
}
err = json.Unmarshal(body, &activate)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
tokenString, err := base64.StdEncoding.DecodeString(activate.Token)
if err != nil {
Error(w, err)
return
}
token, err := jwt.Parse(string(tokenString), func(token *jwt.Token) (i interface{}, e error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(privateConfig.ActivateTokenSecret), nil
})
if err != nil {
Error(w, err)
return
}
if !token.Valid {
RawError(w, "token not valid")
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
RawError(w, "token not valid")
return
}
t, ok := claims["time"].(float64)
if !ok {
RawError(w, "token not valid")
return
}
id, ok := claims["id"].(float64)
if !ok {
RawError(w, "token not valid")
return
}
genTime := time.Unix(int64(t), 0)
if time.Since(genTime) > time.Hour*1 {
TokenExpired.Ferror(w)
return
}
user, err := FetchUser(int(id))
if err == sql.ErrNoRows {
NotFound.Ferror(w)
return
}
if err != nil {
Error(w, err)
return
}
if user.IsActivated {
AlreadyActivated.Ferror(w)
return
}
user, err = ActivateUser(int(id))
Response(w, err, user)
}
func resolveRegisterUser(w http.ResponseWriter, r *http.Request) {
var reg Register
body, err := ReadBody(r)
if err != nil {
w.WriteHeader(400)
Error(w, err)
}
_ = json.Unmarshal(body, &reg)
if reg.Login == "" || reg.Password == "" || reg.Email == "" || reg.Recaptcha == "" {
w.WriteHeader(http.StatusForbidden)
RawError(w, "no")
return
}
_, err = FetchUserByLogin(reg.Login)
if err != sql.ErrNoRows {
RawError(w, "User already register!")
return
}
valid, err := validateRecaptcha(reg.Recaptcha, r.RemoteAddr)
if err != nil || !valid {
RecaptchaFailed.Ferror(w)
return
}
user, err := CreateUser(reg.Login, hashAndSaltPassword([]byte(reg.Password)), reg.Email)
Response(w, err, user)
activateToken := createActivateToken(user)
var mailData = struct {
Name string
URL string
}{Name: user.Username, URL: fmt.Sprintf("%s/activate/%s", config.FrontendUrl, activateToken)}
SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData)
embeds := make([]Embed, 1)
embeds = append(embeds, Embed{
Title: "Nix13",
Type: "rich",
Description: "Был зарегестрирован новый пользователь!",
Timestamp: time.Now(),
URL: fmt.Sprintf("https://unmei.space/user/%d", 1),
})
err = ExecuteWebhook(
"https://discord.com/api/webhooks/790593203862241320/vGyrLPzbmtKXHLaOpVrwNwn15tBK58CrsL3rQdAiUQSbi_jBvclpfUT1SoM75j98hBKY",
WebhookContent{Embeds: embeds},
)
if err != nil {
sentry.CaptureException(err)
log.Println(err)
}
}
func resolveDeleteToken(w http.ResponseWriter, _ *http.Request) {
cookie := &http.Cookie{
Name: "token",
Value: "",
Path: "/",
SameSite: http.SameSiteLaxMode,
Expires: time.Time{},
}
http.SetCookie(w, cookie)
}
func resolveGenerateNewToken(w http.ResponseWriter, r *http.Request) {
var login Login
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
err = json.Unmarshal(body, &login)
if err != nil {
Error(w, err)
return
}
userAttempts := append(loginAttempts[r.RemoteAddr], time.Now())
for index, t := range userAttempts {
if t.Add(time.Hour).Before(time.Now()) {
userAttempts = append(userAttempts[:index], userAttempts[index+1:]...)
}
}
if len(loginAttempts[r.RemoteAddr])+1 >= 5 {
if len(login.Recaptcha) == 0 {
RecaptchaNeeded.Ferror(w)
return
}
valid, err := validateRecaptcha(login.Recaptcha, r.RemoteAddr)
if err != nil {
Error(w, err)
return
}
if !valid {
RecaptchaFailed.Ferror(w)
return
}
}
if len(login.Password) == 0 || len(login.Login) == 0 {
w.WriteHeader(http.StatusBadRequest)
RawError(w, "No login or/and password")
return
}
user, err := FetchUserByLogin(login.Login)
log.Println(login.Login)
if err == sql.ErrNoRows || user == nil {
WrongPassword.Ferror(w)
return
}
if !comparePasswords(login.Password, user.Password) {
WrongPassword.Ferror(w)
return
}
claims := jwt.MapClaims{
"id": user.ID,
"is_superuser": user.IsSuperuser,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(privateConfig.AuthTokenSecret))
if err != nil {
Error(w, err)
return
}
expires := time.Now().Add(365 * 24 * time.Hour)
cookie := &http.Cookie{
Name: "token",
Value: tokenString,
Path: "/",
Expires: expires,
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, cookie)
params := r.URL.Query()
authType := params.Get("auth_type")
switch authType {
case "token":
Response(w, err, tokenString)
default:
Response(w, err, user)
}
}
func resolveGenerateActivateToken(w http.ResponseWriter, r *http.Request) {
tokenCookie, err := r.Cookie("token")
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
if err != nil {
Error(w, err)
return
}
token, err := jwt.Parse(tokenCookie.Value, func(token *jwt.Token) (i interface{}, e error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(privateConfig.AuthTokenSecret), nil
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
if !token.Valid {
w.WriteHeader(http.StatusInternalServerError)
RawError(w, "Unhandled exception!")
return
}
claims := token.Claims.(jwt.MapClaims)
id := int(claims["id"].(float64))
t, ok := activationGenerations[id]
if !ok {
activationGenerations[id] = time.Now()
} else {
if t.Add(time.Minute * 5).After(time.Now()) {
TooOftenGeneration.Ferror(w)
return
}
}
user, err := FetchUser(id)
if err != nil {
Error(w, err)
return
}
if user.IsActivated {
AlreadyActivated.Ferror(w)
return
}
activateToken := createActivateToken(user)
var mailData = struct {
Name string
URL string
}{Name: user.Username, URL: fmt.Sprintf("%s/activate/%s", config.FrontendUrl, activateToken)}
SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData)
Response(w, nil, "ok")
}
func hashAndSaltPassword(password []byte) string {
hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
log.Println(err)
sentry.CaptureException(err)
}
return string(hash)
}
func comparePasswords(password, hashedPassword string) bool {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
}
func createActivateToken(user *User) string {
claims := jwt.MapClaims{
"id": user.ID,
"time": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, _ := token.SignedString([]byte(privateConfig.ActivateTokenSecret))
hashed := base64.StdEncoding.EncodeToString([]byte(tokenString))
return hashed
}
func hasPermission(user *User, permission string) bool {
if user == nil {
return false
}
permissions := strings.Split(user.Group.Permissions, ",")
if user.IsSuperuser || user.Group.IsSuperuser {
return true
}
for _, perm := range permissions {
if perm == permission {
return true
}
}
return false
}
func validateRecaptcha(token string, ip string) (bool, error) {
url := fmt.Sprintf("https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s", privateConfig.RecaptchaSecret, token, ip)
resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte("")))
if err != nil {
return false, err
}
validateBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, err
}
err = resp.Body.Close()
if err != nil {
return false, err
}
var validate map[string]interface{}
err = json.Unmarshal(validateBody, &validate)
if err != nil {
return false, err
}
if !validate["success"].(bool) {
return false, nil
}
return true, nil
}
func JWTMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet || r.Method == http.MethodOptions || strings.HasPrefix(r.URL.Path, "/v1/auth") {
next.ServeHTTP(w, r)
return
}
tokenCookie, err := r.Cookie("token")
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
tokenString := tokenCookie.Value
if len(tokenString) == 0 {
w.WriteHeader(http.StatusUnauthorized)
return
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, e error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(privateConfig.AuthTokenSecret), nil
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
if !token.Valid {
w.WriteHeader(http.StatusInternalServerError)
RawError(w, "Unhandled exception!")
return
}
if strings.HasSuffix(r.URL.Path, "comments") {
next.ServeHTTP(w, r)
return
}
claims := token.Claims.(jwt.MapClaims)
user, err := FetchCurrentUser(r)
if err != nil {
Error(w, err)
return
}
if strings.HasPrefix(r.URL.Path, "/v1/users") {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["userId"])
if !strings.HasPrefix(r.URL.Path, "/v1/users/me") && id != int(claims["id"].(float64)) && !hasPermission(user, "users") {
NoAccess.Ferror(w)
return
}
} else {
path := strings.Split(r.URL.Path, "/")
if len(path) < 3 {
RawError(w, "something wrong")
return
}
category := path[2]
if !hasPermission(user, category) {
NoAccess.Ferror(w)
return
}
}
next.ServeHTTP(w, r)
})
}
+88
View File
@@ -0,0 +1,88 @@
package v1
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
func RegisterCharactersRoutes(r *mux.Router) {
r.HandleFunc("/characters", resolveCharacters).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/characters/{id:[0-9]+}", resolveCharacter).Methods("GET", "OPTIONS", "PUT", "DELETE")
r.HandleFunc("/characters/{id:[0-9]+}/novels", resolveCharacterNovels).Methods("GET", "OPTIONS")
}
func resolveCharacters(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
characters, err := FetchCharacters()
Response(w, err, characters)
} else if r.Method == http.MethodPost {
Response(w, nil, "todo")
}
}
func resolveCharacter(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
if r.Method == http.MethodGet {
char, err := FetchCharacter(id)
Response(w, err, char)
} else if r.Method == http.MethodPut {
user, err := FetchCurrentUser(r)
if err != nil {
NoAccess.Ferror(w)
return
}
hasPerm := hasPermission(user, "characters")
if !hasPerm {
NoAccess.Ferror(w)
return
}
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
character := new(Character)
err = json.Unmarshal(body, character)
if err != nil {
Error(w, err)
return
}
character, err = UpdateCharacter(character)
Response(w, err, character)
} else if r.Method == http.MethodDelete {
user, err := FetchCurrentUser(r)
if err != nil {
NoAccess.Ferror(w)
return
}
hasPerm := hasPermission(user, "characters")
if !hasPerm {
NoAccess.Ferror(w)
return
}
err = DeleteCharacter(id)
if err == sql.ErrNoRows {
NotFound.Ferror(w)
return
}
Response(w, err, "ok")
}
}
func resolveCharacterNovels(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
novels, err := FetchCharacterNovels(id)
Response(w, err, novels)
}
+44
View File
@@ -0,0 +1,44 @@
package v1
type Character struct {
ID int32 `json:"id"`
OriginalName string `db:"original_name" json:"original_name"`
LocalizedName string `db:"localized_name" json:"localized_name"`
Description string `json:"description"`
Image string `json:"image"`
Main bool `json:"main"`
}
func FetchCharacters() ([]*Character, error) {
var characters = make([]*Character, 0)
err := database.Select(&characters, `select * from characters;`)
return characters, err
}
func FetchCharacter(id int) (*Character, error) {
var char = &Character{}
err := database.Get(char, `select * from characters where id=$1;`, id)
return char, err
}
func UpdateCharacter(character *Character) (*Character, error) {
rows, err := database.NamedQuery("update characters set original_name=:original_name, localized_name=:localized_name, description=:description, image=:image where id=:id returning *;", character)
if err != nil {
return nil, err
}
char := new(Character)
if rows.Next() {
err = rows.StructScan(char)
}
return char, err
}
func DeleteCharacter(id int) error {
_, err := database.Exec("delete from characters where id=$1;", id)
return err
}
func FetchCharacterNovels(id int) ([]Novel, error) {
var novels = make([]Novel, 0)
err := database.Select(&novels, `select id, original_name, localized_name, coalesce(n.image, '') as image from character_novel join novels n on (n.id=character_novel.novel_id) where character_id=$1;`, id)
return novels, err
}
+40
View File
@@ -0,0 +1,40 @@
package v1
import (
"github.com/gorilla/mux"
"net/http"
"strconv"
)
func RegisterClubRoutes(r *mux.Router) {
r.HandleFunc("/clubs", resolveClubs).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/clubs/{id:[0-9]+}", resolveClub).Methods("GET", "OPTIONS")
}
func resolveClubs(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
userId, err := GetCurrentUserID(r)
if err != nil {
Error(w, err)
return
}
clubs, err := GetClubs(userId)
Response(w, err, clubs)
}
}
func resolveClub(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
rawId, _ := strconv.Atoi(vars["id"])
id := uint16(rawId)
if r.Method == http.MethodGet {
userId, err := GetCurrentUserID(r)
if err != nil {
Error(w, err)
return
}
club, err := GetClub(id, userId)
Response(w, err, club)
}
}
+21
View File
@@ -0,0 +1,21 @@
package v1
type Club struct {
ID uint16 `json:"id"`
Name string `json:"name"`
Avatar string `json:"avatar"`
Owner int `json:"-"`
IsPrivate bool `json:"is_private" db:"is_private"`
}
func GetClubs(userId uint64) ([]*Club, error) {
clubs := make([]*Club, 0)
err := database.Select(&clubs, "select clubs.* from clubs join club_member cm on clubs.id = cm.club_id where not is_private or cm.user_id=$1;", userId)
return clubs, err
}
func GetClub(id uint16, userId uint64) (*Club, error) {
club := new(Club)
err := database.Get(club, "select clubs.* from clubs join club_member cm on clubs.id = cm.club_id where club_id=$1 and (not is_private or cm.user_id=$2);", id, userId)
return club, err
}
+68
View File
@@ -0,0 +1,68 @@
package v1
import (
"encoding/json"
"io/ioutil"
"log"
"net/smtp"
)
type Config struct {
Address string `json:"address"`
Port string `json:"port"`
DevFrontendUrl string `json:"dev_frontend_url"`
FrontendUrl string `json:"frontend_url"`
CDNPath string `json:"cdn_path"`
CDNUrl string `json:"cdn_url"`
Version string `json:"version"`
Build uint32 `json:"build"`
Debug bool `json:"debug"`
}
type PrivateConfig struct {
AuthTokenSecret string `json:"auth_token"`
ActivateTokenSecret string `json:"activate_token"`
RecaptchaSecret string `json:"recaptcha"`
Database struct {
User string `json:"user"`
Password string `json:"password"`
URL string `json:"url"`
Name string `json:"name"`
} `json:"database"`
Email struct {
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
} `json:"email"`
}
var (
config *Config
privateConfig *PrivateConfig
mailAuth smtp.Auth
)
func LoadConfig() *Config {
data, err := ioutil.ReadFile("config.json")
if err != nil {
log.Fatalln(err)
}
err = json.Unmarshal(data, &config)
if err != nil {
log.Fatalln(err)
}
return config
}
func LoadPrivateConfig() {
data, err := ioutil.ReadFile("private.config.json")
if err != nil {
log.Fatalln(err)
}
err = json.Unmarshal(data, &privateConfig)
if err != nil {
log.Fatalln(err)
}
mailAuth = smtp.PlainAuth("", privateConfig.Email.Username, privateConfig.Email.Password, privateConfig.Email.Host)
}
+31
View File
@@ -0,0 +1,31 @@
package v1
import (
"bytes"
"encoding/json"
"net/http"
"time"
)
type Embed struct {
Title string `json:"title"`
Type string `json:"type"`
Description string `json:"description"`
URL string `json:"url"`
Timestamp time.Time `json:"timestamp"`
Color uint32 `json:"color"`
}
type WebhookContent struct {
Content string `json:"content"`
Embeds []Embed `json:"embeds"`
}
func ExecuteWebhook(url string, content WebhookContent) error {
body, err := json.Marshal(content)
if err != nil {
return err
}
_, err = http.Post(url, "application/json", bytes.NewReader(body))
return err
}
Executable
+107
View File
@@ -0,0 +1,107 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/getsentry/sentry-go"
)
type ApiError struct {
Code uint8 `json:"code"`
Text string `json:"text"`
HTTPCode uint16 `json:"-"`
}
var (
WrongPassword = ApiError{
Code: 1,
Text: "Wrong login and/or password!",
HTTPCode: http.StatusBadRequest,
}
RecaptchaFailed = ApiError{
Code: 2,
Text: "Recaptcha check failed!",
HTTPCode: http.StatusBadRequest,
}
NoToken = ApiError{
Code: 3,
Text: "No authorization token passed!",
HTTPCode: http.StatusUnauthorized,
}
NoAccess = ApiError{
Code: 4,
Text: "No Access!",
HTTPCode: http.StatusForbidden,
}
AlreadyActivated = ApiError{
Code: 5,
Text: "Account already activated!",
HTTPCode: http.StatusBadRequest,
}
TokenExpired = ApiError{
Code: 6,
Text: "Activate token has expired",
HTTPCode: 400,
}
TooOftenGeneration = ApiError{
Code: 7,
Text: "Too often generating links",
HTTPCode: 400,
}
SamePassword = ApiError{
Code: 8,
Text: "Same password",
HTTPCode: 400,
}
RecaptchaNeeded = ApiError{
Code: 9,
Text: "Recaptcha needed",
HTTPCode: 0,
}
NotFound = ApiError{
Code: 100,
Text: "Not found!",
HTTPCode: http.StatusNotFound,
}
TooManyRequests = ApiError{
Code: 101,
Text: "Too many requests",
HTTPCode: 429,
}
Unauthorized = ApiError{
Code: 102,
Text: "Unauthorized",
HTTPCode: http.StatusUnauthorized,
}
)
func (e ApiError) Marshall() string {
data, err := json.Marshal(e)
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
return ""
}
return string(data)
}
func (e ApiError) Ferror(w http.ResponseWriter) {
const baseError = `{"error":true,"error_data":%v}`
if e.HTTPCode > 0 {
w.WriteHeader(int(e.HTTPCode))
}
data, err := json.Marshal(e)
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
}
_, err = fmt.Fprintf(w, baseError, string(data))
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
}
}
+19
View File
@@ -0,0 +1,19 @@
package v1
import (
"github.com/gorilla/mux"
"net/http"
"strconv"
)
func RegisterGamesRoutes(r *mux.Router) {
r.HandleFunc("/games/{id:[0-9]+}", resolveGame)
}
func resolveGame(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
game, err := FetchGame(int32(id))
Response(w, err, game)
}
+18
View File
@@ -0,0 +1,18 @@
package v1
import "time"
type Game struct {
ID int32 `json:"id"`
OriginalName string `json:"original_name" db:"original_name"`
LocalizedName string `json:"localized_name" db:"localized_name"`
Description string `json:"description"`
Image string `json:"image"`
ReleaseDate time.Time `json:"release_date" db:"release_date"`
}
func FetchGame(id int32) (Game, error) {
var game Game
err := database.Get(&game, "select * from games where id = $1 limit 1;", id)
return game, err
}
+218
View File
@@ -0,0 +1,218 @@
package v1
import (
"crypto/md5"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/getsentry/sentry-go"
"github.com/gorilla/mux"
)
var lpServer *LongPollServer
func RegisterRouters(r *mux.Router) {
sub := r.PathPrefix("/v1").Subrouter()
lpServer = InitLongPollServer()
sub.Use(apiMiddleware)
sub.Use(LimitMiddleware)
go CleanupVisitors()
sub.Use(JWTMiddleware)
sub.HandleFunc("/version", func(w http.ResponseWriter, _ *http.Request) {
Response(w, nil, map[string]string{
"version": config.Version,
"build": strconv.Itoa(int(config.Build)),
})
})
RegisterAuthRoutes(sub)
RegisterUsersRoutes(sub)
RegisterNovelsRoutes(sub)
RegisterCharactersRoutes(sub)
RegisterGenresRoutes(sub)
RegisterGamesRoutes(sub)
RegisterClubRoutes(sub)
RegisterGroupsRoutes(sub)
RegisterNewsRoutes(sub)
RegisterTokensRouter(sub)
RegisterLongPollRoutes(sub)
if config.Debug {
err := sub.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
path, err := route.GetPathTemplate()
if err != nil {
return err
}
log.Printf("loading path %s\n", path)
return nil
})
if err != nil {
log.Fatalln(err)
}
}
}
func Response(w http.ResponseWriter, err error, data interface{}) {
const baseResponse = `{"error":false,"data":%v}`
if err != nil {
if err == sql.ErrNoRows {
NotFound.Ferror(w)
} else {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
}
} else {
j, err := json.Marshal(&data)
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
return
}
_, err = fmt.Fprintf(w, baseResponse, string(j))
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
}
}
}
func ResponsePagination(w http.ResponseWriter, err error, data interface{}, pagination interface{}) {
const baseResponse = `{"error":false,"pagination":%v,"data":%v}`
if err != nil {
if err == sql.ErrNoRows {
NotFound.Ferror(w)
} else {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
}
} else {
jsonData, err := json.Marshal(&data)
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
return
}
jsonPagination, err := json.Marshal(&pagination)
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
return
}
_, err = fmt.Fprintf(w, baseResponse, string(jsonPagination), string(jsonData))
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
}
}
}
func RawError(w http.ResponseWriter, e string) {
const baseError = `{"error":true,"error_data":{"code":-1,"text":"%v"}}`
_, err := fmt.Fprintf(w, baseError, strings.ReplaceAll(e, `"`, `\"`))
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
}
}
func Error(w http.ResponseWriter, e error) {
const baseError = `{"error":true,"error_data":{"code":-1,"text":"%v"}}`
_, err := fmt.Fprintf(w, baseError, strings.ReplaceAll(e.Error(), `"`, `\"`))
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
}
}
func JSON(w http.ResponseWriter, data string) {
_, err := fmt.Fprint(w, data)
if err != nil {
fmt.Println(err)
sentry.CaptureException(err)
}
}
func ReadBody(r *http.Request) ([]byte, error) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
err = r.Body.Close()
return body, err
}
func MD5(s string) string {
hash := md5.New()
hash.Write([]byte(s))
return hex.EncodeToString(hash.Sum(nil))
}
func GetCurrentUserID(r *http.Request) (uint64, error) {
tokenCookie, err := r.Cookie("token")
if err != nil {
return 0, err
}
token, err := jwt.Parse(tokenCookie.Value, func(token *jwt.Token) (interface{}, error) {
if token.Method != jwt.SigningMethodHS256 {
return nil, fmt.Errorf("invalid singning method: %s", token.Method)
}
return []byte(privateConfig.AuthTokenSecret), nil
})
if err != nil {
return 0, err
}
claims := token.Claims.(jwt.MapClaims)
id := int(claims["id"].(float64))
return uint64(id), nil
}
func apiMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Dev") == "1" {
w.Header().Add("Access-Control-Allow-Origin", config.DevFrontendUrl)
} else {
w.Header().Add("Access-Control-Allow-Origin", config.FrontendUrl)
}
w.Header().Add("Access-Control-Allow-Credentials", "true")
w.Header().Add("Content-Type", "application/json; charset=utf-8")
//w.Header().Add("Cache-Control", "max-age=60")
if r.Method == http.MethodOptions {
w.Header().Add("Access-Control-Allow-Headers", "Content-Type, Date, Content-Length, Location, dev")
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
return
}
if database == nil {
log.Println("db not initialized")
return
}
err := database.Ping()
if err != nil && strings.Contains(err.Error(), "broken pipe") {
ConnectToDB()
log.Println("connection to db reopened")
}
next.ServeHTTP(w, r)
})
}
+23
View File
@@ -0,0 +1,23 @@
package v1
import (
"fmt"
"github.com/getsentry/sentry-go"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"log"
)
var database *sqlx.DB
func ConnectToDB() {
var err error
conf := privateConfig.Database
url := fmt.Sprintf("host=%s port=5432 user=%s password=%s dbname=%s", conf.URL, conf.User, conf.Password, conf.Name)
database, err = sqlx.Open("postgres", url)
if err != nil {
sentry.CaptureException(err)
log.Fatalln(err)
}
log.Printf("Connected to DB with name %v as %v on %v\n", conf.Name, conf.User, conf.URL)
}
+38
View File
@@ -0,0 +1,38 @@
package v1
import (
"github.com/gorilla/mux"
"net/http"
"strconv"
)
func RegisterGenresRoutes(r *mux.Router) {
r.HandleFunc("/genres", resolveGenres).Methods("GET", "OPTIONS", "POST") // GET, POST
r.HandleFunc("/genres/{id:[0-9]+}", resolveGenre).Methods("GET", "OPTIONS", "PUT", "DELETE") // GET, PUT, DELETE
r.HandleFunc("/genres/{id:[0-9]+}/novels", resolveGenreNovels).Methods("GET", "OPTIONS") // GET
}
func resolveGenres(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
genres, err := FetchGenres()
Response(w, err, genres)
} else if r.Method == http.MethodPost {
RawError(w, "method not implemented")
}
}
func resolveGenre(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
genre, err := FetchGenre(id)
Response(w, err, genre)
}
func resolveGenreNovels(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
novels, err := FetchGenreNovels(id)
Response(w, err, novels)
}
+26
View File
@@ -0,0 +1,26 @@
package v1
// TODO
type Genre struct {
ID int16 `json:"id"`
Name string `json:"name"`
LocalizedName string `json:"localized_name" db:"localized_name"`
}
func FetchGenres() ([]*Genre, error) {
var genres = make([]*Genre, 0)
err := database.Select(&genres, "select * from genres;")
return genres, err
}
func FetchGenre(id int) (*Genre, error) {
var genre = &Genre{}
err := database.Get(genre, "select * from genres where id = $1;", id)
return genre, err
}
func FetchGenreNovels(id int) ([]*Novel, error) {
var novels = make([]*Novel, 0)
err := database.Select(&novels, "select novels.* from novel_genre join novels on (novel_genre.novel_id = novels.id) where genre_id = $1;", id)
return novels, err
}
+31
View File
@@ -0,0 +1,31 @@
package v1
import (
"github.com/gorilla/mux"
"net/http"
"strconv"
)
func RegisterGroupsRoutes(r *mux.Router) {
r.HandleFunc("/groups", resolveGroups).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/groups/{id:[0-9]+}", resolveGroup).Methods("GET", "OPTIONS", "PUT", "DELETE")
}
func resolveGroups(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
groups, err := GetGroups()
Response(w, err, groups)
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func resolveGroup(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
if r.Method == http.MethodGet {
group, err := GetGroup(id)
Response(w, err, group)
}
}
+23
View File
@@ -0,0 +1,23 @@
package v1
type Group struct {
ID uint8 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
MaxAvatarSize int `json:"max_avatar_size" db:"max_ava_size"`
MaxAvaWeight int64 `json:"max_avatar_weight" db:"max_ava_weight"`
IsSuperuser bool `json:"is_superuser" db:"is_superuser"`
Permissions string `json:"permissions"`
}
func GetGroups() ([]*Group, error) {
var groups = make([]*Group, 0)
err := database.Select(&groups, "select * from groups;")
return groups, err
}
func GetGroup(id int) (*Group, error) {
var group = &Group{}
err := database.Get(group, "select * from groups where id=$1;", id)
return group, err
}
Executable
+89
View File
@@ -0,0 +1,89 @@
package v1
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
)
func RegisterLongPollRoutes(r *mux.Router) {
r.HandleFunc("/lp", resolveLongPoll).Methods("GET", "OPTIONS", "POST")
}
func resolveLongPoll(w http.ResponseWriter, r *http.Request) {
userId, err := GetCurrentUserID(r)
if err == ErrNotAuthorized {
NoToken.Ferror(w)
return
}
if err == http.ErrNoCookie {
NoToken.Ferror(w)
return
}
events, err := lpServer.GetEvents(userId)
Response(w, err, events)
}
var NoEventsErr = fmt.Errorf("no events")
type LongPollServer struct {
clients map[uint64]*LongPollClient
}
type LongPollClient struct {
events []*LongPollEvent
}
type LongPollEvent struct {
Action string `json:"action"`
TS int64 `json:"ts"`
Payload interface{} `json:"payload"`
}
func InitLongPollServer() *LongPollServer {
return &LongPollServer{
clients: make(map[uint64]*LongPollClient),
}
}
func (lpServer *LongPollServer) PushEvent(payload interface{}, userId uint64, action string) {
lpClient, ok := lpServer.clients[userId]
if !ok {
lpClient = new(LongPollClient)
lpServer.clients[userId] = lpClient
}
lpClient.events = append(lpClient.events, &LongPollEvent{
action,
time.Now().Unix(),
payload,
})
}
func (lpServer *LongPollServer) PushEventToAll(payload interface{}, action string) {
for _, lpClient := range lpServer.clients {
lpClient.events = append(lpClient.events, &LongPollEvent{
action,
time.Now().Unix(),
payload,
})
}
}
func (lpServer *LongPollServer) GetEvents(userId uint64) ([]*LongPollEvent, error) {
lpClient, ok := lpServer.clients[userId]
if !ok {
lpServer.clients[userId] = new(LongPollClient)
return nil, NoEventsErr
}
if len(lpClient.events) == 0 {
return nil, NoEventsErr
}
events := lpClient.events
lpClient.events = make([]*LongPollEvent, 0)
return events, nil
}
+44
View File
@@ -0,0 +1,44 @@
package v1
import (
"bytes"
"fmt"
"html/template"
"log"
"net/smtp"
"os"
)
const (
format = "From: %v\r\n" +
"To: %v\r\n" +
"Subject: %v\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"MIME-version: 1.0;\r\n\r\n" +
"%v\r\n"
)
func SendMail(to string, subject string, body string) {
msg := []byte(fmt.Sprintf(format, privateConfig.Email.Username, to, subject, body))
err := smtp.SendMail(privateConfig.Email.Host+":587", mailAuth, privateConfig.Email.Username, []string{to}, msg)
if err != nil {
log.Println(err)
}
}
func SendMailTemplate(to string, subject string, name string, data interface{}) {
wd, err := os.Getwd()
if err != nil {
log.Println(err)
}
t, err := template.ParseFiles(wd + `/mail_templates/` + name)
if err != nil {
log.Println(err)
}
buf := new(bytes.Buffer)
if err := t.Execute(buf, data); err != nil {
log.Println(err)
}
body := buf.String()
SendMail(to, subject, body)
}
+107
View File
@@ -0,0 +1,107 @@
package v1
import (
"encoding/json"
"github.com/gorilla/mux"
"net/http"
"strconv"
)
func RegisterNewsRoutes(r *mux.Router) {
r.HandleFunc("/news", resolveNews).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/news/{id:[0-9]+}", resolvePost).Methods("GET", "OPTIONS", "PUT", "DELETE")
}
func resolveNews(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
q := r.URL.Query()
offset, _ := strconv.Atoi(q.Get("offset"))
limit, _ := strconv.Atoi(q.Get("limit"))
if limit == 0 {
limit = 20
}
total, err := FetchTotalNews()
if err != nil {
Error(w, err)
return
}
pagination := map[string]interface{}{
"offset": offset,
"limit": limit,
"total": total,
}
news, err := FetchNews(offset, limit)
ResponsePagination(w, err, news, pagination)
} else if r.Method == http.MethodPost {
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
post := new(Post)
err = json.Unmarshal(body, post)
if err != nil {
Error(w, err)
return
}
uid, err := GetCurrentUserID(r)
if err != nil {
Error(w, err)
return
}
post.AuthorID = uid
createdPost, err := CreatePost(post)
Response(w, err, createdPost)
}
}
func resolvePost(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
if r.Method == http.MethodGet {
post, err := FetchPost(id)
Response(w, err, post)
} else if r.Method == http.MethodPut {
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
post, err := FetchPost(id)
if err != nil {
Error(w, err)
return
}
err = json.Unmarshal(body, post)
if err != nil {
Error(w, err)
return
}
err = UpdatePost(post)
if err != nil {
Error(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
} else if r.Method == http.MethodDelete {
err := DeletePost(id)
if err != nil {
Error(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
Executable
+51
View File
@@ -0,0 +1,51 @@
package v1
import (
"time"
)
type Post struct {
ID uint16 `json:"id"`
Title string `json:"title"`
ShortPost string `json:"short_post" db:"short_post"`
FullPost string `json:"full_post" db:"full_post"`
Date time.Time `json:"date"`
AuthorID uint64 `json:"author_id" db:"author_id" sql:"REFERENCES users(id)"`
Author string `json:"author"`
}
func FetchTotalNews() (int, error) {
count := 0
err := database.Get(&count, `select count("news") from news;`)
return count, err
}
func FetchNews(offset, limit int) ([]Post, error) {
news := make([]Post, 0)
err := database.Select(&news, "select news.*, u.username as author from news inner join users u on news.author_id = u.id offset $1 limit $2;", offset, limit)
return news, err
}
func CreatePost(post *Post) (*Post, error) {
destPost := new(Post)
r, err := database.NamedQuery("insert into news (title, short_post, full_post, author_id) values (:title, :short_post, :full_post, :author_id) returning *;", post)
if err != nil {
return nil, err
}
if r.Next() {
err = r.StructScan(destPost)
}
return destPost, err
}
func FetchPost(id int) (*Post, error) {
post := &Post{}
err := database.Get(post, "select news.*, u.username as author from news inner join users u on news.author_id = u.id where news.id = $1;", id)
return post, err
}
func UpdatePost(post *Post) error {
_, err := database.NamedExec("update news set title=:title, short_post=:short_post, full_post=:full_post where id=:id;", post)
return err
}
func DeletePost(id int) error {
_, err := database.Exec("delete from news where id=$1;", id)
return err
}
+239
View File
@@ -0,0 +1,239 @@
package v1
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/gorilla/mux"
)
func RegisterNovelsRoutes(r *mux.Router) {
r.HandleFunc("/novels", resolveNovels).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/novels/{novelId:[0-9]+}", resolveNovel).Methods("GET", "OPTIONS", "PUT", "DELETE")
r.HandleFunc("/novels/{novelId:[0-9]+}/cover", uploadNovelCover).Methods("POST", "OPTIONS")
r.HandleFunc("/novels/{novelId:[0-9]+}/comments", resolveNovelComments).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/novels/{novelId:[0-9]+}/characters", resolveNovelCharacters).Methods("GET", "OPTIONS")
r.HandleFunc("/novels/{novelId:[0-9]+}/genres", resolveNovelGenres).Methods("GET", "OPTIONS")
}
func resolveNovels(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
sortBy, ok := r.URL.Query()["sort"]
if !ok || len(sortBy) <= 0 {
sortBy = append(sortBy, "rating")
}
query, ok := r.URL.Query()["q"]
if ok {
novels, err := SearchNovels(query[0])
Response(w, err, novels)
return
}
novels, err := FetchNovels(sortBy[0])
Response(w, err, &novels)
} else if r.Method == http.MethodPost {
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
var novel Novel
err = json.Unmarshal(body, &novel)
if err != nil {
Error(w, err)
return
}
novel, err = CreateNovel(&novel)
if err == nil {
w.WriteHeader(http.StatusCreated)
w.Header().Add("Location", fmt.Sprintf("https://%v%v/%v", r.Host, r.URL, novel.ID))
}
Response(w, err, &novel)
}
}
func resolveNovelGenres(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["novelId"])
genres, err := FetchNovelGenres(int32(id))
Response(w, err, genres)
}
func resolveNovelCharacters(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
novelId, _ := strconv.Atoi(vars["novelId"])
characters, err := FetchNovelCharacters(novelId)
Response(w, err, characters)
}
func resolveNovel(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
novelId, _ := strconv.Atoi(vars["novelId"])
if r.Method == http.MethodGet {
novel, err := FetchNovel(novelId)
Response(w, err, novel)
} else if r.Method == http.MethodPut {
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
novel, err := FetchNovel(novelId)
if err != nil {
Response(w, err, nil)
return
}
err = json.Unmarshal(body, novel)
if err != nil {
Error(w, err)
return
}
novel, err = UpdateNovel(novel)
Response(w, err, novel)
} else if r.Method == http.MethodDelete {
_, err := FetchNovel(novelId)
if err == sql.ErrNoRows {
NotFound.Ferror(w)
return
}
err = DeleteNovel(novelId)
if err != nil {
w.WriteHeader(500)
Error(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
JSON(w, "")
}
}
func uploadNovelCover(w http.ResponseWriter, r *http.Request) {
user, err := FetchCurrentUser(r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
if !hasPermission(user, "novel") {
NoAccess.Ferror(w)
return
}
vars := mux.Vars(r)
novelId, _ := strconv.Atoi(vars["novelId"])
err = r.ParseMultipartForm(1024 * 1024 * 2)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
Error(w, err)
return
}
file, handler, err := r.FormFile("cover")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
Error(w, err)
return
}
if !strings.HasPrefix(handler.Header["Content-Type"][0], "image") {
w.WriteHeader(http.StatusBadRequest)
RawError(w, "Wrong document type!")
return
}
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
err = file.Close()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
ext := strings.Split(handler.Header["Content-Type"][0], "/")[1]
path := filepath.Join(config.CDNPath, "novels", "logo", fmt.Sprintf("%v.%v", novelId, ext))
err = ioutil.WriteFile(path, fileBytes, 0644)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
coverPath := fmt.Sprintf("%v/novels/logo/%v.%v", config.CDNUrl, novelId, ext)
err = UpdateNovelCover(novelId, coverPath)
Response(w, err, coverPath)
}
func resolveNovelComments(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
novelId, _ := strconv.Atoi(vars["novelId"])
if r.Method == http.MethodGet {
offsetParam, ok := r.URL.Query()["offset"]
if !ok || len(offsetParam) <= 0 {
offsetParam = append(offsetParam, "0")
}
countParam, ok := r.URL.Query()["count"]
if !ok || len(countParam) <= 0 {
countParam = append(countParam, "5")
}
offset, _ := strconv.Atoi(offsetParam[0])
count, _ := strconv.Atoi(countParam[0])
var response = make(map[string]interface{}, 2)
comments, err := FetchNovelComments(novelId)
response["count"] = len(comments)
if offset+count > len(comments) {
comments = comments[offset:]
} else {
comments = comments[offset : offset+count]
}
response["comments"] = comments
Response(w, err, response)
} else if r.Method == http.MethodPost {
user, err := FetchCurrentUser(r)
if err != nil {
Error(w, err)
return
}
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
commentData := CommentNovel{NovelID: int16(novelId), UserID: user.ID}
err = json.Unmarshal(body, &commentData)
if err != nil {
Error(w, err)
return
}
comment, err := AddNovelComment(commentData)
Response(w, err, comment)
}
}
+225
View File
@@ -0,0 +1,225 @@
package v1
import (
"fmt"
"strings"
"time"
)
type Novel struct {
ID int32 `json:"id"`
OriginalName string `db:"original_name" json:"original_name"`
LocalizedName string `db:"localized_name" json:"localized_name"`
Description string `json:"description"`
Image string `json:"image"`
Rating float32 `json:"rating"`
ReleaseDate time.Time `json:"release_date" db:"release_date"`
Duration uint64 `json:"duration"`
ExitStatus string `json:"exit_status" db:"exit_status"`
IsDemo bool `json:"is_demo" db:"is_demo"`
Platforms string `json:"platforms"`
Genres []Genre `json:"genres" ignore:"1"`
Links []NovelLink `json:"links" ignore:"1"`
Walkthrough string `json:"walkthrough"`
RatingAll []RatingAll `json:"rating_all" ignore:"1"`
StatusAll []StatusAll `json:"status_all" ignore:"1"`
}
type ShortNovel struct {
ID int32 `json:"id"`
OriginalName string `db:"original_name" json:"original_name"`
LocalizedName string `db:"localized_name" json:"localized_name"`
Description string `json:"-"`
Image string `json:"image"`
Rating float32 `json:"rating"`
ReleaseDate time.Time `json:"release_date" db:"release_date"`
Duration uint64 `json:"-"`
ExitStatus string `json:"exit_status" db:"exit_status"`
IsDemo bool `json:"is_demo" db:"is_demo"`
Platforms string `json:"-"`
Genres []Genre `json:"genres" ignore:"1"`
Links []NovelLink `json:"-" ignore:"1"`
Walkthrough string `json:"-"`
RatingAll []RatingAll `json:"rating_all" ignore:"1"`
}
type CommentNovel struct {
ID int32 `json:"id" db:"id"`
UserID int64 `json:"user_id" db:"user_id" sql:"REFERENCES users(id)"`
NovelID int16 `json:"novel_id" db:"novel_id" sql:"REFERENCES novels(id)"`
Text string `json:"text" db:"text"`
}
type NovelLink struct {
NovelID int32 `json:"novel_id" db:"novel_id"`
Name string `json:"name"`
Link string `json:"link"`
Type string `json:"type"`
}
type RatingAll struct {
Mark int `json:"mark"`
Count int `json:"count"`
}
type StatusAll struct {
Status string `json:"status"`
Count int `json:"count"`
}
func FetchNovels(orderBy string) ([]*ShortNovel, error) {
var novels = make([]*ShortNovel, 0)
err := database.Select(&novels, fmt.Sprintf("select id, original_name, localized_name, image, rating, release_date from novels order by %v desc;", orderBy))
if err != nil {
return nil, err
}
tx := database.MustBegin()
for _, novel := range novels {
novel.Genres = make([]Genre, 0)
err = tx.Select(&novel.Genres, "select g.* from novel_genre join genres g on g.id = novel_genre.genre_id where novel_id=$1;", novel.ID)
if err != nil {
return nil, err
}
}
err = tx.Commit()
return novels, err
}
func FetchNovel(id int) (*Novel, error) {
novel := new(Novel)
err := database.Get(novel, `select * from novels where id = $1;`, id)
if err != nil {
return novel, err
}
novel.Genres = make([]Genre, 0)
err = database.Select(&novel.Genres, `select genres.* from novel_genre join genres on (novel_genre.genre_id = genres.id) where novel_id = $1`, id)
if err != nil {
return novel, err
}
novel.RatingAll = make([]RatingAll, 0)
err = database.Select(&novel.RatingAll, `select mark, count(mark) from user_novel where novel_id=$1 and mark > 0 group by mark;`, id)
if err != nil {
return novel, err
}
novel.StatusAll = make([]StatusAll, 0)
err = database.Select(&novel.StatusAll, `select status, count(status) from user_novel where novel_id=$1 group by status;`, id)
if err != nil {
return novel, err
}
novel.Links = make([]NovelLink, 0)
err = database.Select(&novel.Links, "select * from novel_link where novel_id=$1;", novel.ID)
return novel, err
}
func UpdateNovel(novel *Novel) (*Novel, error) {
var newNovel = new(Novel)
setPart := ""
setPartList := make([]string, 0)
for _, i := range GetStructAndNamedForDatabase(newNovel) {
setPartList = append(setPartList, i[0]+"="+i[1])
}
setPart = strings.Join(setPartList, ",")
rows, err := database.NamedQuery(fmt.Sprintf(`update novels set %s where id=:id returning *;`, setPart), novel)
if err != nil {
return newNovel, err
}
err = UpdateNovelGenres(novel.ID, novel.Genres)
if err != nil {
return newNovel, err
}
rows.Next()
err = rows.StructScan(&newNovel)
if err != nil {
return newNovel, err
}
genres, err := FetchNovelGenres(novel.ID)
newNovel.Genres = genres
return newNovel, err
}
func UpdateNovelGenres(novelId int32, genres []Genre) error {
_, err := database.Exec("delete from novel_genre where novel_id=$1;", novelId)
if err != nil {
return err
}
tx, err := database.Begin()
if err != nil {
return err
}
for _, genre := range genres {
_, err = tx.Exec("insert into novel_genre values ($1, $2);", genre.ID, novelId)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func UpdateNovelCover(novelId int, url string) error {
_, err := database.Exec("update novels set image=$1 where id=$2;", url, novelId)
return err
}
func CreateNovel(novelData *Novel) (Novel, error) {
var novel Novel
rows, err := database.NamedQuery(`insert into novels (original_name, localized_name, image, description, is_demo) values (:original_name, :localized_name, :image, :description, :is_demo) returning *;`, novelData)
if rows != nil && rows.Next() {
_ = rows.StructScan(&novel)
}
return novel, err
}
func DeleteNovel(id int) error {
_, err := database.Exec("delete from novels where id = $1;", id)
return err
}
func SearchNovels(query string) ([]ShortNovel, error) {
novels := make([]ShortNovel, 0)
err := database.Select(&novels, "select * from search_novel($1) order by rating desc;", query)
if err != nil {
return novels, err
}
tx := database.MustBegin()
for _, novel := range novels {
novel.Genres = make([]Genre, 0)
err = tx.Select(&novel.Genres, "select g.* from novel_genre join genres g on g.id = novel_genre.genre_id where novel_id=$1;", novel.ID)
if err != nil {
return nil, err
}
}
err = tx.Commit()
return novels, err
}
func FetchNovelComments(id int) ([]CommentNovel, error) {
var comments = make([]CommentNovel, 0)
err := database.Select(&comments, `select * from novel_comment where novel_id = $1;`, id)
return comments, err
}
func AddNovelComment(commentData CommentNovel) (CommentNovel, error) {
var comment CommentNovel
rows, err := database.NamedQuery(`insert into novel_comment(user_id, novel_id, text) values (:user_id, :novel_id, :text) returning *;`, commentData)
if rows != nil && rows.Next() {
_ = rows.StructScan(&comment)
}
return comment, err
}
func FetchNovelCharacters(id int) ([]Character, error) {
var characters = make([]Character, 0)
err := database.Select(&characters, `select characters.*, character_novel.main from character_novel join characters on (characters.id=character_novel.character_id) where novel_id=$1;`, id)
return characters, err
}
func FetchNovelGenres(id int32) ([]Genre, error) {
var genres = make([]Genre, 0)
err := database.Select(&genres, `select genres.* from novel_genre join genres on (novel_genre.genre_id = genres.id) where novel_id = $1`, id)
return genres, err
}
+63
View File
@@ -0,0 +1,63 @@
package v1
import (
"net"
"net/http"
"sync"
"time"
"golang.org/x/time/rate"
)
type visitor struct {
limiter *rate.Limiter
lastSeen time.Time
}
var visitors = make(map[string]*visitor)
var mu sync.Mutex
func CleanupVisitors() {
for {
time.Sleep(time.Minute)
mu.Lock()
for ip, v := range visitors {
if time.Since(v.lastSeen) > time.Minute*3 {
delete(visitors, ip)
}
}
mu.Unlock()
}
}
func getVisitor(ip string) *rate.Limiter {
mu.Lock()
defer mu.Unlock()
v, exists := visitors[ip]
if !exists {
limiter := rate.NewLimiter(1, 20)
visitors[ip] = &visitor{ limiter, time.Now() }
return limiter
}
v.lastSeen = time.Now()
return v.limiter
}
func LimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return
}
limiter := getVisitor(ip)
if !limiter.Allow() {
TooManyRequests.Ferror(w)
return
}
next.ServeHTTP(w, r)
})
}
+38
View File
@@ -0,0 +1,38 @@
package v1
import (
"github.com/gorilla/mux"
"net/http"
)
func RegisterTokensRouter(r *mux.Router) {
sub := r.PathPrefix("/tokens").Subrouter()
sub.HandleFunc("/auth", auth)
sub.HandleFunc("/apps", resolveApps)
sub.HandleFunc("/apps/{id:[0-9]+}", auth)
}
func auth(w http.ResponseWriter, r *http.Request) {
tokenCookie, err := r.Cookie("token")
if err == http.ErrNoCookie || len(tokenCookie.Value) == 0 {
Unauthorized.Ferror(w)
return
}
}
func resolveApps(w http.ResponseWriter, r *http.Request) {
user, err := FetchCurrentUser(r)
if err != nil {
Error(w, err)
return
}
if !hasPermission(user, "apps") {
NoAccess.Ferror(w)
return
}
apps, err := FetchApps()
Response(w, err, apps)
}
+20
View File
@@ -0,0 +1,20 @@
package v1
type App struct {
ID int `json:"id"`
Name string `json:"name"`
Secret string `json:"-"`
IsVerified bool `json:"is_verified" db:"is_verified"`
}
func FetchApp(id int) (*App, error) {
var app = &App{}
err := database.Get(app, "select * from apps where id=$1;", id)
return app, err
}
func FetchApps() ([]*App, error) {
var apps = make([]*App, 0)
err := database.Select(&apps, "select * from apps;")
return apps, err
}
+300
View File
@@ -0,0 +1,300 @@
package v1
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io/ioutil"
"log"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/gorilla/mux"
)
func RegisterUsersRoutes(r *mux.Router) {
r.HandleFunc("/users", resolveUsers).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/users/me", resolveCurrentUser).Methods("GET", "OPTIONS")
r.HandleFunc("/users/me/avatar", uploadAvatar).Methods("OPTIONS", "POST")
r.HandleFunc("/users/me/settings", resolveSettings).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/users/{userId:[0-9]+}", resolveUser).Methods("GET", "OPTIONS", "PUT")
r.HandleFunc("/users/{userId:[0-9]+}/novels", resolveUserNovels).Methods("GET", "OPTIONS", "POST")
r.HandleFunc("/users/{userId:[0-9]+}/novels/{novelId:[0-9]+}", resolveUserNovel).Methods("GET", "OPTIONS", "PUT", "DELETE")
r.HandleFunc("/users/{userId:[0-9]+}/novels/comments", resolveUserNovelsComments).Methods("GET", "OPTIONS", "POST")
}
type Settings struct {
UseGravatar bool `json:"use_gravatar" db:"use_gravatar"`
Avatar string `json:"avatar"`
Theme string `json:"theme"`
}
func resolveSettings(w http.ResponseWriter, r *http.Request) {
id, err := GetCurrentUserID(r)
if err != nil {
Error(w, err)
return
}
if r.Method == "GET" {
settings, err := FetchUserSettings(id)
Response(w, err, settings)
} else if r.Method == "POST" {
settings, err := FetchUserSettings(id)
if err != nil {
Error(w, err)
return
}
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
err = json.Unmarshal(body, settings)
if err != nil {
Error(w, err)
return
}
user, err := UpdateUserSettings(id, settings)
Response(w, err, user)
}
}
func resolveUsers(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
users, err := FetchUsers()
Response(w, err, users)
} else {
RawError(w, "Work in progress...")
}
}
func resolveCurrentUser(w http.ResponseWriter, r *http.Request) {
user, err := FetchCurrentUser(r)
if err == ErrNotAuthorized {
NoToken.Ferror(w)
return
}
if err == http.ErrNoCookie {
NoToken.Ferror(w)
return
}
Response(w, err, user)
}
func resolveUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["userId"])
if r.Method == http.MethodGet {
user, err := FetchUser(id)
Response(w, err, user)
return
}
currentUser, err := FetchCurrentUser(r)
if err != nil && err != ErrNotAuthorized {
Error(w, err)
return
} else if err == ErrNotAuthorized || currentUser == nil {
currentUser = &User{ID: -1}
}
if r.Method == http.MethodPut {
if currentUser.ID != int64(id) && !hasPermission(currentUser, "users") {
NoAccess.Ferror(w)
return
}
_, err := FetchUser(id)
if err != nil {
Error(w, err)
return
}
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
var u *User
err = json.Unmarshal(body, u)
if err != nil {
Error(w, err)
return
}
user, err := UpdateUser(u)
Response(w, err, user)
}
}
func uploadAvatar(w http.ResponseWriter, r *http.Request) {
user, err := FetchCurrentUser(r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
err = r.ParseMultipartForm(user.Group.MaxAvaWeight)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
Error(w, err)
return
}
file, handler, err := r.FormFile("avatar")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
Error(w, err)
return
}
if !strings.HasPrefix(handler.Header["Content-Type"][0], "image") {
w.WriteHeader(http.StatusBadRequest)
RawError(w, "Wrong document type!")
return
}
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
err = file.Close()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
img, _, err := image.DecodeConfig(bytes.NewReader(fileBytes))
if err != nil {
log.Println(err)
}
width, height := img.Width, img.Width
if width > user.Group.MaxAvatarSize && height > user.Group.MaxAvatarSize {
w.WriteHeader(http.StatusBadRequest)
RawError(w, "Image too big!")
return
}
ext := strings.Split(handler.Header["Content-Type"][0], "/")[1]
path := filepath.Join(config.CDNPath, "users", "avatars", fmt.Sprintf("%v.%v", user.ID, ext))
err = ioutil.WriteFile(path, fileBytes, 0644)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Error(w, err)
return
}
avatarPath := fmt.Sprintf("%v/users/avatars/%v.%v", config.CDNUrl, user.ID, ext)
Response(w, nil, avatarPath)
}
func resolveUserNovels(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userId, _ := strconv.Atoi(vars["userId"])
if r.Method == http.MethodGet {
novels, err := FetchUserNovels(userId)
Response(w, err, novels)
} else if r.Method == http.MethodPost {
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
var novel = &UserNovels{}
err = json.Unmarshal(body, novel)
if err != nil {
Error(w, err)
return
}
novel.UserID = int64(userId)
novel, err = CreateUserNovel(novel)
if err == nil {
w.WriteHeader(http.StatusCreated)
w.Header().Add("Location", fmt.Sprintf("https://%v%v/%v", r.Host, r.URL, novel.NovelID))
}
Response(w, err, novel)
}
}
func resolveUserNovel(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userId, _ := strconv.Atoi(vars["userId"])
novelId, _ := strconv.Atoi(vars["novelId"])
if r.Method == http.MethodGet {
novel, err := FetchUserNovel(userId, novelId)
Response(w, err, novel)
} else if r.Method == http.MethodPut {
_, err := FetchNovel(novelId)
if err == sql.ErrNoRows {
NotFound.Ferror(w)
return
}
_, err = FetchUser(userId)
if err == sql.ErrNoRows {
NotFound.Ferror(w)
return
}
novel, _ := FetchUserNovel(userId, novelId)
body, err := ReadBody(r)
if err != nil {
Error(w, err)
return
}
if novel == nil {
NotFound.Ferror(w)
return
}
err = json.Unmarshal(body, &novel)
if err != nil {
Error(w, err)
return
}
updatedNovel, err := UpdateUserNovel(novel)
lpServer.PushEvent(updatedNovel, uint64(userId), "update-user-novel")
Response(w, err, updatedNovel)
} else if r.Method == http.MethodDelete {
_, err := FetchUserNovel(userId, novelId)
if err == sql.ErrNoRows {
NotFound.Ferror(w)
return
}
err = DeleteUserNovel(userId, novelId)
if err != nil {
Error(w, err)
}
w.WriteHeader(http.StatusNoContent)
}
}
func resolveUserNovelsComments(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["userId"])
comments, err := FetchUserNovelsComments(id)
Response(w, err, comments)
}
+202
View File
@@ -0,0 +1,202 @@
package v1
import (
"fmt"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
)
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
Email string `json:"-"`
RegistrationDate time.Time `json:"reg_date" db:"reg_date"`
LastSeen time.Time `json:"last_seen" db:"last_seen"`
Avatar string `json:"avatar"`
IsSuperuser bool `json:"is_superuser" db:"is_superuser"`
IsActivated bool `json:"is_activated" db:"is_activated"`
GroupID int8 `json:"-" db:"group_id"`
Group Group `json:"group"`
Cover string `json:"cover"`
UseGravatar bool `json:"use_gravatar" db:"use_gravatar"`
Theme string `json:"theme"`
IsBanned bool `json:"is_banned" db:"is_banned"`
}
type UserNovels struct {
UserID int64 `json:"user_id" db:"user_id" sql:"REFERENCES users(id)"`
NovelID int16 `json:"novel_id" db:"novel_id" sql:"REFERENCES novels(id)"`
Mark int8 `json:"mark"`
Status string `json:"status"`
}
type UserNovelWithInfo struct {
UserNovels
Novel
}
var (
ErrNotAuthorized = fmt.Errorf("%s", "not authorized")
)
func FetchUsers() ([]*User, error) {
var users = make([]*User, 0)
err := database.Select(&users, "select * from users limit 100;")
for _, user := range users {
if user.UseGravatar {
user.Avatar = GetUserGravatar(user.Email)
}
}
return users, err
}
func FetchUser(id int) (*User, error) {
var user = &User{}
err := database.Get(user, `select users.* from users where users.id = $1;`, id)
if err == nil {
if user.UseGravatar {
user.Avatar = GetUserGravatar(user.Email)
}
err = database.Get(&user.Group, `select * from groups where id = $1;`, user.GroupID)
}
return user, err
}
func UpdateUser(user *User) (*User, error) {
var u = &User{}
rows, err := database.NamedQuery(`update users set avatar=:avatar, username=:username, is_activated=:is_activated, use_gravatar=:use_gravatar where id=:id returning *;`, user)
if rows != nil && rows.Next() {
_ = rows.StructScan(u)
if u.UseGravatar {
u.Avatar = GetUserGravatar(u.Email)
}
}
return u, err
}
func UpdatePassword(id int, password string) error {
_, err := database.Exec("update users set password=$1 where id=$2;", password, id)
return err
}
func FetchCurrentUser(r *http.Request) (*User, error) {
cookie, err := r.Cookie("token")
if err == http.ErrNoCookie {
return nil, ErrNotAuthorized
}
if err != nil {
return nil, err
}
if len(cookie.Value) == 0 {
return nil, ErrNotAuthorized
}
return FetchUserByToken(cookie.Value)
}
func FetchUserByToken(tokenString string) (*User, error) {
var user = &User{}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, e error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(privateConfig.AuthTokenSecret), nil
})
if err != nil {
return user, err
}
if !token.Valid {
return user, fmt.Errorf("token not valid")
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
err = database.Get(user, `select users.* from users where users.id = $1;`, claims["id"])
if err == nil {
if user.UseGravatar {
user.Avatar = GetUserGravatar(user.Email)
}
err = database.Get(&user.Group, `select * from groups where id = $1;`, user.GroupID)
}
}
return user, err
}
func FetchUserByLogin(login string) (*User, error) {
user := new(User)
err := database.Get(user, `select * from users where username = $1 or email = $1;`, login)
if err == nil {
if user.UseGravatar {
user.Avatar = GetUserGravatar(user.Email)
}
err = database.Get(&user.Group, `select * from groups where id = $1;`, user.GroupID)
}
return user, err
}
func FetchUserNovels(id int) ([]*UserNovelWithInfo, error) {
var novels = make([]*UserNovelWithInfo, 0)
err := database.Select(&novels, `select novels.*, user_novel.* from user_novel inner join novels on (novels.id = user_novel.novel_id) where user_id = $1;`, id)
return novels, err
}
func CreateUserNovel(novelData *UserNovels) (*UserNovels, error) {
var novel = &UserNovels{}
rows, err := database.NamedQuery(`insert into user_novel(user_id, novel_id, mark, status) VALUES (:user_id, :novel_id, :mark, :status) returning *;`, novelData)
if rows != nil && rows.Next() {
_ = rows.StructScan(novel)
}
return novel, err
}
func FetchUserNovel(userId int, novelId int) (*UserNovels, error) {
var novel = &UserNovels{}
err := database.Get(novel, `select user_novel.* from user_novel where user_id = $1 and novel_id = $2;`, userId, novelId)
return novel, err
}
func UpdateUserNovel(novel *UserNovels) (*UserNovels, error) {
var updatedNovel = &UserNovels{}
rows, err := database.NamedQuery(`update user_novel set mark=:mark, status=:status where user_id=:user_id and novel_id=:novel_id returning *;`, novel)
_, _ = database.Exec(`update novels set rating=avg_novel($1) where id=$1;`, novel.NovelID)
if rows != nil && rows.Next() {
_ = rows.StructScan(updatedNovel)
}
return updatedNovel, err
}
func DeleteUserNovel(userId int, novelId int) error {
_, err := database.Exec("delete from user_novel where user_id = $1 and novel_id = $2;", userId, novelId)
return err
}
func FetchUserNovelsComments(id int) ([]CommentNovel, error) {
var comments = make([]CommentNovel, 0)
err := database.Select(&comments, `select * from novel_comment where user_id = $1;`, id)
return comments, err
}
func CreateUser(username string, password string, email string) (*User, error) {
var user = &User{}
var currentTime = time.Now()
err := database.Get(user, "insert into users(username, password, email, reg_date, last_seen) VALUES ($1, $2, $3, $4, $4) returning *;", username, password, email, currentTime)
return user, err
}
func ActivateUser(id int) (*User, error) {
var user = &User{}
err := database.Get(user, `update users set is_activated=true where id=$1 returning *;`, id)
return user, err
}
func GetUserGravatar(email string) string {
return fmt.Sprintf("https://www.gravatar.com/avatar/%s", MD5(email))
}
// Settings
func FetchUserSettings(id uint64) (*Settings, error) {
settings := new(Settings)
err := database.Get(settings, "select use_gravatar, avatar, theme from users where id=$1;", id)
return settings, err
}
func UpdateUserSettings(id uint64, settings *Settings) (*User, error) {
user := new(User)
err := database.Get(user, "update users set use_gravatar=$2, avatar=$3, theme=$4 where id=$1 returning *;", id, settings.UseGravatar, settings.Avatar, settings.Theme)
return user, err
}
+65
View File
@@ -0,0 +1,65 @@
package v1
import (
"reflect"
"strings"
)
func StringOrDefault(a, b string) string {
if len(a) == 0 {
return b
}
return a
}
func GetStructForDatabase(s interface{}) []string {
out := make([]string, 0)
el := reflect.ValueOf(s).Elem()
for i := 0; i < el.NumField(); i++ {
t := el.Type().Field(i)
tag := t.Tag
db := tag.Get("db")
if len(db) == 0 {
out = append(out, strings.ToLower(t.Name))
} else {
out = append(out, db)
}
}
return out
}
func GetNamedForDatabase(s interface{}) []string {
out := make([]string, 0)
el := reflect.ValueOf(s).Elem()
for i := 0; i < el.NumField(); i++ {
t := el.Type().Field(i)
tag := t.Tag
db := tag.Get("db")
if len(db) == 0 {
out = append(out, ":"+strings.ToLower(t.Name))
} else {
out = append(out, ":"+db)
}
}
return out
}
func GetStructAndNamedForDatabase(s interface{}) [][]string {
out := make([][]string, 0)
el := reflect.ValueOf(s).Elem()
for i := 0; i < el.NumField(); i++ {
t := el.Type().Field(i)
tag := t.Tag
ignore := tag.Get("ignore")
if ignore != "1" {
db := tag.Get("db")
if len(db) == 0 {
out = append(out, []string{strings.ToLower(t.Name), ":" + strings.ToLower(t.Name)})
} else {
out = append(out, []string{db, ":" + db})
}
}
}
return out
}