Compare commits

...

6 Commits

Author SHA1 Message Date
dfe00f8359 add mock 2023-09-12 17:08:17 +07:00
dcacc9288d fix user endpoint 2023-09-12 17:07:57 +07:00
d1ba0251c0 update user model 2023-09-12 17:07:03 +07:00
4f962bb104 add data seed and updae users table 2023-09-12 12:52:01 +07:00
96dfa4ae29 add commets drop table 2023-09-11 14:24:11 +07:00
ed608f170a add migration for comments 2023-09-11 12:20:10 +07:00
27 changed files with 696 additions and 53 deletions

View File

@ -2,12 +2,17 @@ include dev.env
migrateup: migrateup:
migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -verbose up migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -verbose up
migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}_test?sslmode=disable" -verbose up
migratedown: migratedown:
migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -verbose down $N migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -verbose down $N
migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}_test?sslmode=disable" -verbose down $N
seed:
./import_csv.sh
mock-generate: mock-generate:
mockgen -package mockdb -destination db/mock/store.go git.nochill.in/nochill/naice_pos/db/sqlc Store mockgen -package mockdb -destination db/mock/store.go git.nochill.in/nochill/hiling_go/db/sqlc Store
sqlc: sqlc:
sqlc generate && make mock-generate sqlc generate && make mock-generate

View File

