From 9cd41b2f9ce919fa7763291ab76b0e5a5ea53cd4 Mon Sep 17 00:00:00 2001 From: Matt Linder Date: Sat, 20 Jul 2024 16:36:19 -0400 Subject: [PATCH] fixing user actions and adding some recipe actions --- go.mod | 45 +++----- go.sum | 102 ++++-------------- main.go | 43 +++++--- models/recipe.go | 136 ++++++++++++++++------- models/relations.go | 15 +++ models/user.go | 251 ++++++++++++++++++++++++++++++++----------- routes/auth.go | 110 +++++++++++++------ routes/middleware.go | 40 +++---- routes/recipe.go | 60 +++++++---- storage/db.go | 32 ++++++ storage/tables.sql | 39 +++++++ utils/config.go | 11 -- utils/db.go | 19 ---- utils/utils.go | 41 +++++++ 14 files changed, 603 insertions(+), 341 deletions(-) create mode 100644 models/relations.go create mode 100644 storage/db.go create mode 100644 storage/tables.sql delete mode 100644 utils/config.go delete mode 100644 utils/db.go create mode 100644 utils/utils.go diff --git a/go.mod b/go.mod index 6fca80f..fc1ec3d 100644 --- a/go.mod +++ b/go.mod @@ -1,40 +1,19 @@ -module CookingApp +module cookingapp go 1.22.1 +require github.com/joho/godotenv v1.5.1 + require ( - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/bytedance/sonic v1.11.9 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.4 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.0 // indirect - github.com/goccy/go-json v0.10.3 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect + github.com/google/uuid v1.6.0 + github.com/labstack/echo v3.3.10+incompatible + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/tursodatabase/libsql-client-go v0.0.0-20240416075003-747366ff79c4 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.25.0 + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - nhooyr.io/websocket v1.8.10 // indirect ) diff --git a/go.sum b/go.sum index ef0d12e..6933516 100644 --- a/go.sum +++ b/go.sum @@ -1,93 +1,37 @@ -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= -github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= -github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= -github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= -github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tursodatabase/libsql-client-go v0.0.0-20240416075003-747366ff79c4 h1:wNN8t3qiLLzFiETD4jL086WemAgQLfARClUx2Jfk78w= -github.com/tursodatabase/libsql-client-go v0.0.0-20240416075003-747366ff79c4/go.mod h1:2Fu26tjM011BLeR5+jwTfs6DX/fNMEWV/3CBZvggrA4= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= -nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index c3bbdf1..5c298d8 100644 --- a/main.go +++ b/main.go @@ -1,31 +1,42 @@ package main import ( - "CookingApp/routes" - "CookingApp/utils" + "cookingapp/routes" + "cookingapp/storage" + "cookingapp/utils" - "github.com/gin-gonic/gin" - _ "github.com/tursodatabase/libsql-client-go/libsql" + "github.com/labstack/echo" ) func main() { // Load .env file utils.LoadEnv() - // Create Gin router - router := gin.Default() + // Connect to database + err := storage.InitDB() + if err != nil { + panic(err) + } - // Middleware - router.Use(routes.AuthMiddleware) - router.Use(routes.DBMiddleware) + // Create router + e := echo.New() - // Define routes - router.POST("/auth/login", routes.Login) - router.POST("/auth/register", routes.Register) + // Use use auth middleware (api-key header) + e.Use(routes.ApiKeyMiddleware) - router.GET("/recipes", routes.GetUserRecipes) - // router.POST("/recipes", routes.CreateRecipe) + // Routes - // Start server - router.Run(":8080") + // Auth + auth := e.Group("/auth") + auth.POST("/login", routes.Login) + auth.POST("/register", routes.Register) + auth.GET("/token", routes.LoginWithToken) + auth.GET("/logout", routes.Logout) + + // protected + protected := e.Group("/protected") + protected.Use(routes.AuthMiddleware) + protected.Use(routes.AuthMiddleware) + + e.Logger.Fatal(e.Start(":8080")) } diff --git a/models/recipe.go b/models/recipe.go index 05b7998..bb99c78 100644 --- a/models/recipe.go +++ b/models/recipe.go @@ -1,85 +1,139 @@ package models import ( + "cookingapp/storage" "database/sql" "encoding/json" - "strings" - - "github.com/google/uuid" ) type Recipe struct { - ID uuid.UUID `json:"id"` + ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` Ingredients []Ingredient `json:"ingredients"` } -func CreateRecipe(db *sql.DB, name string, description string, ingredients []Ingredient) (Recipe, error) { - recipe := Recipe{ - ID: uuid.New(), +func newRecipe(name string, description string, ingredients []Ingredient) *Recipe { + return &Recipe{ Name: name, Description: description, Ingredients: ingredients, } +} - jsonIngredients, err := json.Marshal(recipe.Ingredients) +func getRecipesFromRows(rows *sql.Rows) ([]Recipe, error) { + + var recipes []Recipe + for rows.Next() { + var encodedRecipe EncodedRecipe + + err := rows.Scan(&encodedRecipe.ID, &encodedRecipe.Name, &encodedRecipe.Description, &encodedRecipe.Ingredients) + if err != nil { + return nil, err + } + + recipe, err := encodedRecipe.Decode() + if err != nil { + return nil, err + } + + recipes = append(recipes, *recipe) + } + + return recipes, nil +} + +// Create + +func CreateRecipeInDB(token string, name string, description string, ingredients []Ingredient) error { + + db, err := storage.GetDB() if err != nil { - return Recipe{}, err + return err } - _, err = db.Exec("INSERT INTO recipes (id, name, description, ingredients) VALUES (?, ?, ?, ?)", - recipe.ID, recipe.Name, recipe.Description, jsonIngredients) + recipe := newRecipe(name, description, ingredients) + encodedRecipe, err := recipe.Encode() if err != nil { - return Recipe{}, err + return err } - return recipe, nil + row := db.QueryRow(` + INSERT INTO recipes + (id, name, description, ingredients) + VALUES (?, ?, ?) + RETURNING id + `, encodedRecipe.Name, encodedRecipe.Description, encodedRecipe.Ingredients) + + var id int + row.Scan(&id) + + err = CreateUserRecipeRelation(db, token, id) + if err != nil { + return err + } + + return nil } -func GetUserRecipes(db *sql.DB, token string) ([]Recipe, error) { - user, err := GetUser(db, uuid.MustParse(token)) +// Read + +func ReadRecipesFromDBWithToken(token string, limit int, offset int) ([]Recipe, error) { + + db, err := storage.GetDB() if err != nil { return nil, err } - var recipeIds []string - - rows, err := db.Query("SELECT rid FROM user_recipe WHERE uid = ?", user.ID) + rows, err := db.Query(` + SELECT r.id, r.name, r.description, r.ingredients + FROM users u + JOIN user_recipes ur ON u.id = ur.user_id + JOIN recipes r ON ur.recipe_id = r.id + WHERE u.token = ? + LIMIT ? OFFSET ? + `, token, limit, offset) if err != nil { return nil, err } - defer rows.Close() + return getRecipesFromRows(rows) +} - for rows.Next() { - var recipeId string - if err := rows.Scan(&recipeId); err != nil { - return nil, err - } - recipeIds = append(recipeIds, recipeId) - } +type EncodedRecipe struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Ingredients []byte `json:"ingredients"` +} + +func (r *Recipe) Encode() (*EncodedRecipe, error) { - rows, err = db.Query("SELECT id, name, description, ingredients FROM recipes WHERE id IN (" + strings.Join(recipeIds, ",") + ")") + jsonIngredients, err := json.Marshal(r.Ingredients) if err != nil { return nil, err } - defer rows.Close() + return &EncodedRecipe{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + Ingredients: jsonIngredients, + }, nil +} + +func (r *EncodedRecipe) Decode() (*Recipe, error) { - var recipes []Recipe - for rows.Next() { - var recipe Recipe - var jsonIngredients string - if err := rows.Scan(&recipe.ID, &recipe.Name, &recipe.Description, &jsonIngredients); err != nil { - return nil, err - } - err := json.Unmarshal(([]byte)(jsonIngredients), &recipe.Ingredients) - if err != nil { - return nil, err - } - recipes = append(recipes, recipe) + var ingredients []Ingredient + err := json.Unmarshal(r.Ingredients, &ingredients) + if err != nil { + return nil, err } - return recipes, nil + return &Recipe{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + Ingredients: ingredients, + }, nil } diff --git a/models/relations.go b/models/relations.go new file mode 100644 index 0000000..dbcc191 --- /dev/null +++ b/models/relations.go @@ -0,0 +1,15 @@ +package models + +import "database/sql" + +func CreateUserRecipeRelation(db *sql.DB, token string, recipeId int) error { + + _, err := db.Exec(` + INSERT INTO user_recipe + (uid, rid) + VALUES + ((SELECT id FROM users WHERE token = ?), ?) + `, token, recipeId) + + return err +} diff --git a/models/user.go b/models/user.go index 6fba848..f4786d6 100644 --- a/models/user.go +++ b/models/user.go @@ -1,110 +1,233 @@ package models import ( + "cookingapp/storage" "database/sql" - "errors" "time" "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" ) type User struct { - ID uuid.UUID `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"-"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Token uuid.UUID `json:"token"` - TokenExpiration time.Time `json:"token_expiration"` + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + Token *string `json:"token"` + SubscriptionStart int64 `json:"subscriptionStart"` + SubscriptionEnd int64 `json:"subscriptionEnd"` } -func LoginUser(db *sql.DB, email string, password string) (User, error) { - var user User - var createdAt, updatedAt, tokenExpiration sql.NullTime - var id, token sql.NullString +// create new user (should only be used in register) +func newUser(username, email, password string) *User { + token := uuid.New().String() + + u := User{ + Username: username, + Email: email, + Password: password, + Token: &token, + SubscriptionStart: 0, + SubscriptionEnd: 0, + } + return &u +} - err := db.QueryRow("SELECT id, email, username, password, created_at, updated_at, token, token_expiration FROM users WHERE email = ? AND password = ?", email, password). - Scan(&id, &user.Email, &user.Username, &user.Password, &createdAt, &updatedAt, &token, &tokenExpiration) +// parse user from row +func userFromRow(row *sql.Row) (*User, error) { + var u User + err := row.Scan(&u.ID, &u.Username, &u.Email, &u.Password, &u.Token, &u.SubscriptionStart, &u.SubscriptionEnd) if err != nil { - if err == sql.ErrNoRows { - return User{}, errors.New("user not found") - } - return User{}, err + return nil, err } - // Parse UUID and time fields - if id.Valid { - user.ID, _ = uuid.Parse(id.String) + return &u, err +} + +func hashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + if err != nil { + return "", err } - if createdAt.Valid { - user.CreatedAt = createdAt.Time + + return string(bytes), nil +} + +func checkPasswordHash(password string, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func (u *User) GetSubscriptionStart() *time.Time { + if u.SubscriptionStart == 0 { + return nil } - if updatedAt.Valid { - user.UpdatedAt = updatedAt.Time + + t := time.Unix(u.SubscriptionStart, 0) + return &t +} + +func (u *User) GetSubscriptionEnd() *time.Time { + if u.SubscriptionEnd == 0 { + return nil } - if token.Valid { - user.Token, _ = uuid.Parse(token.String) + + t := time.Unix(u.SubscriptionEnd, 0) + return &t +} + +// CREATE + +func CreateUserWithEmailUsernameAndPassword(email string, username string, password string) (*User, error) { + + db, err := storage.GetDB() + if err != nil { + return nil, err + } + + passwordHash, err := hashPassword(password) + if err != nil { + return nil, err + } + + user := newUser(username, email, passwordHash) + + _, err = db.Exec(` + INSERT INTO users + (username, email, password, token, subscription_start, subscription_end) + VALUES (?, ?, ?, ?, ?, ?) + `, user.Username, user.Email, user.Password, user.Token, user.SubscriptionStart, user.SubscriptionEnd) + if err != nil { + return nil, err } - if tokenExpiration.Valid { - user.TokenExpiration = tokenExpiration.Time + + user, err = QueryUserByEmail(email) + if err != nil { + return nil, err } return user, nil } -func RegisterUser(db *sql.DB, email string, username string, password string) (User, error) { - user := User{ - ID: uuid.New(), - Email: email, - Username: username, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Token: uuid.New(), - TokenExpiration: time.Now().Add(24 * time.Hour), +// READ + +func QueryUserByEmail(email string) (*User, error) { + + db, err := storage.GetDB() + if err != nil { + return nil, err } - _, err := db.Exec("INSERT INTO users (id, email, username, password, created_at, updated_at, token, token_expiration) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - user.ID, user.Email, user.Username, user.Password, user.CreatedAt, user.UpdatedAt, user.Token, user.TokenExpiration) + row := db.QueryRow(` + SELECT + id, username, email, password, token, subscription_start, subscription_end + FROM users + WHERE email = ? + `, email) + + return userFromRow(row) +} + +func QueryUserByToken(token string) (*User, error) { + + db, err := storage.GetDB() + if err != nil { + return nil, err + } + + row := db.QueryRow(` + SELECT + id, username, email, password, token, subscription_start, subscription_end + FROM users + WHERE token = ? + `, token) + + return userFromRow(row) +} + +func QueryUserByEmailAndPassword(email string, password string) (*User, error) { + + db, err := storage.GetDB() + if err != nil { + return nil, err + } + + row := db.QueryRow(` + SELECT + id, username, email, password, token, subscription_start, subscription_end + FROM users + WHERE email = ? + `, email) + + user, err := userFromRow(row) if err != nil { - return User{}, err + return nil, err + } + + if !checkPasswordHash(password, user.Password) { + return nil, err } return user, nil } -func GetUser(db *sql.DB, token uuid.UUID) (User, error) { - var user User - var createdAt, updatedAt, tokenExpiration sql.NullTime - var id, tokenString sql.NullString +// UPDATE - err := db.QueryRow("SELECT id, email, username, created_at, updated_at, token, token_expiration FROM users WHERE token = ?", token). - Scan(&id, &user.Email, &user.Username, &createdAt, &updatedAt, &tokenString, &tokenExpiration) +func UpdateUser(user *User) error { + db, err := storage.GetDB() if err != nil { - if err == sql.ErrNoRows { - return User{}, errors.New("user not found") - } - return User{}, err + return err } - // Parse UUID and time fields - if id.Valid { - user.ID, _ = uuid.Parse(id.String) + _, err = db.Exec(` + UPDATE users + SET + username = ?, + email = ?, + password = ?, + token = ?, + subscription_start = ?, + subscription_end = ? + WHERE id = ? + `, user.Username, user.Email, user.Password, user.Token, user.SubscriptionStart, user.SubscriptionEnd, user.ID) + if err != nil { + return err } - if createdAt.Valid { - user.CreatedAt = createdAt.Time + + return nil +} + +func ClearToken(token string) error { + + db, err := storage.GetDB() + if err != nil { + return err } - if updatedAt.Valid { - user.UpdatedAt = updatedAt.Time + + _, err = db.Exec("UPDATE users SET token = NULL WHERE token = ?", token) + if err != nil { + return err } - if tokenString.Valid { - user.Token, _ = uuid.Parse(tokenString.String) + + return nil +} + +// DELETE + +func DeleteUserFromDB(id int) error { + + db, err := storage.GetDB() + if err != nil { + return err } - if tokenExpiration.Valid { - user.TokenExpiration = tokenExpiration.Time + + _, err = db.Exec("DELETE FROM users WHERE id = ?", id) + if err != nil { + return err } - return user, nil + return nil } diff --git a/routes/auth.go b/routes/auth.go index c5cde6b..c918cd9 100644 --- a/routes/auth.go +++ b/routes/auth.go @@ -1,71 +1,111 @@ package routes import ( - "CookingApp/models" - "database/sql" + "cookingapp/models" "net/http" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" ) type responseBody struct { - ID string `json:"id"` + ID int `json:"id"` Username string `json:"username"` Email string `json:"email"` Token string `json:"token"` } -type loginBody struct { - Email string `json:"email"` - Password string `json:"password"` +type loginRequest struct { + Email string `body:"email"` + Password string `body:"email"` } -func Login(c *gin.Context) { - db := c.MustGet("db").(*sql.DB) +func Login(e echo.Context) error { - var body loginBody - if err := c.BindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + var request loginRequest + if err := e.Bind(&request); err != nil { + return err } - if body.Email == "" || body.Password == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Email and password are required"}) - return + if request.Email == "" || request.Password == "" { + return e.JSON(http.StatusBadRequest, "Email and password are required") } - user, err := models.LoginUser(db, body.Email, body.Password) + user, err := models.QueryUserByEmailAndPassword(request.Password, request.Password) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return e.JSON(http.StatusBadRequest, "Invalid email or password") } - c.JSON(http.StatusOK, responseBody{user.ID.String(), user.Username, user.Email, user.Token.String()}) + return e.JSON(http.StatusOK, responseBody{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Token: *user.Token, + }) } -type registerBody struct { - Email string `json:"email"` - Username string `json:"username"` - Password string `json:"password"` +type registerRequest struct { + Email string `body:"email"` + Username string `body:"username"` + Password string `body:"password"` } -func Register(c *gin.Context) { - db := c.MustGet("db").(*sql.DB) +func Register(e echo.Context) error { + + var request registerRequest + + if err := e.Bind(&request); err != nil { + return err + } - var body registerBody - if err := c.BindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if request.Email == "" || request.Username == "" || request.Password == "" { + return e.JSON(http.StatusBadRequest, "Email, username and password are required") } - if body.Email == "" || body.Username == "" || body.Password == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Email, username and password are required"}) - return + user, err := models.CreateUserWithEmailUsernameAndPassword(request.Email, request.Username, request.Password) + if err != nil { + return e.JSON(http.StatusBadRequest, "Error creating user") + } + + return e.JSON(http.StatusOK, responseBody{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Token: *user.Token, + }) +} + +func LoginWithToken(e echo.Context) error { + + token := e.Request().Header.Get("Authorization") + + if token == "" { + return e.JSON(http.StatusBadRequest, "Token is required") + } + + user, err := models.QueryUserByToken(token) + if err != nil { + return e.JSON(http.StatusBadRequest, "Invalid token") + } + + return e.JSON(http.StatusOK, responseBody{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Token: *user.Token, + }) +} + +func Logout(e echo.Context) error { + + token := e.Request().Header.Get("Authorization") + if token == "" { + return e.JSON(http.StatusBadRequest, "Token is required") } - user, err := models.RegisterUser(db, body.Email, body.Username, body.Password) + err := models.ClearToken(token) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + return e.JSON(http.StatusBadRequest, "Invalid token") } - c.JSON(http.StatusCreated, responseBody{user.ID.String(), user.Username, user.Email, user.Token.String()}) + return e.JSON(http.StatusOK, "Logged out") } diff --git a/routes/middleware.go b/routes/middleware.go index 728e57d..d761114 100644 --- a/routes/middleware.go +++ b/routes/middleware.go @@ -1,33 +1,33 @@ package routes import ( - "CookingApp/utils" "net/http" "os" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" ) -func DBMiddleware(c *gin.Context) { - db, err := utils.DBConnect() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - c.Abort() - return +// Used for entire backend to protect from unauthorized access +func ApiKeyMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + envKey := os.Getenv("API_KEY") + key := c.Request().Header.Get("api-key") + if key != envKey { + return c.JSON(http.StatusBadRequest, "Unauthorized") + } + + return next(c) } - c.Set("db", db) - c.Next() } -func AuthMiddleware(c *gin.Context) { - apiToken := os.Getenv("API_KEY") - authToken := c.GetHeader("Authorization") - if authToken != apiToken { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return +// Used for protected routes where user's token is requried +func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + token := c.Request().Header.Get("Authorization") + if token == "" { + return c.JSON(http.StatusBadRequest, "Token is required") + } + + return next(c) } - token := c.GetHeader("Token") - c.Set("token", token) - c.Next() } diff --git a/routes/recipe.go b/routes/recipe.go index 91e194c..3bca76f 100644 --- a/routes/recipe.go +++ b/routes/recipe.go @@ -1,43 +1,57 @@ package routes import ( - "CookingApp/models" - "database/sql" + "cookingapp/models" "net/http" - "github.com/gin-gonic/gin" + "github.com/labstack/echo" ) -type createRecipeBody struct { - Name string `json:"name"` - Description string `json:"description"` - Ingredients []models.Ingredient `json:"ingredients"` +type getRecipesRequest struct { + Limit int `query:"limit"` + Offset int `query:"offset"` } -func CreateRecipe(c *gin.Context) { - db := c.MustGet("db").(*sql.DB) +func GetRecipes(e echo.Context) error { + + var request getRecipesRequest + if err := e.Bind(&request); err != nil { + e.String(http.StatusBadRequest, err.Error()) + } - var body createRecipeBody - if err := c.BindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if request.Limit == 0 || request.Limit > 30 { + request.Limit = 10 } - recipe, err := models.CreateRecipe(db, body.Name, body.Description, body.Ingredients) + token := e.Request().Header.Get("Authorization") + + recipes, err := models.ReadRecipesFromDBWithToken(token, request.Limit, request.Offset) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + e.String(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusCreated, recipe) + return e.JSON(http.StatusOK, recipes) +} + +type createRecipeRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Ingredients []models.Ingredient `json:"ingredients"` } -func GetUserRecipes(c *gin.Context) { - db := c.MustGet("db").(*sql.DB) - token := c.MustGet("token").(string) - recipes, err := models.GetUserRecipes(db, token) +func CreateRecipe(e echo.Context) error { + + var request createRecipeRequest + if err := e.Bind(&request); err != nil { + e.String(http.StatusBadRequest, err.Error()) + } + + token := e.Request().Header.Get("Authorization") + + err := models.CreateRecipeInDB(token, request.Name, request.Description, request.Ingredients) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + e.String(http.StatusInternalServerError, err.Error()) } - c.JSON(http.StatusOK, recipes) + + return e.String(http.StatusCreated, "Recipe created") } diff --git a/storage/db.go b/storage/db.go new file mode 100644 index 0000000..c88ac07 --- /dev/null +++ b/storage/db.go @@ -0,0 +1,32 @@ +package storage + +import ( + "database/sql" + "fmt" + "os" +) + +var db *sql.DB + +func InitDB() error { + dbUrl := os.Getenv("DB_URL") + dbToken := os.Getenv("DB_TOKEN") + + var err error + db, err = sql.Open("libsql", fmt.Sprintf("%s?authToken=%s", dbUrl, dbToken)) + if err != nil { + return err + } + + return nil +} + +func GetDB() (*sql.DB, error) { + if db == nil { + err := InitDB() + if err != nil { + return nil, err + } + } + return db, nil +} diff --git a/storage/tables.sql b/storage/tables.sql new file mode 100644 index 0000000..49a97ab --- /dev/null +++ b/storage/tables.sql @@ -0,0 +1,39 @@ +-- ==================================================== +-- TABLES +-- ==================================================== + +-- Users +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + username TEXT NOT NULL, + password TEXT NOT NULL, + token TEXT, + subscription_start BIGINT, + subscription_end BIGINT +) + +CREATE INDEX IF NOT EXISTS user_email_index ON users(email) + +CREATE INDEX IF NOT EXISTS user_token_index ON users(token) + +-- Recipes +CREATE TABLE IF NOT EXISTS recipes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT NOT NULL, + ingredients BLOB +) + +-- ==================================================== +-- RELATIONS +-- ==================================================== + +-- UserRecipe +CREATE TABLE IF NOT EXISTS user_recipe ( + user_id INTEGER NOT NULL, + recipe_id INTEGER NOT NULL, + PRIMARY KEY (user_id, recipe_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (recipe_id) REFERENCES recipes(id) +) \ No newline at end of file diff --git a/utils/config.go b/utils/config.go deleted file mode 100644 index d56a5eb..0000000 --- a/utils/config.go +++ /dev/null @@ -1,11 +0,0 @@ -package utils - -import "github.com/joho/godotenv" - -func LoadEnv() error { - err := godotenv.Load(".env") - if err != nil { - return err - } - return nil -} diff --git a/utils/db.go b/utils/db.go deleted file mode 100644 index 69cec36..0000000 --- a/utils/db.go +++ /dev/null @@ -1,19 +0,0 @@ -package utils - -import ( - "database/sql" - "fmt" - "os" -) - -func DBConnect() (*sql.DB, error) { - dbUrl := os.Getenv("DB_URL") - dbToken := os.Getenv("DB_TOKEN") - - db, err := sql.Open("libsql", fmt.Sprintf("%s?authToken=%s", dbUrl, dbToken)) - if err != nil { - return nil, err - } - - return db, nil -} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..4cfcf19 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,41 @@ +package utils + +import ( + "encoding/json" + + "github.com/joho/godotenv" +) + +func LoadEnv() error { + err := godotenv.Load(".env") + if err != nil { + return err + } + return nil +} + +func Decode[T any](bytes []byte) (T, error) { + var decoded T + err := json.Unmarshal(bytes, decoded) + if err != nil { + return decoded, err + } + return decoded, nil +} + +type Optional[T any] struct { + Value T + isPresent bool +} + +func (o Optional[T]) IsPresent() bool { + return o.isPresent +} + +func (o Optional[T]) IsEmpty() bool { + return !o.isPresent +} + +func (o Optional[T]) Get() T { + return o.Value +}