Compare commits
6 Commits
ed85ece58a
...
dfe00f8359
Author | SHA1 | Date | |
---|---|---|---|
dfe00f8359 | |||
dcacc9288d | |||
d1ba0251c0 | |||
4f962bb104 | |||
96dfa4ae29 | |||
ed608f170a |
7
Makefile
7
Makefile
@ -2,12 +2,17 @@ include dev.env
|
||||
|
||||
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}_test?sslmode=disable" -verbose up
|
||||
|
||||
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}_test?sslmode=disable" -verbose down $N
|
||||
|
||||
seed:
|
||||
./import_csv.sh
|
||||
|
||||
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 generate && make mock-generate
|
||||
|
@ -2,14 +2,14 @@ package api
|
||||
|
||||
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{
|
||||
"error": err.Error(),
|
||||
"message": msg,
|
||||
}
|
||||
}
|
||||
|
||||
func validResponse(data interface{}, msg string) gin.H {
|
||||
func ValidResponse(data interface{}, msg string) gin.H {
|
||||
return gin.H{
|
||||
"message": msg,
|
||||
"data": data,
|
||||
|
@ -13,7 +13,7 @@ type Server struct {
|
||||
config util.Config
|
||||
store db.Store
|
||||
tokenMaker token.Maker
|
||||
router *gin.Engine
|
||||
Router *gin.Engine
|
||||
}
|
||||
|
||||
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() {
|
||||
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 {
|
||||
return server.router.Run(address)
|
||||
return server.Router.Run(address)
|
||||
}
|
||||
|
32
api/test/main_test.go
Normal file
32
api/test/main_test.go
Normal 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
60
api/test/user_test.go
Normal 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
|
||||
}
|
95
api/user.go
95
api/user.go
@ -1,27 +1,106 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/go-playground/validator/v10"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type CreateUserRequest struct {
|
||||
ClientIpV4 string `json:"client_ip" binding:"required"`
|
||||
type createUserRequest struct {
|
||||
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) {
|
||||
var req CreateUserRequest
|
||||
var req createUserRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
|
||||
return
|
||||
if err != nil {
|
||||
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 {
|
||||
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
|
||||
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
|
||||
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)
|
||||
}
|
||||
|
10
db/csv_seeder/locations.csv
Normal file
10
db/csv_seeder/locations.csv
Normal file
@ -0,0 +1,10 @@
|
||||
id#address#name#submitted_by#thumbnail#regency_id#google_maps_link
|
||||
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
|
||||
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
|
|
2
db/csv_seeder/user.csv
Normal file
2
db/csv_seeder/user.csv
Normal file
@ -0,0 +1,2 @@
|
||||
id,username,password
|
||||
1,user123,password
|
|
@ -1,4 +1,7 @@
|
||||
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 locations;
|
||||
DROP TABLE IF EXISTS regencies;
|
||||
@ -6,3 +9,7 @@ DROP TABLE IF EXISTS provinces;
|
||||
DROP TABLE IF EXISTS regions;
|
||||
DROP TABLE IF EXISTS tags;
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
|
||||
DROP TYPE IF EXISTS user_reports_type;
|
||||
DROP TYPE IF EXISTS comment_type;
|
@ -1,20 +1,54 @@
|
||||
CREATE TABLE users(
|
||||
"id" serial primary key not null,
|
||||
"email" varchar unique,
|
||||
"username" varchar unique,
|
||||
"password" varchar,
|
||||
"avatar_picture" varchar,
|
||||
"google_sign_in_payload" varchar,
|
||||
"banned_at" timestamp,
|
||||
"banned_until" timestamp,
|
||||
"is_admin" boolean,
|
||||
"is_critics" boolean,
|
||||
"is_verified" boolean,
|
||||
"ipv4_address" varchar(15) not null,
|
||||
"social_media" jsonb,
|
||||
"created_at" timestamp default(now()),
|
||||
"updated_at" timestamp default(now())
|
||||
);
|
||||
CREATE TABLE users(
|
||||
"id" serial primary key not null,
|
||||
"email" varchar unique,
|
||||
"username" varchar unique not null,
|
||||
"password" varchar not null,
|
||||
"avatar_picture" varchar,
|
||||
"google_sign_in_payload" varchar,
|
||||
"banned_at" timestamp,
|
||||
"banned_until" timestamp,
|
||||
"ban_reason" varchar,
|
||||
"is_permaban" boolean,
|
||||
"is_admin" boolean,
|
||||
"is_critics" boolean,
|
||||
"is_verified" boolean,
|
||||
"social_media" jsonb,
|
||||
"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(
|
||||
"id" serial primary key not null,
|
||||
@ -41,12 +75,12 @@ CREATE TABLE regencies(
|
||||
|
||||
CREATE TABLE locations(
|
||||
"id" serial primary key not null,
|
||||
"address" varchar,
|
||||
"name" varchar,
|
||||
"address" varchar not null,
|
||||
"name" varchar not null,
|
||||
"google_maps_link" varchar,
|
||||
"submitted_by" integer references "users"("id") not null,
|
||||
"total_visited" integer,
|
||||
"thumbnail" varchar,
|
||||
"thumbnail" varchar not null,
|
||||
"regency_id" smallint references "regencies"("id") not null,
|
||||
"is_deleted" boolean,
|
||||
"created_at" timestamp default(now()),
|
||||
@ -80,5 +114,21 @@ CREATE TABLE reviews (
|
||||
"location_id" integer references "locations"("id") not null,
|
||||
"created_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
80
db/mock/store.go
Normal 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)
|
||||
}
|
@ -1,6 +1,22 @@
|
||||
-- name: CreateUser :exec
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
ipv4_address
|
||||
) VALUES (
|
||||
$1
|
||||
);
|
||||
username,
|
||||
password
|
||||
) 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;
|
||||
|
@ -6,10 +6,124 @@ package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
ID int32 `json:"id"`
|
||||
Address sql.NullString `json:"address"`
|
||||
@ -79,17 +193,29 @@ type Tag struct {
|
||||
type User struct {
|
||||
ID int32 `json:"id"`
|
||||
Email sql.NullString `json:"email"`
|
||||
Username sql.NullString `json:"username"`
|
||||
Password sql.NullString `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
AvatarPicture sql.NullString `json:"avatar_picture"`
|
||||
GoogleSignInPayload sql.NullString `json:"google_sign_in_payload"`
|
||||
BannedAt sql.NullTime `json:"banned_at"`
|
||||
BannedUntil sql.NullTime `json:"banned_until"`
|
||||
BanReason sql.NullString `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"`
|
||||
Ipv4Address string `json:"ipv4_address"`
|
||||
SocialMedia pqtype.NullRawMessage `json:"social_media"`
|
||||
CreatedAt sql.NullTime `json:"created_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"`
|
||||
}
|
||||
|
@ -9,7 +9,9 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
|
32
db/sqlc/test/main_test.go
Normal file
32
db/sqlc/test/main_test.go
Normal 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())
|
||||
}
|
23
db/sqlc/test/users_test.go
Normal file
23
db/sqlc/test/users_test.go
Normal 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)
|
||||
}
|
@ -7,17 +7,105 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :exec
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
ipv4_address
|
||||
) VALUES (
|
||||
$1
|
||||
)
|
||||
username,
|
||||
password
|
||||
) 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 {
|
||||
_, err := q.db.ExecContext(ctx, createUser, ipv4Address)
|
||||
type CreateUserParams struct {
|
||||
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
|
||||
}
|
||||
|
||||
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
1
go.mod
@ -45,6 +45,7 @@ require (
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // 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/crypto v0.13.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
|
2
go.sum
2
go.sum
@ -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.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
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.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
|
16
import_csv.sh
Executable file
16
import_csv.sh
Executable 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
11
notes
@ -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
|
||||
|
||||
|
||||
##########################################################################################
|
@ -9,6 +9,7 @@ import (
|
||||
type Config struct {
|
||||
DBDriver string `mapstructure:"DB_TYPE"`
|
||||
DBSource string `mapstructure:"DB_SOURCE"`
|
||||
DBSourceTest string `mapstructure:"DB_SOURCE_TEST"`
|
||||
ServerAddress string `mapstructure:"SERVER_ADDRESS"`
|
||||
TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
|
||||
TokenDuration time.Duration `mapstructure:"TOKEN_DURATION"`
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
func HashPassword(password string) (string, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to hash password: %w", err)
|
||||
return "", fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user