@ -2,14 +2,14 @@ package api
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
func errorResponse(err error, msg string) gin.H { func ErrorResponse(err error, msg string) gin.H {
return gin.H{ return gin.H{
"error": err.Error(), "error": err.Error(),
"message": msg, "message": msg,
} }
} }
func validResponse(data interface{}, msg string) gin.H { func ValidResponse(data interface{}, msg string) gin.H {
return gin.H{ return gin.H{
"message": msg, "message": msg,
"data": data, "data": data,

View File

@ -13,7 +13,7 @@ type Server struct {
config util.Config config util.Config
store db.Store store db.Store
tokenMaker token.Maker tokenMaker token.Maker
router *gin.Engine Router *gin.Engine
} }
func NewServer(config util.Config, store db.Store) (*Server, error) { func NewServer(config util.Config, store db.Store) (*Server, error) {
@ -35,11 +35,11 @@ func NewServer(config util.Config, store db.Store) (*Server, error) {
func (server *Server) getRoutes() { func (server *Server) getRoutes() {
router := gin.Default() router := gin.Default()
router.POST("/user/create", server.createUser) router.POST("/user/signup", server.createUser)
server.router = router server.Router = router
} }
func (server *Server) Start(address string) error { func (server *Server) Start(address string) error {
return server.router.Run(address) return server.Router.Run(address)
} }

32
api/test/main_test.go Normal file
View File

@ -0,0 +1,32 @@
package api_test
import (
"os"
"testing"
"time"
api "git.nochill.in/nochill/hiling_go/api"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
"github.com/stretchr/testify/require"
)
func newTestServer(t *testing.T, store db.Store) *api.Server {
config := util.Config{
TokenSymmetricKey: util.RandomString(32),
TokenDuration: time.Minute,
}
server, err := api.NewServer(config, store)
require.NoError(t, err)
return server
}
func TestMain(m *testing.M) {
gin.SetMode(gin.TestMode)
os.Exit(m.Run())
}

60
api/test/user_test.go Normal file
View File

@ -0,0 +1,60 @@
package api_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
mockdb "git.nochill.in/nochill/hiling_go/db/mock"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestSignupAPI(t *testing.T) {
user, pass := createUser(t)
t.Run("OK", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockdb.NewMockStore(ctrl)
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(user, nil)
server := newTestServer(t, store)
recorder := httptest.NewRecorder()
data, err := json.Marshal(gin.H{
"username": user.Username,
"password": pass,
})
require.NoError(t, err)
url := "/user/signup"
request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
require.NoError(t, err)
server.Router.ServeHTTP(recorder, request)
require.Equal(t, http.StatusOK, recorder.Code)
})
}
func createUser(t *testing.T) (user db.User, password string) {
passw := util.RandomString(10)
hashedPassword, err := util.HashPassword(passw)
require.NoError(t, err)
user = db.User{
Username: util.RandomString(10),
Password: hashedPassword,
}
return
}

View File

@ -1,27 +1,106 @@
package api package api
import ( import (
"database/sql"
"errors"
"fmt"
"net/http" "net/http"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/lib/pq"
) )
type CreateUserRequest struct { type createUserRequest struct {
ClientIpV4 string `json:"client_ip" binding:"required"` Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,min=7"`
}
type createUserResponse struct {
ID int32 `json:"id"`
Username string `json:"username"`
AvatarPicture string `json:"avatar_picture"` // avatar_url
BannedAt sql.NullTime `json:"banned_at"`
BannedUntil sql.NullTime `json:"banned_until"`
BanReason string `json:"ban_reason"`
IsPermaban sql.NullBool `json:"is_permaban"`
IsAdmin sql.NullBool `json:"is_admin"`
IsCritics sql.NullBool `json:"is_critics"`
IsVerified sql.NullBool `json:"is_verified"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type ApiError struct {
Field string
Msg string
}
func msgForTag(field string, param string, tag string) string {
switch tag {
case "min":
return fmt.Sprintf("%s character min %s", field, param)
default:
return fmt.Sprintf("%s %s", field, tag)
}
} }
func (server *Server) createUser(ctx *gin.Context) { func (server *Server) createUser(ctx *gin.Context) {
var req CreateUserRequest var req createUserRequest
if err := ctx.ShouldBindJSON(&req); err != nil { if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err, "")) if err != nil {
return var ve validator.ValidationErrors
if errors.As(err, &ve) {
out := make([]ApiError, len(ve))
for i, fe := range ve {
out[i] = ApiError{fe.Field(), msgForTag(fe.Field(), fe.Param(), fe.Tag())}
}
ctx.JSON(http.StatusBadRequest, gin.H{"errors": out})
}
return
}
} }
err := server.store.CreateUser(ctx, req.ClientIpV4) hashedPassword, err := util.HashPassword(req.Password)
if err != nil { if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err, "")) ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
return return
} }
ctx.Writer.WriteHeader(http.StatusOK) arg := db.CreateUserParams{
Username: req.Username,
Password: hashedPassword,
}
user, err := server.store.CreateUser(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "foreign_key_violation", "unique_violation":
ctx.JSON(http.StatusConflict, ErrorResponse(err, "Something went wrong while try to save"))
}
}
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
return
}
res := createUserResponse{
ID: user.ID,
Username: user.Username,
AvatarPicture: user.AvatarPicture.String,
BannedAt: sql.NullTime{Valid: user.BannedAt.Valid, Time: user.BannedAt.Time},
BannedUntil: sql.NullTime{Valid: user.BannedUntil.Valid, Time: user.BannedUntil.Time},
BanReason: user.BanReason.String,
IsPermaban: user.IsPermaban,
IsAdmin: user.IsAdmin,
IsCritics: user.IsCritics,
IsVerified: user.IsVerified,
CreatedAt: sql.NullTime{Valid: true, Time: user.CreatedAt.Time},
UpdatedAt: sql.NullTime{Valid: true, Time: user.UpdatedAt.Time},
}
ctx.JSON(http.StatusOK, res)
} }

View File

@ -0,0 +1,10 @@
id#address#name#submitted_by#thumbnail#regency_id#google_maps_link
1#Jalan Raya Beside the bridge Ubud#Murnis Warung#1#https://cdn.discordapp.com/attachments/743422487882104837/1150972798320267304/image.png#5104#https://www.google.com/maps/place/Murni's+Warung/@-8.5048696,115.2553417,19z/data=!4m6!3m5!1s0x2dd23d3e0ffaa071:0xf2fa69b4cb211e41!8m2!3d-8.5051184!4d115.2547196!16s%2Fg%2F1tdsmcq7?entry=ttu
2#Jl.Taman Wijaya Kusuma Ps. Baru Kecamatan Sawah Besar#Masjid Istiqlal#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/04/11/31/33/istiqlal-mosque-mesjid.jpg?w=500&h=-1&s=1#3173#https://www.google.com/maps/place/Masjid+Istiqlal/@-6.1703155,106.8308434,19z/data=!4m15!1m7!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!2sMasjid+Istiqlal!8m2!3d-6.17017!4d106.83139!16zL20vMDRzam1q!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!8m2!3d-6.17017!4d106.83139!15sCg9tYXNqaWQgaXN0aXFsYWxaESIPbWFzamlkIGlzdGlxbGFskgEGbW9zcXVl4AEA!16zL20vMDRzam1q?entry=ttu
3#Jl. Mayjend Sungkono no. 89#Hotel Ciputra World Surabaya#1#https://lh5.googleusercontent.com/p/AF1QipOvHDO-M6riRoqBrWU3MskhwL_bue8JmN9faq7Q=w500-h500-k-no#3578#https://www.google.com/maps/place/Ciputra+World+Hotel+Surabaya/@-7.2923061,112.7191552,15z/data=!4m2!3m1!1s0x0:0x736a9c49dcc2ac42?sa=X&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgVEAA&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgjEAc
4#Jl. Taman Safari No.101 . B Cibeureum Kec. Cisarua#Club Huis#1#https://media-cdn.tripadvisor.com/media/photo-o/0d/6a/5d/63/our-peaceful-backyard.jpg#3201#https://www.google.com/maps/place/Club+Huis/@-6.7027857,106.9453741,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69b679d7a09e01:0xf9fc2df396f09977!8m2!3d-6.7027857!4d106.947949!16s%2Fg%2F11c57lh8ky?entry=ttu
5#Desa Tambakrejo Kecamatan Sumbermanjing Wetan#Pulau Sempu#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/11/3b/06/a5/pulau-sempu.jpg?w=500&h=-1&s=1#3507#https://www.google.com/maps/place/Pulau+Sempu/@-8.446621,112.6746143,14z/data=!3m1!4b1!4m6!3m5!1s0x2dd60120edbc901f:0x8efd89687a308993!8m2!3d-8.4428564!4d112.6973355!16s%2Fm%2F0r8k540?entry=ttu
6#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu
7#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY
8#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu
9#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu
1 id#address#name#submitted_by#thumbnail#regency_id#google_maps_link
2 1#Jalan Raya Beside the bridge Ubud#Murni’s Warung#1#https://cdn.discordapp.com/attachments/743422487882104837/1150972798320267304/image.png#5104#https://www.google.com/maps/place/Murni's+Warung/@-8.5048696,115.2553417,19z/data=!4m6!3m5!1s0x2dd23d3e0ffaa071:0xf2fa69b4cb211e41!8m2!3d-8.5051184!4d115.2547196!16s%2Fg%2F1tdsmcq7?entry=ttu
3 2#Jl.Taman Wijaya Kusuma Ps. Baru Kecamatan Sawah Besar#Masjid Istiqlal#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/04/11/31/33/istiqlal-mosque-mesjid.jpg?w=500&h=-1&s=1#3173#https://www.google.com/maps/place/Masjid+Istiqlal/@-6.1703155,106.8308434,19z/data=!4m15!1m7!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!2sMasjid+Istiqlal!8m2!3d-6.17017!4d106.83139!16zL20vMDRzam1q!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!8m2!3d-6.17017!4d106.83139!15sCg9tYXNqaWQgaXN0aXFsYWxaESIPbWFzamlkIGlzdGlxbGFskgEGbW9zcXVl4AEA!16zL20vMDRzam1q?entry=ttu
4 3#Jl. Mayjend Sungkono no. 89#Hotel Ciputra World Surabaya#1#https://lh5.googleusercontent.com/p/AF1QipOvHDO-M6riRoqBrWU3MskhwL_bue8JmN9faq7Q=w500-h500-k-no#3578#https://www.google.com/maps/place/Ciputra+World+Hotel+Surabaya/@-7.2923061,112.7191552,15z/data=!4m2!3m1!1s0x0:0x736a9c49dcc2ac42?sa=X&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgVEAA&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgjEAc
5 4#Jl. Taman Safari No.101 . B Cibeureum Kec. Cisarua#Club Huis#1#https://media-cdn.tripadvisor.com/media/photo-o/0d/6a/5d/63/our-peaceful-backyard.jpg#3201#https://www.google.com/maps/place/Club+Huis/@-6.7027857,106.9453741,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69b679d7a09e01:0xf9fc2df396f09977!8m2!3d-6.7027857!4d106.947949!16s%2Fg%2F11c57lh8ky?entry=ttu
6 5#Desa Tambakrejo Kecamatan Sumbermanjing Wetan#Pulau Sempu#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/11/3b/06/a5/pulau-sempu.jpg?w=500&h=-1&s=1#3507#https://www.google.com/maps/place/Pulau+Sempu/@-8.446621,112.6746143,14z/data=!3m1!4b1!4m6!3m5!1s0x2dd60120edbc901f:0x8efd89687a308993!8m2!3d-8.4428564!4d112.6973355!16s%2Fm%2F0r8k540?entry=ttu
7 6#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu
8 7#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY
9 8#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu
10 9#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu

2
db/csv_seeder/user.csv Normal file
View File

@ -0,0 +1,2 @@
id,username,password
1,user123,password
1 id username password
2 1 user123 password

View File

View File

@ -1,4 +1,7 @@
DROP TABLE IF EXISTS location_images; DROP TABLE IF EXISTS location_images;
DROP TABLE IF EXISTS comments;
DROP TABLE IF EXISTS client_ips;
DROP TABLE IF EXISTS user_reports;
DROP TABLE IF EXISTS reviews; DROP TABLE IF EXISTS reviews;
DROP TABLE IF EXISTS locations; DROP TABLE IF EXISTS locations;
DROP TABLE IF EXISTS regencies; DROP TABLE IF EXISTS regencies;
@ -6,3 +9,7 @@ DROP TABLE IF EXISTS provinces;
DROP TABLE IF EXISTS regions; DROP TABLE IF EXISTS regions;
DROP TABLE IF EXISTS tags; DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS users;
DROP TYPE IF EXISTS user_reports_type;
DROP TYPE IF EXISTS comment_type;

View File

@ -1,20 +1,54 @@
CREATE TABLE users( CREATE TABLE users(
"id" serial primary key not null, "id" serial primary key not null,
"email" varchar unique, "email" varchar unique,
"username" varchar unique, "username" varchar unique not null,
"password" varchar, "password" varchar not null,
"avatar_picture" varchar, "avatar_picture" varchar,
"google_sign_in_payload" varchar, "google_sign_in_payload" varchar,
"banned_at" timestamp, "banned_at" timestamp,
"banned_until" timestamp, "banned_until" timestamp,
"is_admin" boolean, "ban_reason" varchar,
"is_critics" boolean, "is_permaban" boolean,
"is_verified" boolean, "is_admin" boolean,
"ipv4_address" varchar(15) not null, "is_critics" boolean,
"social_media" jsonb, "is_verified" boolean,
"created_at" timestamp default(now()), "social_media" jsonb,
"updated_at" timestamp default(now()) "created_at" timestamp default(now()),
); "updated_at" timestamp default(now())
);
CREATE TABLE client_ips(
"id" serial primary key not null,
"ipv4" varchar(15) not null,
"ipv6" varchar(40),
"banned_at" timestamp,
"banned_until" timestamp,
"reason" varchar,
"is_permaban" boolean,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);
CREATE TYPE user_reports_type as ENUM(
'comments',
'reviews',
'locations',
'users',
'stories'
);
CREATE TABLE user_reports(
"id" serial primary key not null,
"message" text not null,
"date" timestamp not null,
"report_target" integer not null,
"report_type" user_reports_type not null,
"submitted_by" integer references "users"("id") not null,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);
CREATE TABLE regions( CREATE TABLE regions(
"id" serial primary key not null, "id" serial primary key not null,
@ -41,12 +75,12 @@ CREATE TABLE regencies(
CREATE TABLE locations( CREATE TABLE locations(
"id" serial primary key not null, "id" serial primary key not null,
"address" varchar, "address" varchar not null,
"name" varchar, "name" varchar not null,
"google_maps_link" varchar, "google_maps_link" varchar,
"submitted_by" integer references "users"("id") not null, "submitted_by" integer references "users"("id") not null,
"total_visited" integer, "total_visited" integer,
"thumbnail" varchar, "thumbnail" varchar not null,
"regency_id" smallint references "regencies"("id") not null, "regency_id" smallint references "regencies"("id") not null,
"is_deleted" boolean, "is_deleted" boolean,
"created_at" timestamp default(now()), "created_at" timestamp default(now()),
@ -80,5 +114,21 @@ CREATE TABLE reviews (
"location_id" integer references "locations"("id") not null, "location_id" integer references "locations"("id") not null,
"created_at" timestamp default(now()), "created_at" timestamp default(now()),
"updated_at" timestamp default(now()) "updated_at" timestamp default(now())
);
CREATE TYPE comment_type AS ENUM(
'stories',
'news',
'reviews'
);
CREATE TABLE comments(
"id" serial primary key not null,
"submitted_by" integer not null,
"comment_on" integer not null,
"comment_type" comment_type not null,
"reply_to" integer,
"is_hide" boolean,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
); );

80
db/mock/store.go Normal file
View File

@ -0,0 +1,80 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: git.nochill.in/nochill/hiling_go/db/sqlc (interfaces: Store)
// Package mockdb is a generated GoMock package.
package mockdb
import (
context "context"
reflect "reflect"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
gomock "go.uber.org/mock/gomock"
)
// MockStore is a mock of Store interface.
type MockStore struct {
ctrl *gomock.Controller
recorder *MockStoreMockRecorder
}
// MockStoreMockRecorder is the mock recorder for MockStore.
type MockStoreMockRecorder struct {
mock *MockStore
}
// NewMockStore creates a new mock instance.
func NewMockStore(ctrl *gomock.Controller) *MockStore {
mock := &MockStore{ctrl: ctrl}
mock.recorder = &MockStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStore) EXPECT() *MockStoreMockRecorder {
return m.recorder
}
// CreateUser mocks base method.
func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUser", arg0, arg1)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUser indicates an expected call of CreateUser.
func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1)
}
// UpdatePassword mocks base method.
func (m *MockStore) UpdatePassword(arg0 context.Context, arg1 db.UpdatePasswordParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePassword", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdatePassword indicates an expected call of UpdatePassword.
func (mr *MockStoreMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockStore)(nil).UpdatePassword), arg0, arg1)
}
// UpdateUser mocks base method.
func (m *MockStore) UpdateUser(arg0 context.Context, arg1 db.UpdateUserParams) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", arg0, arg1)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockStoreMockRecorder) UpdateUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), arg0, arg1)
}

View File

@ -1,6 +1,22 @@
-- name: CreateUser :exec -- name: CreateUser :one
INSERT INTO users ( INSERT INTO users (
ipv4_address username,
) VALUES ( password
$1 ) VALUES ($1, $2)
); RETURNING *;
-- name: UpdateUser :one
UPDATE users
SET
email = COALESCE(sqlc.narg(email), email),
username = COALESCE(sqlc.narg(username), username),
avatar_picture = COALESCE(sqlc.narg(avatar_picture), avatar_picture)
WHERE
id = sqlc.arg(id)
RETURNING *;
-- name: UpdatePassword :exec
UPDATE users
SET password = $1
WHERE id = $2;

View File

@ -6,10 +6,124 @@ package db
import ( import (
"database/sql" "database/sql"
"database/sql/driver"
"fmt"
"time"
"github.com/sqlc-dev/pqtype" "github.com/sqlc-dev/pqtype"
) )
type CommentType string
const (
CommentTypeStories CommentType = "stories"
CommentTypeNews CommentType = "news"
CommentTypeReviews CommentType = "reviews"
)
func (e *CommentType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = CommentType(s)
case string:
*e = CommentType(s)
default:
return fmt.Errorf("unsupported scan type for CommentType: %T", src)
}
return nil
}
type NullCommentType struct {
CommentType CommentType `json:"comment_type"`
Valid bool `json:"valid"` // Valid is true if CommentType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullCommentType) Scan(value interface{}) error {
if value == nil {
ns.CommentType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.CommentType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullCommentType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.CommentType), nil
}
type UserReportsType string
const (
UserReportsTypeComments UserReportsType = "comments"
UserReportsTypeReviews UserReportsType = "reviews"
UserReportsTypeLocations UserReportsType = "locations"
UserReportsTypeUsers UserReportsType = "users"
UserReportsTypeStories UserReportsType = "stories"
)
func (e *UserReportsType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = UserReportsType(s)
case string:
*e = UserReportsType(s)
default:
return fmt.Errorf("unsupported scan type for UserReportsType: %T", src)
}
return nil
}
type NullUserReportsType struct {
UserReportsType UserReportsType `json:"user_reports_type"`
Valid bool `json:"valid"` // Valid is true if UserReportsType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullUserReportsType) Scan(value interface{}) error {
if value == nil {
ns.UserReportsType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.UserReportsType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullUserReportsType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.UserReportsType), nil
}
type ClientIp struct {
ID int32 `json:"id"`
Ipv4 string `json:"ipv4"`
Ipv6 sql.NullString `json:"ipv6"`
BannedAt sql.NullTime `json:"banned_at"`
BannedUntil sql.NullTime `json:"banned_until"`
Reason sql.NullString `json:"reason"`
IsPermaban sql.NullBool `json:"is_permaban"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type Comment struct {
ID int32 `json:"id"`
SubmittedBy int32 `json:"submitted_by"`
CommentOn int32 `json:"comment_on"`
CommentType CommentType `json:"comment_type"`
ReplyTo sql.NullInt32 `json:"reply_to"`
IsHide sql.NullBool `json:"is_hide"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type Location struct { type Location struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Address sql.NullString `json:"address"` Address sql.NullString `json:"address"`
@ -79,17 +193,29 @@ type Tag struct {
type User struct { type User struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Email sql.NullString `json:"email"` Email sql.NullString `json:"email"`
Username sql.NullString `json:"username"` Username string `json:"username"`
Password sql.NullString `json:"password"` Password string `json:"password"`
AvatarPicture sql.NullString `json:"avatar_picture"` AvatarPicture sql.NullString `json:"avatar_picture"`
GoogleSignInPayload sql.NullString `json:"google_sign_in_payload"` GoogleSignInPayload sql.NullString `json:"google_sign_in_payload"`
BannedAt sql.NullTime `json:"banned_at"` BannedAt sql.NullTime `json:"banned_at"`
BannedUntil sql.NullTime `json:"banned_until"` BannedUntil sql.NullTime `json:"banned_until"`
BanReason sql.NullString `json:"ban_reason"`
IsPermaban sql.NullBool `json:"is_permaban"`
IsAdmin sql.NullBool `json:"is_admin"` IsAdmin sql.NullBool `json:"is_admin"`
IsCritics sql.NullBool `json:"is_critics"` IsCritics sql.NullBool `json:"is_critics"`
IsVerified sql.NullBool `json:"is_verified"` IsVerified sql.NullBool `json:"is_verified"`
Ipv4Address string `json:"ipv4_address"`
SocialMedia pqtype.NullRawMessage `json:"social_media"` SocialMedia pqtype.NullRawMessage `json:"social_media"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type UserReport struct {
ID int32 `json:"id"`
Message string `json:"message"`
Date time.Time `json:"date"`
ReportTarget int32 `json:"report_target"`
ReportType UserReportsType `json:"report_type"`
SubmittedBy int32 `json:"submitted_by"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}

View File

@ -9,7 +9,9 @@ import (
) )
type Querier interface { type Querier interface {
CreateUser(ctx context.Context, ipv4Address string) error CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
} }
var _ Querier = (*Queries)(nil) var _ Querier = (*Queries)(nil)

32
db/sqlc/test/main_test.go Normal file
View File

@ -0,0 +1,32 @@
package db_test
import (
"database/sql"
"log"
"os"
"testing"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
_ "github.com/lib/pq"
)
var testQueries *db.Queries
var testDB *sql.DB
func TestMain(m *testing.M) {
var err error
config, err := util.LoadConfig("../../..")
if err != nil {
log.Fatal("cannot load config: ", err)
}
testDB, err = sql.Open(config.DBDriver, config.DBSourceTest)
if err != nil {
log.Fatal("cannot connect db: ", err)
}
testQueries = db.New(testDB)
os.Exit(m.Run())
}

View File

@ -0,0 +1,23 @@
package db_test
import (
"context"
"testing"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/stretchr/testify/require"
)
func TestCreateUser(t *testing.T) {
arg := db.CreateUserParams{
Username: util.RandomString(10),
Password: util.RandomString(10),
}
user, err := testQueries.CreateUser(context.Background(), arg)
require.NoError(t, err)
require.Equal(t, user.Username, arg.Username)
require.Equal(t, user.Password, arg.Password)
}

View File

@ -7,17 +7,105 @@ package db
import ( import (
"context" "context"
"database/sql"
) )
const createUser = `-- name: CreateUser :exec const createUser = `-- name: CreateUser :one
INSERT INTO users ( INSERT INTO users (
ipv4_address username,
) VALUES ( password
$1 ) VALUES ($1, $2)
) RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, social_media, created_at, updated_at
` `
func (q *Queries) CreateUser(ctx context.Context, ipv4Address string) error { type CreateUserParams struct {
_, err := q.db.ExecContext(ctx, createUser, ipv4Address) Username string `json:"username"`
Password string `json:"password"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.Password)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.Password,
&i.AvatarPicture,
&i.GoogleSignInPayload,
&i.BannedAt,
&i.BannedUntil,
&i.BanReason,
&i.IsPermaban,
&i.IsAdmin,
&i.IsCritics,
&i.IsVerified,
&i.SocialMedia,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updatePassword = `-- name: UpdatePassword :exec
UPDATE users
SET password = $1
WHERE id = $2
`
type UpdatePasswordParams struct {
Password string `json:"password"`
ID int32 `json:"id"`
}
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error {
_, err := q.db.ExecContext(ctx, updatePassword, arg.Password, arg.ID)
return err return err
} }
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET
email = COALESCE($1, email),
username = COALESCE($2, username),
avatar_picture = COALESCE($3, avatar_picture)
WHERE
id = $4
RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, social_media, created_at, updated_at
`
type UpdateUserParams struct {
Email sql.NullString `json:"email"`
Username sql.NullString `json:"username"`
AvatarPicture sql.NullString `json:"avatar_picture"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUser,
arg.Email,
arg.Username,
arg.AvatarPicture,
arg.ID,
)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.Password,
&i.AvatarPicture,
&i.GoogleSignInPayload,
&i.BannedAt,
&i.BannedUntil,
&i.BanReason,
&i.IsPermaban,
&i.IsAdmin,
&i.IsCritics,
&i.IsVerified,
&i.SocialMedia,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

1
go.mod
View File

@ -45,6 +45,7 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/mock v0.2.0 // indirect
golang.org/x/arch v0.5.0 // indirect golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/net v0.15.0 // indirect

2
go.sum
View File

@ -241,6 +241,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU=
go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=

16
import_csv.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/sh
cp ./db/csv_seeder/* /tmp/
sudo -u postgres psql \
-c '\copy users(id,username,password) FROM '"'/tmp/user.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regions(id, region_name) FROM '"'/tmp/regions.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy provinces(id, province_name, region_id) FROM '"'/tmp/provinsi.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regencies(id, province_id, regency_name) FROM '"'/tmp/kabupaten.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy locations(id,address,name,submitted_by,thumbnail,regency_id,google_maps_link) FROM '"'/tmp/locations.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-d hiling_dev &&
sudo -u postgres psql \
-c '\copy users(id,username,password) FROM '"'/tmp/user.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regions(id, region_name) FROM '"'/tmp/regions.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy provinces(id, province_name, region_id) FROM '"'/tmp/provinsi.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regencies(id, province_id, regency_name) FROM '"'/tmp/kabupaten.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy locations(id,address,name,submitted_by,thumbnail,regency_id,google_maps_link) FROM '"'/tmp/locations.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-d hiling_dev_test

11
notes
View File

@ -33,3 +33,14 @@ https://en.wikipedia.org/wiki/Provinces_of_Indonesia
########################################################################################## ##########################################################################################
######################### CUSTOM GIN VALIDATION ERR MESSAGE ##############################
make custom err message, and get all the type, the err message sucks now
https://github.com/gin-gonic/gin/issues/430 (using middleware)
tbh i'd raher use like a wrapper instead of middleware but we'll see
##########################################################################################

View File

@ -9,6 +9,7 @@ import (
type Config struct { type Config struct {
DBDriver string `mapstructure:"DB_TYPE"` DBDriver string `mapstructure:"DB_TYPE"`
DBSource string `mapstructure:"DB_SOURCE"` DBSource string `mapstructure:"DB_SOURCE"`
DBSourceTest string `mapstructure:"DB_SOURCE_TEST"`
ServerAddress string `mapstructure:"SERVER_ADDRESS"` ServerAddress string `mapstructure:"SERVER_ADDRESS"`
TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"` TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
TokenDuration time.Duration `mapstructure:"TOKEN_DURATION"` TokenDuration time.Duration `mapstructure:"TOKEN_DURATION"`

View File

@ -9,7 +9,7 @@ import (
func HashPassword(password string) (string, error) { func HashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
return "", fmt.Errorf("Failed to hash password: %w", err) return "", fmt.Errorf("failed to hash password: %w", err)
} }
return string(hashedPassword), nil return string(hashedPassword), nil
} }