diff --git a/.gitignore b/.gitignore index 2fd5d82..76d6160 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ build/ cdn/ .DS_Store -private.config.json \ No newline at end of file +private.config.json +.env +*.env \ No newline at end of file diff --git a/v1/characters_sql.go b/database/characters.go old mode 100755 new mode 100644 similarity index 77% rename from v1/characters_sql.go rename to database/characters.go index e404441..c8ef561 --- a/v1/characters_sql.go +++ b/database/characters.go @@ -1,4 +1,4 @@ -package v1 +package database type Character struct { ID int32 `json:"id"` @@ -11,17 +11,17 @@ type Character struct { func FetchCharacters() ([]*Character, error) { var characters = make([]*Character, 0) - err := database.Select(&characters, `select * from characters;`) + 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) + 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) + 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 } @@ -33,12 +33,12 @@ func UpdateCharacter(character *Character) (*Character, error) { return char, err } func DeleteCharacter(id int) error { - _, err := database.Exec("delete from characters where id=$1;", id) + _, 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) + 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 } diff --git a/v1/club_sql.go b/database/club.go similarity index 66% rename from v1/club_sql.go rename to database/club.go index 2cbd06a..ba5c127 100644 --- a/v1/club_sql.go +++ b/database/club.go @@ -1,4 +1,4 @@ -package v1 +package database type Club struct { ID uint16 `json:"id"` @@ -8,14 +8,14 @@ type Club struct { IsPrivate bool `json:"is_private" db:"is_private"` } -func GetClubs(userId uint64) ([]*Club, error) { +func GetClubs(userId int) ([]*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) + 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) { +func GetClub(id int, 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) + 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 } diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..fcb2f22 --- /dev/null +++ b/database/database.go @@ -0,0 +1,29 @@ +package database + +import ( + "fmt" + "github.com/getsentry/sentry-go" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "log" + "os" +) + +var Database *sqlx.DB + +func ConnectToDB() { + var err error + + host := os.Getenv("DB_HOST") + user := os.Getenv("DB_USER") + password := os.Getenv("DB_PASSWORD") + name := os.Getenv("DB_NAME") + + url := fmt.Sprintf("host=%s port=5432 user=%s password=%s dbname=%s sslmode=disable", host, user, password, 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", name, user, host) +} diff --git a/database/games.go b/database/games.go new file mode 100644 index 0000000..9567432 --- /dev/null +++ b/database/games.go @@ -0,0 +1,18 @@ +package database + +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 +} diff --git a/v1/genres_sql.go b/database/genres.go similarity index 72% rename from v1/genres_sql.go rename to database/genres.go index 3d17879..378025c 100644 --- a/v1/genres_sql.go +++ b/database/genres.go @@ -1,4 +1,4 @@ -package v1 +package database // TODO type Genre struct { @@ -9,18 +9,18 @@ type Genre struct { func FetchGenres() ([]*Genre, error) { var genres = make([]*Genre, 0) - err := database.Select(&genres, "select * from genres;") + 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) + 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) + 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 } diff --git a/database/groups.go b/database/groups.go new file mode 100644 index 0000000..44db7e2 --- /dev/null +++ b/database/groups.go @@ -0,0 +1,23 @@ +package database + +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 +} diff --git a/v1/news_sql.go b/database/news.go old mode 100755 new mode 100644 similarity index 75% rename from v1/news_sql.go rename to database/news.go index 9534e97..cee8f64 --- a/v1/news_sql.go +++ b/database/news.go @@ -1,4 +1,4 @@ -package v1 +package database import ( "time" @@ -16,17 +16,17 @@ type Post struct { func FetchTotalNews() (int, error) { count := 0 - err := database.Get(&count, `select count("news") from news;`) + 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) + 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) + 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 } @@ -38,14 +38,14 @@ func CreatePost(post *Post) (*Post, error) { 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) + 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) + _, 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) + _, err := Database.Exec("delete from news where id=$1;", id) return err } diff --git a/v1/novels_sql.go b/database/novels.go similarity index 76% rename from v1/novels_sql.go rename to database/novels.go index de9d67a..4103a55 100644 --- a/v1/novels_sql.go +++ b/database/novels.go @@ -1,6 +1,7 @@ -package v1 +package database import ( + "backend/utils" "fmt" "strings" "time" @@ -21,8 +22,9 @@ type Novel struct { 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"` + RatingAll []RatingAll `json:"rating_all" ignore:"1"` + StatusAll []StatusAll `json:"status_all" ignore:"1"` + IsNSFW bool `json:"is_nsfw" db:"is_nsfw"` } type ShortNovel struct { @@ -40,7 +42,8 @@ type ShortNovel struct { Genres []Genre `json:"genres" ignore:"1"` Links []NovelLink `json:"-" ignore:"1"` Walkthrough string `json:"-"` - RatingAll []RatingAll `json:"rating_all" ignore:"1"` + RatingAll []RatingAll `json:"rating_all" ignore:"1"` + IsNSFW bool `json:"is_nsfw" db:"is_nsfw"` } type CommentNovel struct { @@ -58,23 +61,23 @@ type NovelLink struct { } type RatingAll struct { - Mark int `json:"mark"` + Mark int `json:"mark"` Count int `json:"count"` } type StatusAll struct { Status string `json:"status"` - Count int `json:"count"` + 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)) + 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() + 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) @@ -88,46 +91,46 @@ func FetchNovels(orderBy string) ([]*ShortNovel, error) { func FetchNovel(id int) (*Novel, error) { novel := new(Novel) - err := database.Get(novel, `select * from novels where id = $1;`, id) + 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) + 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) + 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) + 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) + 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) { + for _, i := range utils.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) + 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) + err = UpdateNovelGenres(int(novel.ID), novel.Genres) if err != nil { return newNovel, err } @@ -136,18 +139,18 @@ func UpdateNovel(novel *Novel) (*Novel, error) { if err != nil { return newNovel, err } - genres, err := FetchNovelGenres(novel.ID) + genres, err := FetchNovelGenres(int(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) +func UpdateNovelGenres(novelId int, genres []Genre) error { + _, err := Database.Exec("delete from novel_genre where novel_id=$1;", novelId) if err != nil { return err } - tx, err := database.Begin() + tx, err := Database.Begin() if err != nil { return err } @@ -164,29 +167,29 @@ func UpdateNovelGenres(novelId int32, genres []Genre) error { return nil } func UpdateNovelCover(novelId int, url string) error { - _, err := database.Exec("update novels set image=$1 where id=$2;", url, novelId) + _, 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) + 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) + _, 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) + err := Database.Select(&novels, "select * from search_novel($1) order by rating desc;", query) if err != nil { return novels, err } - tx := database.MustBegin() + 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) @@ -200,12 +203,12 @@ func SearchNovels(query string) ([]ShortNovel, error) { func FetchNovelComments(id int) ([]CommentNovel, error) { var comments = make([]CommentNovel, 0) - err := database.Select(&comments, `select * from novel_comment where novel_id = $1;`, id) + 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) + 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) } @@ -214,12 +217,12 @@ func AddNovelComment(commentData CommentNovel) (CommentNovel, error) { 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) + 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) { +func FetchNovelGenres(id int) ([]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) + 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 } diff --git a/database/tokens.go b/database/tokens.go new file mode 100644 index 0000000..d867905 --- /dev/null +++ b/database/tokens.go @@ -0,0 +1,20 @@ +package database + +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 +} diff --git a/v1/users_sql.go b/database/users.go old mode 100755 new mode 100644 similarity index 61% rename from v1/users_sql.go rename to database/users.go index f00959f..05da893 --- a/v1/users_sql.go +++ b/database/users.go @@ -1,11 +1,9 @@ -package v1 +package database import ( + "backend/utils" "fmt" - "net/http" "time" - - "github.com/dgrijalva/jwt-go" ) type User struct { @@ -26,6 +24,12 @@ type User struct { IsBanned bool `json:"is_banned" db:"is_banned"` } +type UserSettings struct { + UseGravatar bool `json:"use_gravatar" db:"use_gravatar"` + Avatar string `json:"avatar"` + Theme string `json:"theme"` +} + 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)"` @@ -44,29 +48,30 @@ var ( func FetchUsers() ([]*User, error) { var users = make([]*User, 0) - err := database.Select(&users, "select * from users limit 100;") + err := Database.Select(&users, "select * from users limit 20;") for _, user := range users { if user.UseGravatar { user.Avatar = GetUserGravatar(user.Email) } + err = Database.Get(&user.Group, `select * from groups where id = $1;`, user.GroupID) } 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) + user := new(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) + 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) + 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 { @@ -76,72 +81,31 @@ func UpdateUser(user *User) (*User, error) { return u, err } func UpdatePassword(id int, password string) error { - _, err := database.Exec("update users set password=$1 where id=$2;", password, id) + _, 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) + 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) + 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) + 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) + 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) } @@ -149,54 +113,54 @@ func CreateUserNovel(novelData *UserNovels) (*UserNovels, error) { } 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) + 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) + 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) + _, 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) + 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) + 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) + 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)) + return fmt.Sprintf("https://www.gravatar.com/avatar/%s", utils.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) +func FetchUserSettings(id uint64) (*UserSettings, error) { + settings := new(UserSettings) + 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) { +func UpdateUserSettings(id uint64, settings *UserSettings) (*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) + 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 } diff --git a/go.mod b/go.mod index 29a882d..a8e5546 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,19 @@ module backend -go 1.14 +go 1.24 require ( - github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/getsentry/sentry-go v0.10.0 - github.com/gorilla/mux v1.8.0 - github.com/jmoiron/sqlx v1.3.3 - github.com/lib/pq v1.10.1 - golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b - golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba - google.golang.org/appengine v1.6.6 // indirect + github.com/getsentry/sentry-go v0.31.1 + github.com/go-chi/chi/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/jmoiron/sqlx v1.4.0 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.36.0 + golang.org/x/time v0.11.0 +) + +require ( + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index d250a48..5d08066 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= @@ -27,18 +28,25 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g= github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= +github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= +github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= @@ -64,6 +72,10 @@ github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -87,6 +99,8 @@ github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo= github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -95,6 +109,7 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= @@ -161,6 +176,8 @@ golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -182,14 +199,24 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/main.go b/main.go index f138a1d..81c824a 100644 --- a/main.go +++ b/main.go @@ -1,52 +1,61 @@ package main import ( - v1 "backend/v1" + "backend/database" + "backend/routes" + "backend/utils" "fmt" + "github.com/getsentry/sentry-go" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/joho/godotenv" "log" "net/http" + "os" "time" - - "github.com/getsentry/sentry-go" - "github.com/gorilla/mux" ) -var router *mux.Router -var config *v1.Config +var router chi.Router func init() { - v1.LoadPrivateConfig() - log.Println("Private config was loaded") + err := godotenv.Load(".env", ".private.env", ".db.env", ".email.env") + if err != nil { + log.Fatal("Error loading .env file") + } - config = v1.LoadConfig() - log.Println("Config was loaded") - log.Println(fmt.Sprintf("Initizalized CDN path at %v", config.CDNPath)) - log.Println(fmt.Sprintf("Initizalized CDN URL at %v", config.CDNUrl)) + log.Println(".env files loaded") - err := sentry.Init(sentry.ClientOptions{ - Dsn: "https://b4bdbdbf01b344178b6fd69920ab898c@sentry.io/1817643", + log.Println(fmt.Sprintf("Initizalized CDN path at %v", os.Getenv("CDN_PATH"))) + log.Println(fmt.Sprintf("Initizalized CDN URL at %v", os.Getenv("CDN_URL"))) + + err = sentry.Init(sentry.ClientOptions{ + Dsn: os.Getenv("SENTRY_DSN"), }) - if err != nil { fmt.Printf("Sentry initialization failed: %v\n", err) } - router = mux.NewRouter().StrictSlash(true) - v1.RegisterRouters(router) + router = chi.NewRouter() - v1.ConnectToDB() + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(middleware.Logger) + router.Use(middleware.Recoverer) + router.Route("/v1", routes.RegisterRouters) + + database.ConnectToDB() } func main() { server := http.Server{ - Addr: fmt.Sprintf("%v:%v", config.Address, config.Port), + Addr: fmt.Sprintf("%v:%v", os.Getenv("ADDRESS"), os.Getenv("PORT")), Handler: router, IdleTimeout: 120 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } - log.Println(fmt.Sprintf("Staring on %v:%v", config.Address, config.Port)) + log.Println(fmt.Sprintf("Staring on %v:%v", os.Getenv("ADDRESS"), utils.Getenv("PORT", "8080"))) err := server.ListenAndServe() if err != nil { log.Println(err) diff --git a/v1/auth_routes.go b/routes/auth.go similarity index 81% rename from v1/auth_routes.go rename to routes/auth.go index f56fc2c..c96f9db 100644 --- a/v1/auth_routes.go +++ b/routes/auth.go @@ -1,21 +1,25 @@ -package v1 +package routes import ( + "backend/database" + "backend/utils" "bytes" "database/sql" "encoding/base64" "encoding/json" + "errors" "fmt" + "github.com/getsentry/sentry-go" + "github.com/go-chi/chi/v5" + "github.com/golang-jwt/jwt/v5" + "io" "io/ioutil" "log" "net/http" - "strconv" + "os" "strings" "time" - "github.com/dgrijalva/jwt-go" - "github.com/getsentry/sentry-go" - "github.com/gorilla/mux" "golang.org/x/crypto/bcrypt" ) @@ -36,17 +40,17 @@ 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") +func RegisterAuthRoutes(r chi.Router) { + r.Post("/login", resolveGenerateNewToken) + r.Post("/logout", resolveDeleteToken) + r.Post("/register", resolveRegisterUser) + r.Post("/register/vk", resolveRegisterUserVK) - r.HandleFunc("/auth/activate", resolveActivateUser).Methods("OPTIONS", "POST") - r.HandleFunc("/auth/activateToken", resolveGenerateActivateToken).Methods("OPTIONS", "POST") + r.Post("/activate", resolveActivateUser) + r.Post("/activateToken", resolveGenerateActivateToken) - r.HandleFunc("/auth/restore", resolveRestorePassword).Methods("OPTIONS", "POST") - r.HandleFunc("/auth/restoreToken", resolveGenerateRestorePassword).Methods("OPTIONS", "POST") + r.Post("/restore", resolveRestorePassword) + r.Post("/restoreToken", resolveGenerateRestorePassword) } var ( @@ -71,13 +75,13 @@ func resolveRegisterUserVK(w http.ResponseWriter, r *http.Request) { 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) + 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) + b, err := io.ReadAll(req.Body) if err != nil { Error(w, err) return @@ -113,7 +117,7 @@ func resolveRestorePassword(w http.ResponseWriter, r *http.Request) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - return []byte(privateConfig.ActivateTokenSecret), nil + return []byte(os.Getenv("ACTIVATE_TOKEN_SECRET")), nil }) if err != nil { Error(w, err) @@ -148,7 +152,7 @@ func resolveRestorePassword(w http.ResponseWriter, r *http.Request) { return } - user, err := FetchUser(int(id)) + user, err := database.FetchUser(int(id)) if err != nil { Error(w, err) return @@ -161,7 +165,7 @@ func resolveRestorePassword(w http.ResponseWriter, r *http.Request) { hashedPassword := hashAndSaltPassword([]byte(restore.NewPassword)) - err = UpdatePassword(int(id), hashedPassword) + err = database.UpdatePassword(int(id), hashedPassword) Response(w, err, "ok") } @@ -194,8 +198,8 @@ func resolveGenerateRestorePassword(w http.ResponseWriter, r *http.Request) { return } - user, err := FetchUserByLogin(restore.Email) - if err == sql.ErrNoRows { + user, err := database.FetchUserByLogin(restore.Email) + if errors.Is(err, sql.ErrNoRows) { NotFound.Ferror(w) return } @@ -218,8 +222,8 @@ func resolveGenerateRestorePassword(w http.ResponseWriter, r *http.Request) { 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) + }{Name: user.Username, URL: fmt.Sprintf("%s/restore/%s", utils.GetFrontendURL(), restoreToken)} + utils.SendMailTemplate(user.Email, "Восстановление пароля", "restore_password.html", mailData) Response(w, nil, "ok") } @@ -246,7 +250,7 @@ func resolveActivateUser(w http.ResponseWriter, r *http.Request) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - return []byte(privateConfig.ActivateTokenSecret), nil + return []byte(os.Getenv("ACTIVATE_TOKEN_SECRET")), nil }) if err != nil { Error(w, err) @@ -282,8 +286,8 @@ func resolveActivateUser(w http.ResponseWriter, r *http.Request) { return } - user, err := FetchUser(int(id)) - if err == sql.ErrNoRows { + user, err := database.FetchUser(int(id)) + if errors.Is(err, sql.ErrNoRows) { NotFound.Ferror(w) return } @@ -295,7 +299,7 @@ func resolveActivateUser(w http.ResponseWriter, r *http.Request) { AlreadyActivated.Ferror(w) return } - user, err = ActivateUser(int(id)) + user, err = database.ActivateUser(int(id)) Response(w, err, user) } @@ -314,8 +318,8 @@ func resolveRegisterUser(w http.ResponseWriter, r *http.Request) { return } - _, err = FetchUserByLogin(reg.Login) - if err != sql.ErrNoRows { + _, err = database.FetchUserByLogin(reg.Login) + if !errors.Is(err, sql.ErrNoRows) { RawError(w, "User already register!") return } @@ -326,27 +330,27 @@ func resolveRegisterUser(w http.ResponseWriter, r *http.Request) { return } - user, err := CreateUser(reg.Login, hashAndSaltPassword([]byte(reg.Password)), reg.Email) + user, err := database.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) + }{Name: user.Username, URL: fmt.Sprintf("%s/activate/%s", utils.GetFrontendURL(), activateToken)} + utils.SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData) - embeds := make([]Embed, 1) - embeds = append(embeds, Embed{ + embeds := make([]utils.Embed, 1) + embeds = append(embeds, utils.Embed{ Title: "Nix13", Type: "rich", Description: "Был зарегестрирован новый пользователь!", Timestamp: time.Now(), - URL: fmt.Sprintf("https://unmei.space/user/%d", 1), + URL: fmt.Sprintf("%s/user/%d", utils.GetFrontendURL(), 1), }) - err = ExecuteWebhook( + err = utils.ExecuteWebhook( "https://discord.com/api/webhooks/790593203862241320/vGyrLPzbmtKXHLaOpVrwNwn15tBK58CrsL3rQdAiUQSbi_jBvclpfUT1SoM75j98hBKY", - WebhookContent{Embeds: embeds}, + utils.WebhookContent{Embeds: embeds}, ) if err != nil { sentry.CaptureException(err) @@ -409,9 +413,9 @@ func resolveGenerateNewToken(w http.ResponseWriter, r *http.Request) { return } - user, err := FetchUserByLogin(login.Login) + user, err := database.FetchUserByLogin(login.Login) log.Println(login.Login) - if err == sql.ErrNoRows || user == nil { + if errors.Is(err, sql.ErrNoRows) || user == nil { WrongPassword.Ferror(w) return } @@ -427,7 +431,7 @@ func resolveGenerateNewToken(w http.ResponseWriter, r *http.Request) { } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(privateConfig.AuthTokenSecret)) + tokenString, err := token.SignedString([]byte(os.Getenv("AUTH_TOKEN_SECRET"))) if err != nil { Error(w, err) return @@ -457,7 +461,7 @@ func resolveGenerateNewToken(w http.ResponseWriter, r *http.Request) { func resolveGenerateActivateToken(w http.ResponseWriter, r *http.Request) { tokenCookie, err := r.Cookie("token") - if err == http.ErrNoCookie { + if errors.Is(err, http.ErrNoCookie) { w.WriteHeader(http.StatusUnauthorized) return } @@ -470,7 +474,7 @@ func resolveGenerateActivateToken(w http.ResponseWriter, r *http.Request) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - return []byte(privateConfig.AuthTokenSecret), nil + return []byte(os.Getenv("AUTH_TOKEN_SECRET")), nil }) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -497,7 +501,7 @@ func resolveGenerateActivateToken(w http.ResponseWriter, r *http.Request) { } } - user, err := FetchUser(id) + user, err := database.FetchUser(id) if err != nil { Error(w, err) return @@ -510,8 +514,8 @@ func resolveGenerateActivateToken(w http.ResponseWriter, r *http.Request) { 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) + }{Name: user.Username, URL: fmt.Sprintf("%s/activate/%s", utils.GetFrontendURL(), activateToken)} + utils.SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData) Response(w, nil, "ok") } @@ -528,19 +532,19 @@ func comparePasswords(password, hashedPassword string) bool { return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil } -func createActivateToken(user *User) string { +func createActivateToken(user *database.User) string { claims := jwt.MapClaims{ "id": user.ID, "time": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, _ := token.SignedString([]byte(privateConfig.ActivateTokenSecret)) + tokenString, _ := token.SignedString([]byte(os.Getenv("ACTIVATE_TOKEN_SECRET"))) hashed := base64.StdEncoding.EncodeToString([]byte(tokenString)) return hashed } -func hasPermission(user *User, permission string) bool { +func hasPermission(user *database.User, permission string) bool { if user == nil { return false } @@ -557,7 +561,7 @@ func hasPermission(user *User, permission string) bool { } 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) + url := fmt.Sprintf("https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s", os.Getenv("RECAPTCHA_SECRET"), token, ip) resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(""))) if err != nil { @@ -591,7 +595,7 @@ func JWTMiddleware(next http.Handler) http.Handler { } tokenCookie, err := r.Cookie("token") - if err == http.ErrNoCookie { + if errors.Is(err, http.ErrNoCookie) { w.WriteHeader(http.StatusUnauthorized) return } @@ -611,7 +615,7 @@ func JWTMiddleware(next http.Handler) http.Handler { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - return []byte(privateConfig.AuthTokenSecret), nil + return []byte(os.Getenv("AUTH_TOKEN_SECRET")), nil }) if err != nil { @@ -633,15 +637,14 @@ func JWTMiddleware(next http.Handler) http.Handler { claims := token.Claims.(jwt.MapClaims) - user, err := FetchCurrentUser(r) + user, err := GetCurrentUser(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"]) + id := IURLParam(r, "userId") if !strings.HasPrefix(r.URL.Path, "/v1/users/me") && id != int(claims["id"].(float64)) && !hasPermission(user, "users") { NoAccess.Ferror(w) diff --git a/routes/characters.go b/routes/characters.go new file mode 100644 index 0000000..1494f1e --- /dev/null +++ b/routes/characters.go @@ -0,0 +1,93 @@ +package routes + +import ( + "backend/database" + "database/sql" + "encoding/json" + "errors" + "github.com/go-chi/chi/v5" + "net/http" +) + +func RegisterCharactersRoutes(r chi.Router) { + r.Route("/", func(s chi.Router) { + s.Get("/", getCharacters) + s.Post("/", createCharacter) + }) + r.Route("/{id:[0-9]+}", func(s chi.Router) { + s.Get("/", getCharacter) + s.Put("/", updateCharacter) + s.Delete("/", deleteCharacter) + }) + r.Get("/characters/{id:[0-9]+}/novels", getCharacterNovels) +} + +func getCharacters(w http.ResponseWriter, r *http.Request) { + characters, err := database.FetchCharacters() + Response(w, err, characters) +} +func createCharacter(w http.ResponseWriter, r *http.Request) { + Response(w, nil, "todo") +} + +func getCharacter(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + char, err := database.FetchCharacter(id) + Response(w, err, char) +} +func updateCharacter(w http.ResponseWriter, r *http.Request) { + user, err := GetCurrentUser(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(database.Character) + err = json.Unmarshal(body, character) + if err != nil { + Error(w, err) + return + } + + character, err = database.UpdateCharacter(character) + Response(w, err, character) +} +func deleteCharacter(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + + user, err := GetCurrentUser(r) + if err != nil { + NoAccess.Ferror(w) + return + } + + hasPerm := hasPermission(user, "characters") + if !hasPerm { + NoAccess.Ferror(w) + return + } + + err = database.DeleteCharacter(id) + if errors.Is(err, sql.ErrNoRows) { + NotFound.Ferror(w) + return + } + Response(w, err, "ok") +} + +func getCharacterNovels(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + novels, err := database.FetchCharacterNovels(id) + Response(w, err, novels) +} diff --git a/routes/clubs.go b/routes/clubs.go new file mode 100644 index 0000000..c9b0a95 --- /dev/null +++ b/routes/clubs.go @@ -0,0 +1,38 @@ +package routes + +import ( + "backend/database" + "github.com/go-chi/chi/v5" + "net/http" +) + +func RegisterClubRoutes(r chi.Router) { + r.Route("/", func(s chi.Router) { + s.Get("/", getClubs) + }) + r.Route("/{id:[0-9]+}", func(s chi.Router) { + s.Get("/", getClub) + }) +} + +func getClubs(w http.ResponseWriter, r *http.Request) { + userId, err := GetCurrentUserID(r) + if err != nil { + Error(w, err) + return + } + clubs, err := database.GetClubs(int(userId)) + Response(w, err, clubs) +} + +func getClub(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + + userId, err := GetCurrentUserID(r) + if err != nil { + Error(w, err) + return + } + club, err := database.GetClub(id, userId) + Response(w, err, club) +} diff --git a/v1/errors.go b/routes/errors.go old mode 100755 new mode 100644 similarity index 99% rename from v1/errors.go rename to routes/errors.go index ebfeed6..ecf36a9 --- a/v1/errors.go +++ b/routes/errors.go @@ -1,4 +1,4 @@ -package v1 +package routes import ( "encoding/json" diff --git a/routes/games.go b/routes/games.go new file mode 100644 index 0000000..06239f0 --- /dev/null +++ b/routes/games.go @@ -0,0 +1,17 @@ +package routes + +import ( + "backend/database" + "github.com/go-chi/chi/v5" + "net/http" +) + +func RegisterGamesRoutes(r chi.Router) { + r.Get("/{id:[0-9]+}", getGame) +} + +func getGame(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + game, err := database.FetchGame(int32(id)) + Response(w, err, game) +} diff --git a/routes/genres.go b/routes/genres.go new file mode 100644 index 0000000..7f40418 --- /dev/null +++ b/routes/genres.go @@ -0,0 +1,42 @@ +package routes + +import ( + "backend/database" + "github.com/go-chi/chi/v5" + "net/http" +) + +func RegisterGenresRoutes(r chi.Router) { + r.Route("/", func(s chi.Router) { + s.Get("/", getGenres) + s.Post("/", addGenre) + }) + r.Route("/{id:[0-9]+}", func(s chi.Router) { + s.Get("/", getGenre) + }) + r.Route("/{id:[0-9]+}/novels", func(s chi.Router) { + s.Get("/", getGenreNovels) + }) +} + +func getGenres(w http.ResponseWriter, r *http.Request) { + genres, err := database.FetchGenres() + Response(w, err, genres) +} +func addGenre(w http.ResponseWriter, r *http.Request) { + RawError(w, "method not implemented") +} + +func getGenre(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + + genre, err := database.FetchGenre(id) + Response(w, err, genre) +} + +func getGenreNovels(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + + novels, err := database.FetchGenreNovels(id) + Response(w, err, novels) +} diff --git a/routes/groups.go b/routes/groups.go new file mode 100644 index 0000000..75eb848 --- /dev/null +++ b/routes/groups.go @@ -0,0 +1,27 @@ +package routes + +import ( + "backend/database" + "github.com/go-chi/chi/v5" + "net/http" +) + +func RegisterGroupsRoutes(r chi.Router) { + r.Route("/groups", func(s chi.Router) { + s.Get("/", getGroups) + }) + r.Route("/groups/{id:[0-9]+}", func(s chi.Router) { + s.Get("/", getGroup) + }) +} + +func getGroups(w http.ResponseWriter, r *http.Request) { + groups, err := database.GetGroups() + Response(w, err, groups) +} + +func getGroup(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + group, err := database.GetGroup(id) + Response(w, err, group) +} diff --git a/v1/lp.go b/routes/lp.go old mode 100755 new mode 100644 similarity index 87% rename from v1/lp.go rename to routes/lp.go index be4aaac..ec206df --- a/v1/lp.go +++ b/routes/lp.go @@ -1,24 +1,25 @@ -package v1 +package routes import ( + "backend/database" + "errors" "fmt" + "github.com/go-chi/chi/v5" "net/http" "time" - - "github.com/gorilla/mux" ) -func RegisterLongPollRoutes(r *mux.Router) { - r.HandleFunc("/lp", resolveLongPoll).Methods("GET", "OPTIONS", "POST") +func RegisterLongPollRoutes(r chi.Router) { + r.Get("/", resolveLongPoll) } func resolveLongPoll(w http.ResponseWriter, r *http.Request) { userId, err := GetCurrentUserID(r) - if err == ErrNotAuthorized { + if errors.Is(err, database.ErrNotAuthorized) { NoToken.Ferror(w) return } - if err == http.ErrNoCookie { + if errors.Is(err, http.ErrNoCookie) { NoToken.Ferror(w) return } diff --git a/routes/news.go b/routes/news.go new file mode 100644 index 0000000..ed7ef5b --- /dev/null +++ b/routes/news.go @@ -0,0 +1,115 @@ +package routes + +import ( + "backend/database" + "encoding/json" + "github.com/go-chi/chi/v5" + "net/http" + "strconv" +) + +func RegisterNewsRoutes(r chi.Router) { + r.Route("/", func(s chi.Router) { + s.Get("/", getNews) + s.Post("/", addPost) + }) + r.Route("/{id:[0-9]+}", func(s chi.Router) { + s.Get("/", getPost) + s.Put("/", updatePost) + s.Delete("/", deletePost) + }) +} + +func getNews(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + + offset, _ := strconv.Atoi(q.Get("offset")) + limit, _ := strconv.Atoi(q.Get("limit")) + if limit == 0 { + limit = 20 + } + + total, err := database.FetchTotalNews() + if err != nil { + Error(w, err) + return + } + + pagination := map[string]interface{}{ + "offset": offset, + "limit": limit, + "total": total, + } + + news, err := database.FetchNews(offset, limit) + ResponsePagination(w, err, news, pagination) +} + +func addPost(w http.ResponseWriter, r *http.Request) { + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + post := new(database.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 := database.CreatePost(post) + Response(w, err, createdPost) +} + +func getPost(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + post, err := database.FetchPost(id) + Response(w, err, post) +} +func updatePost(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + post, err := database.FetchPost(id) + if err != nil { + Error(w, err) + return + } + + err = json.Unmarshal(body, post) + if err != nil { + Error(w, err) + return + } + + err = database.UpdatePost(post) + if err != nil { + Error(w, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} +func deletePost(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "id") + err := database.DeletePost(id) + if err != nil { + Error(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/routes/novels.go b/routes/novels.go new file mode 100644 index 0000000..c3ebf42 --- /dev/null +++ b/routes/novels.go @@ -0,0 +1,245 @@ +package routes + +import ( + "backend/database" + "database/sql" + "encoding/json" + "errors" + "fmt" + "github.com/go-chi/chi/v5" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" +) + +func RegisterNovelsRoutes(r chi.Router) { + r.Route("/", func(s chi.Router) { + s.Get("/", getNovels) + s.Post("/", createNovel) + }) + r.Route("/{novelId:[0-9]+}", func(s chi.Router) { + s.Get("/", getNovel) + s.Put("/", updateNovel) + s.Delete("/", deleteNovel) + }) + r.Post("/{novelId:[0-9]+}/cover", uploadNovelCover) + r.Route("/{novelId:[0-9]+}/comments", func(s chi.Router) { + s.Get("/", getNovelComments) + s.Post("/", createNovelComment) + }) + r.Get("/{novelId:[0-9]+}/characters", resolveNovelCharacters) + r.Get("/{novelId:[0-9]+}/genres", resolveNovelGenres) +} + +func getNovels(w http.ResponseWriter, r *http.Request) { + 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 := database.SearchNovels(query[0]) + Response(w, err, novels) + return + } + + novels, err := database.FetchNovels(sortBy[0]) + Response(w, err, &novels) +} +func createNovel(w http.ResponseWriter, r *http.Request) { + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + var novel database.Novel + err = json.Unmarshal(body, &novel) + if err != nil { + Error(w, err) + return + } + + novel, err = database.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 getNovel(w http.ResponseWriter, r *http.Request) { + novelId := IURLParam(r, "novelId") + novel, err := database.FetchNovel(novelId) + Response(w, err, novel) +} +func updateNovel(w http.ResponseWriter, r *http.Request) { + novelId := IURLParam(r, "novelId") + + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + novel, err := database.FetchNovel(novelId) + if err != nil { + Response(w, err, nil) + return + } + err = json.Unmarshal(body, novel) + if err != nil { + Error(w, err) + return + } + + novel, err = database.UpdateNovel(novel) + Response(w, err, novel) +} +func deleteNovel(w http.ResponseWriter, r *http.Request) { + novelId := IURLParam(r, "novelId") + _, err := database.FetchNovel(novelId) + if errors.Is(err, sql.ErrNoRows) { + NotFound.Ferror(w) + return + } + + err = database.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 := GetCurrentUser(r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + + if !hasPermission(user, "novel") { + NoAccess.Ferror(w) + return + } + + novelId := IURLParam(r, "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 := io.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(os.Getenv("CDN_PATH"), "novels", "logo", fmt.Sprintf("%v.%v", novelId, ext)) + + err = os.WriteFile(path, fileBytes, 0644) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + coverPath := fmt.Sprintf("%v/novels/logo/%v.%v", os.Getenv("CDN_URL"), novelId, ext) + err = database.UpdateNovelCover(novelId, coverPath) + Response(w, err, coverPath) +} + +func getNovelComments(w http.ResponseWriter, r *http.Request) { + novelId := IURLParam(r, "novelId") + 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 := database.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) +} + +func createNovelComment(w http.ResponseWriter, r *http.Request) { + novelId := IURLParam(r, "novelId") + user, err := GetCurrentUser(r) + if err != nil { + Error(w, err) + return + } + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + commentData := database.CommentNovel{NovelID: int16(novelId), UserID: user.ID} + err = json.Unmarshal(body, &commentData) + if err != nil { + Error(w, err) + return + } + + comment, err := database.AddNovelComment(commentData) + Response(w, err, comment) +} + +func resolveNovelGenres(w http.ResponseWriter, r *http.Request) { + novelId := IURLParam(r, "novelId") + genres, err := database.FetchNovelGenres(novelId) + Response(w, err, genres) +} + +func resolveNovelCharacters(w http.ResponseWriter, r *http.Request) { + novelId := IURLParam(r, "novelId") + characters, err := database.FetchNovelCharacters(novelId) + Response(w, err, characters) +} diff --git a/v1/ratelimiter.go b/routes/ratelimiter.go similarity index 93% rename from v1/ratelimiter.go rename to routes/ratelimiter.go index d751992..0ee662e 100644 --- a/v1/ratelimiter.go +++ b/routes/ratelimiter.go @@ -1,4 +1,4 @@ -package v1 +package routes import ( "net" @@ -38,7 +38,7 @@ func getVisitor(ip string) *rate.Limiter { v, exists := visitors[ip] if !exists { limiter := rate.NewLimiter(1, 20) - visitors[ip] = &visitor{ limiter, time.Now() } + visitors[ip] = &visitor{limiter, time.Now()} return limiter } diff --git a/v1/general_routes.go b/routes/routes.go similarity index 60% rename from v1/general_routes.go rename to routes/routes.go index 9158e60..3a084ba 100644 --- a/v1/general_routes.go +++ b/routes/routes.go @@ -1,63 +1,57 @@ -package v1 +package routes import ( - "crypto/md5" + "backend/database" + "backend/utils" "database/sql" - "encoding/hex" "encoding/json" + "errors" "fmt" - "io/ioutil" + "github.com/getsentry/sentry-go" + "github.com/go-chi/chi/v5" + "github.com/golang-jwt/jwt/v5" + "io" "log" "net/http" + "os" "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() - +func RegisterRouters(r chi.Router) { lpServer = InitLongPollServer() - sub.Use(apiMiddleware) - sub.Use(LimitMiddleware) + r.Use(apiMiddleware) + r.Use(LimitMiddleware) go CleanupVisitors() - sub.Use(JWTMiddleware) + r.Use(JWTMiddleware) - sub.HandleFunc("/version", func(w http.ResponseWriter, _ *http.Request) { + r.Get("/version", func(w http.ResponseWriter, _ *http.Request) { Response(w, nil, map[string]string{ - "version": config.Version, - "build": strconv.Itoa(int(config.Build)), + "version": os.Getenv("VERSION"), + "build": os.Getenv("BUILD"), }) }) - RegisterAuthRoutes(sub) - RegisterUsersRoutes(sub) - RegisterNovelsRoutes(sub) - RegisterCharactersRoutes(sub) - RegisterGenresRoutes(sub) - RegisterGamesRoutes(sub) - RegisterClubRoutes(sub) - RegisterGroupsRoutes(sub) - RegisterNewsRoutes(sub) - RegisterTokensRouter(sub) - RegisterLongPollRoutes(sub) + r.Route("/auth", RegisterAuthRoutes) + r.Route("/users", RegisterUsersRoutes) + r.Route("/novels", RegisterNovelsRoutes) + r.Route("/characters", RegisterCharactersRoutes) + r.Route("/genres", RegisterGenresRoutes) + r.Route("/games", RegisterGamesRoutes) + r.Route("/clubs", RegisterClubRoutes) + r.Route("/groups", RegisterGroupsRoutes) + r.Route("/news", RegisterNewsRoutes) + r.Route("/tokens", RegisterTokensRouter) + r.Route("/lp", RegisterLongPollRoutes) - 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) + if utils.IsDevelopment() && utils.IsDebug() { + err := chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + log.Printf("[%s]: %s\n", method, route) return nil }) - if err != nil { log.Fatalln(err) } @@ -68,7 +62,7 @@ func Response(w http.ResponseWriter, err error, data interface{}) { const baseResponse = `{"error":false,"data":%v}` if err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { NotFound.Ferror(w) } else { w.WriteHeader(http.StatusInternalServerError) @@ -94,7 +88,7 @@ func ResponsePagination(w http.ResponseWriter, err error, data interface{}, pagi const baseResponse = `{"error":false,"pagination":%v,"data":%v}` if err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { NotFound.Ferror(w) } else { w.WriteHeader(http.StatusInternalServerError) @@ -152,7 +146,7 @@ func JSON(w http.ResponseWriter, data string) { } func ReadBody(r *http.Request) ([]byte, error) { - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) if err != nil { return nil, err } @@ -161,10 +155,14 @@ func ReadBody(r *http.Request) ([]byte, error) { return body, err } -func MD5(s string) string { - hash := md5.New() - hash.Write([]byte(s)) - return hex.EncodeToString(hash.Sum(nil)) +func IURLParam(r *http.Request, key string) int { + par := chi.URLParam(r, key) + i, err := strconv.Atoi(par) + if err != nil { + return 0 + } else { + return i + } } func GetCurrentUserID(r *http.Request) (uint64, error) { @@ -176,7 +174,7 @@ func GetCurrentUserID(r *http.Request) (uint64, error) { if token.Method != jwt.SigningMethodHS256 { return nil, fmt.Errorf("invalid singning method: %s", token.Method) } - return []byte(privateConfig.AuthTokenSecret), nil + return []byte(os.Getenv("AUTH_TOKEN_SECRET")), nil }) if err != nil { return 0, err @@ -186,13 +184,39 @@ func GetCurrentUserID(r *http.Request) (uint64, error) { return uint64(id), nil } +func GetCurrentUser(r *http.Request) (*database.User, error) { + cookie, err := r.Cookie("token") + if err != nil { + return nil, err + } + if len(cookie.Value) == 0 { + return nil, fmt.Errorf("token cookie is empty") + } + + user := new(database.User) + tokenString := cookie.Value + + 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(os.Getenv("AUTH_TOKEN_SECRET")), 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 { + user, err = database.FetchUser(int(claims["id"].(float64))) + } + return user, err +} + 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-Origin", utils.GetFrontendURL()) 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") @@ -203,13 +227,13 @@ func apiMiddleware(next http.Handler) http.Handler { return } - if database == nil { + if database.Database == nil { log.Println("db not initialized") return } - err := database.Ping() + err := database.Database.Ping() if err != nil && strings.Contains(err.Error(), "broken pipe") { - ConnectToDB() + database.ConnectToDB() log.Println("connection to db reopened") } diff --git a/routes/tokens.go b/routes/tokens.go new file mode 100644 index 0000000..1749719 --- /dev/null +++ b/routes/tokens.go @@ -0,0 +1,40 @@ +package routes + +import ( + "backend/database" + "errors" + "github.com/go-chi/chi/v5" + "net/http" +) + +func RegisterTokensRouter(r chi.Router) { + r.Post("/auth", auth) + r.Route("/apps", func(s chi.Router) { + s.Get("/", getApps) + s.Get("/{id:[0-9]+}", getApp) + }) +} + +func auth(w http.ResponseWriter, r *http.Request) { + tokenCookie, err := r.Cookie("token") + if errors.Is(err, http.ErrNoCookie) || len(tokenCookie.Value) == 0 { + Unauthorized.Ferror(w) + return + } +} + +func getApps(w http.ResponseWriter, r *http.Request) { + user, err := GetCurrentUser(r) + if err != nil { + Error(w, err) + return + } + if !hasPermission(user, "apps") { + NoAccess.Ferror(w) + return + } + apps, err := database.FetchApps() + Response(w, err, apps) +} + +func getApp(w http.ResponseWriter, r *http.Request) {} diff --git a/routes/users.go b/routes/users.go new file mode 100644 index 0000000..8afd75e --- /dev/null +++ b/routes/users.go @@ -0,0 +1,300 @@ +package routes + +import ( + "backend/database" + "bytes" + "database/sql" + "encoding/json" + "errors" + "fmt" + "github.com/go-chi/chi/v5" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" +) + +func RegisterUsersRoutes(r chi.Router) { + r.Get("/", getUsers) + r.Get("/me", getCurrentUser) + r.Post("/me/avatar", uploadAvatar) + r.Route("/me/settings", func(s chi.Router) { + s.Get("/", getSettings) + s.Post("/", updateSettings) + }) + r.Route("/{userId:[0-9]+}", func(s chi.Router) { + s.Get("/", getUser) + s.Put("/", updateUser) + }) + r.Route("/{userId:[0-9]+}/novels", func(s chi.Router) { + s.Get("/", getUserNovels) + s.Post("/", addUserNovel) + }) + r.Route("/{userId:[0-9]+}/novels/{novelId:[0-9]+}", func(s chi.Router) { + s.Get("/", getUserNovel) + s.Put("/", updateUserNovel) + s.Delete("/", deleteUserNovel) + }) + r.Get("/{userId:[0-9]+}/novels/comments", resolveUserNovelsComments) +} + +func getUsers(w http.ResponseWriter, r *http.Request) { + users, err := database.FetchUsers() + Response(w, err, users) +} + +func getCurrentUser(w http.ResponseWriter, r *http.Request) { + user, err := GetCurrentUser(r) + if errors.Is(err, database.ErrNotAuthorized) { + NoToken.Ferror(w) + return + } + if errors.Is(err, http.ErrNoCookie) { + NoToken.Ferror(w) + return + } + Response(w, err, user) +} + +func uploadAvatar(w http.ResponseWriter, r *http.Request) { + user, err := GetCurrentUser(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 := io.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(os.Getenv("CDN_PATH"), "users", "avatars", fmt.Sprintf("%v.%v", user.ID, ext)) + + err = os.WriteFile(path, fileBytes, 0644) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + avatarPath := fmt.Sprintf("%v/avatars/%v.%v", os.Getenv("CDN_URL"), user.ID, ext) + Response(w, nil, avatarPath) +} + +func getSettings(w http.ResponseWriter, r *http.Request) { + id, err := GetCurrentUserID(r) + if err != nil { + Error(w, err) + return + } + settings, err := database.FetchUserSettings(id) + Response(w, err, settings) +} +func updateSettings(w http.ResponseWriter, r *http.Request) { + id, err := GetCurrentUserID(r) + settings, err := database.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 := database.UpdateUserSettings(id, settings) + Response(w, err, user) +} + +func getUser(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(chi.URLParam(r, "userId")) + user, err := database.FetchUser(id) + Response(w, err, user) +} +func updateUser(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(chi.URLParam(r, "userId")) + + currentUser, err := GetCurrentUser(r) + if err != nil && !errors.Is(err, database.ErrNotAuthorized) { + Error(w, err) + return + } else if errors.Is(err, database.ErrNotAuthorized) || currentUser == nil { + currentUser = &database.User{ID: -1} + } + if currentUser.ID != int64(id) && !hasPermission(currentUser, "users") { + NoAccess.Ferror(w) + return + } + _, err = database.FetchUser(id) + if err != nil { + Error(w, err) + return + } + + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + var u *database.User + err = json.Unmarshal(body, u) + if err != nil { + Error(w, err) + return + } + user, err := database.UpdateUser(u) + Response(w, err, user) +} + +func getUserNovels(w http.ResponseWriter, r *http.Request) { + userId, _ := strconv.Atoi(chi.URLParam(r, "userId")) + + novels, err := database.FetchUserNovels(userId) + Response(w, err, novels) +} +func addUserNovel(w http.ResponseWriter, r *http.Request) { + userId, _ := strconv.Atoi(chi.URLParam(r, "userId")) + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + var novel = &database.UserNovels{} + err = json.Unmarshal(body, novel) + if err != nil { + Error(w, err) + return + } + + novel.UserID = int64(userId) + + novel, err = database.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 getUserNovel(w http.ResponseWriter, r *http.Request) { + userId := IURLParam(r, "userId") + novelId := IURLParam(r, "novelId") + novel, err := database.FetchUserNovel(userId, novelId) + Response(w, err, novel) + +} +func updateUserNovel(w http.ResponseWriter, r *http.Request) { + userId := IURLParam(r, "userId") + novelId := IURLParam(r, "novelId") + + _, err := database.FetchNovel(novelId) + if errors.Is(err, sql.ErrNoRows) { + NotFound.Ferror(w) + return + } + + _, err = database.FetchUser(userId) + if errors.Is(err, sql.ErrNoRows) { + NotFound.Ferror(w) + return + } + + novel, _ := database.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 := database.UpdateUserNovel(novel) + Response(w, err, updatedNovel) +} +func deleteUserNovel(w http.ResponseWriter, r *http.Request) { + userId := IURLParam(r, "userId") + novelId := IURLParam(r, "novelId") + _, err := database.FetchUserNovel(userId, novelId) + if errors.Is(err, sql.ErrNoRows) { + NotFound.Ferror(w) + return + } + + err = database.DeleteUserNovel(userId, novelId) + if err != nil { + Error(w, err) + } + w.WriteHeader(http.StatusNoContent) +} + +func resolveUserNovelsComments(w http.ResponseWriter, r *http.Request) { + id := IURLParam(r, "userId") + + comments, err := database.FetchUserNovelsComments(id) + Response(w, err, comments) +} diff --git a/v1/discord.go b/utils/discord.go similarity index 97% rename from v1/discord.go rename to utils/discord.go index d995d99..4e33f48 100644 --- a/v1/discord.go +++ b/utils/discord.go @@ -1,4 +1,4 @@ -package v1 +package utils import ( "bytes" diff --git a/utils/env.go b/utils/env.go new file mode 100644 index 0000000..85ba415 --- /dev/null +++ b/utils/env.go @@ -0,0 +1,27 @@ +package utils + +import ( + "os" +) + +func GetFrontendURL() string { + if IsDevelopment() { + return os.Getenv("DEV_FRONTEND_URL") + } + return os.Getenv("FRONTEND_URL") +} + +func IsDevelopment() bool { + return os.Getenv("DEVELOPMENT") == "true" +} +func IsDebug() bool { + return os.Getenv("DEBUG") == "true" +} + +func Getenv(key, def string) string { + value := os.Getenv(key) + if value == "" { + return def + } + return value +} diff --git a/v1/mail.go b/utils/mail.go similarity index 69% rename from v1/mail.go rename to utils/mail.go index d36730c..49c0a49 100644 --- a/v1/mail.go +++ b/utils/mail.go @@ -1,4 +1,4 @@ -package v1 +package utils import ( "bytes" @@ -18,9 +18,11 @@ const ( "%v\r\n" ) +var mailAuth = smtp.PlainAuth("", os.Getenv("MAIL_USERNAME"), os.Getenv("MAIL_PASSWORD"), os.Getenv("MAIL_HOST")) + 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) + msg := []byte(fmt.Sprintf(format, os.Getenv("MAIL_USERNAME"), to, subject, body)) + err := smtp.SendMail(os.Getenv("MAIL_HOST")+":587", mailAuth, os.Getenv("MAIL_USERNAME"), []string{to}, msg) if err != nil { log.Println(err) } diff --git a/v1/utils.go b/utils/utils.go similarity index 88% rename from v1/utils.go rename to utils/utils.go index 5351301..5a2d182 100644 --- a/v1/utils.go +++ b/utils/utils.go @@ -1,6 +1,8 @@ -package v1 +package utils import ( + "crypto/md5" + "encoding/hex" "reflect" "strings" ) @@ -61,5 +63,10 @@ func GetStructAndNamedForDatabase(s interface{}) [][]string { } } return out +} +func MD5(s string) string { + hash := md5.New() + hash.Write([]byte(s)) + return hex.EncodeToString(hash.Sum(nil)) } diff --git a/v1/characters_routes.go b/v1/characters_routes.go deleted file mode 100755 index f7a29b1..0000000 --- a/v1/characters_routes.go +++ /dev/null @@ -1,88 +0,0 @@ -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) -} diff --git a/v1/club_routes.go b/v1/club_routes.go deleted file mode 100644 index 3957d45..0000000 --- a/v1/club_routes.go +++ /dev/null @@ -1,40 +0,0 @@ -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) - } -} diff --git a/v1/config.go b/v1/config.go deleted file mode 100644 index aea6470..0000000 --- a/v1/config.go +++ /dev/null @@ -1,68 +0,0 @@ -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) -} diff --git a/v1/games_routes.go b/v1/games_routes.go deleted file mode 100644 index e1f7b29..0000000 --- a/v1/games_routes.go +++ /dev/null @@ -1,19 +0,0 @@ -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) -} - diff --git a/v1/games_sql.go b/v1/games_sql.go deleted file mode 100644 index 9ceaede..0000000 --- a/v1/games_sql.go +++ /dev/null @@ -1,18 +0,0 @@ -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 -} \ No newline at end of file diff --git a/v1/general_sql.go b/v1/general_sql.go deleted file mode 100644 index 29e16d6..0000000 --- a/v1/general_sql.go +++ /dev/null @@ -1,23 +0,0 @@ -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) -} diff --git a/v1/genres_routes.go b/v1/genres_routes.go deleted file mode 100755 index 836cc0f..0000000 --- a/v1/genres_routes.go +++ /dev/null @@ -1,38 +0,0 @@ -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) -} diff --git a/v1/groups_routes.go b/v1/groups_routes.go deleted file mode 100644 index ce7a11b..0000000 --- a/v1/groups_routes.go +++ /dev/null @@ -1,31 +0,0 @@ -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) - } -} \ No newline at end of file diff --git a/v1/groups_sql.go b/v1/groups_sql.go deleted file mode 100644 index 6a7e741..0000000 --- a/v1/groups_sql.go +++ /dev/null @@ -1,23 +0,0 @@ -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 -} \ No newline at end of file diff --git a/v1/news_routes.go b/v1/news_routes.go deleted file mode 100755 index 5a729bb..0000000 --- a/v1/news_routes.go +++ /dev/null @@ -1,107 +0,0 @@ -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) - } -} diff --git a/v1/novels_routes.go b/v1/novels_routes.go deleted file mode 100644 index cc61481..0000000 --- a/v1/novels_routes.go +++ /dev/null @@ -1,239 +0,0 @@ -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) - } -} diff --git a/v1/tokens_routes.go b/v1/tokens_routes.go deleted file mode 100644 index 56b5185..0000000 --- a/v1/tokens_routes.go +++ /dev/null @@ -1,38 +0,0 @@ -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) -} diff --git a/v1/tokens_sql.go b/v1/tokens_sql.go deleted file mode 100644 index 47d72b5..0000000 --- a/v1/tokens_sql.go +++ /dev/null @@ -1,20 +0,0 @@ -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 -} \ No newline at end of file diff --git a/v1/users_routes.go b/v1/users_routes.go deleted file mode 100755 index c81afc3..0000000 --- a/v1/users_routes.go +++ /dev/null @@ -1,300 +0,0 @@ -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) -}