From b280fed204cc7eb5ee3f0dfac5aebb93ea907143 Mon Sep 17 00:00:00 2001 From: "sh.kairatuly" Date: Tue, 30 Jul 2024 18:24:18 +0500 Subject: [PATCH 1/8] Kairatuly Shynggyskhan exercises --- exercise1/problem1/main.go | 9 ++++++++- exercise1/problem10/main.go | 18 ++++++++++++++++- exercise1/problem2/main.go | 40 ++++++++++++++++++++++++++++++++++++- exercise1/problem3/main.go | 4 +++- exercise1/problem4/main.go | 10 +++++++++- exercise1/problem5/main.go | 28 +++++++++++++++++++++++++- exercise1/problem6/main.go | 31 +++++++++++++++++++++++++++- exercise1/problem7/main.go | 17 +++++++++++++++- exercise1/problem8/main.go | 11 +++++++++- exercise1/problem9/main.go | 12 ++++++++--- 10 files changed, 168 insertions(+), 12 deletions(-) diff --git a/exercise1/problem1/main.go b/exercise1/problem1/main.go index dfca465c..430c6f4c 100644 --- a/exercise1/problem1/main.go +++ b/exercise1/problem1/main.go @@ -1,3 +1,10 @@ package main -func addUp() {} +func addUp(i int) int { + var sum int = 0 + for j := 1; j <= i; j++ { + sum += int(j) + } + + return sum +} diff --git a/exercise1/problem10/main.go b/exercise1/problem10/main.go index 04ec3430..1330e06b 100644 --- a/exercise1/problem10/main.go +++ b/exercise1/problem10/main.go @@ -1,3 +1,19 @@ package main -func sum() {} +import ( + "fmt" + "strconv" +) + +func sum(str1 string, str2 string) (string, error) { + val1, err := strconv.Atoi(string(str1)) + val2, err2 := strconv.Atoi(string(str2)) + + if err != nil { + return "", err + } else if err2 != nil { + return "", err2 + } + + return fmt.Sprintf("%d", val1+val2), nil +} diff --git a/exercise1/problem2/main.go b/exercise1/problem2/main.go index 2ca540b8..2e8b593d 100644 --- a/exercise1/problem2/main.go +++ b/exercise1/problem2/main.go @@ -1,3 +1,41 @@ package main -func binary() {} +import ( + "fmt" + "strconv" +) + +func binary(num int) string { + way := 2 + var result string + + switch way { + case 1: + result = ConvertInt(num, 2) + case 2: + result = convert(num) + } + + return result +} + +func convert(num int) string { + n := 1 + + for n*2 <= num { + n = n * 2 + } + + res := "" + for n >= 1 { + res = res + fmt.Sprintf("%d", num/n) + num = num % n + n = n / 2 + } + + return res +} + +func ConvertInt(val int, toBase int) string { + return strconv.FormatInt(int64(val), toBase) +} diff --git a/exercise1/problem3/main.go b/exercise1/problem3/main.go index d346641a..c5997789 100644 --- a/exercise1/problem3/main.go +++ b/exercise1/problem3/main.go @@ -1,3 +1,5 @@ package main -func numberSquares() {} +func numberSquares(n int) int { + return (n * (n + 1) * (2*n + 1)) / 6 +} diff --git a/exercise1/problem4/main.go b/exercise1/problem4/main.go index 74af9044..08d4012d 100644 --- a/exercise1/problem4/main.go +++ b/exercise1/problem4/main.go @@ -1,3 +1,11 @@ package main -func detectWord() {} +func detectWord(str string) string { + var result string + for _, letter := range str { + if letter <= 122 && letter >= 97 { + result = result + string(letter) + } + } + return result +} diff --git a/exercise1/problem5/main.go b/exercise1/problem5/main.go index c5a804c9..248e1b1e 100644 --- a/exercise1/problem5/main.go +++ b/exercise1/problem5/main.go @@ -1,3 +1,29 @@ package main -func potatoes() {} +import "strings" + +func potatoes(text string) int { + way := 2 + var res int + switch way { + case 1: + res = strings.Count(text, "potato") + case 2: + res = way2(text) + } + + return res +} + +func way2(text string) int { + substr := "potato" + substrLen := len(substr) + count := 0 + + for i := 0; i <= len(text)-substrLen; i++ { + if text[i:i+substrLen] == substr { + count++ + } + } + return count +} diff --git a/exercise1/problem6/main.go b/exercise1/problem6/main.go index 06043890..9da1e687 100644 --- a/exercise1/problem6/main.go +++ b/exercise1/problem6/main.go @@ -1,3 +1,32 @@ package main -func emojify() {} +func emojify(sentence string) string { + wordToEmoji := map[string]string{ + "smile": "🙂", + "grin": "😀", + "sad": "😥", + "mad": "😠", + } + + words := []rune(sentence) + emojiSentence := "" + + i := 0 + for i < len(words) { + match := false + for word, emoji := range wordToEmoji { + if i+len(word) <= len(words) && sentence[i:i+len(word)] == word { + emojiSentence += emoji + i += len(word) + match = true + break + } + } + if !match { + emojiSentence += string(words[i]) + i++ + } + } + + return emojiSentence +} diff --git a/exercise1/problem7/main.go b/exercise1/problem7/main.go index 57c99b5c..6821091c 100644 --- a/exercise1/problem7/main.go +++ b/exercise1/problem7/main.go @@ -1,3 +1,18 @@ package main -func highestDigit() {} +import ( + "fmt" + "strconv" +) + +func highestDigit(num int) int { + highest := 0 + strnum := fmt.Sprintf("%d", num) + for _, val := range strnum { + newVal, _ := strconv.Atoi(string(val)) + if newVal > highest { + highest = int(newVal) + } + } + return highest +} diff --git a/exercise1/problem8/main.go b/exercise1/problem8/main.go index 97fa0dae..e0976c0b 100644 --- a/exercise1/problem8/main.go +++ b/exercise1/problem8/main.go @@ -1,3 +1,12 @@ package main -func countVowels() {} +func countVowels(str string) int { + var count int + for _, ch := range str { + if ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u' { + count++ + } + } + + return count +} diff --git a/exercise1/problem9/main.go b/exercise1/problem9/main.go index e8c84a54..c6e38248 100644 --- a/exercise1/problem9/main.go +++ b/exercise1/problem9/main.go @@ -1,7 +1,13 @@ package main -func bitwiseAND() {} +func bitwiseAND(val1 int, val2 int) int { + return val1 & val2 +} -func bitwiseOR() {} +func bitwiseOR(val1 int, val2 int) int { + return val1 | val2 +} -func bitwiseXOR() {} +func bitwiseXOR(val1 int, val2 int) int { + return val1 ^ val2 +} From a3c1f9544c944e048c37a24cc2958334f8966fa5 Mon Sep 17 00:00:00 2001 From: "sh.kairatuly" Date: Wed, 25 Sep 2024 22:24:54 +0500 Subject: [PATCH 2/8] Shynggyskhan.Kairatuly exercise 2 --- exercise2/problem1/problem1.go | 10 ++++++++- exercise2/problem10/problem10.go | 16 +++++++++++++- exercise2/problem11/problem11.go | 13 ++++++++++- exercise2/problem12/problem12.go | 25 ++++++++++++++++++++- exercise2/problem2/problem2.go | 16 +++++++++++++- exercise2/problem3/problem3.go | 37 +++++++++++++++++++++++++++++++- exercise2/problem4/problem4.go | 11 +++++++++- exercise2/problem5/problem5.go | 22 ++++++++++++++++++- exercise2/problem6/problem6.go | 13 ++++++++++- exercise2/problem7/problem7.go | 3 ++- exercise2/problem8/problem8.go | 8 +++---- exercise2/problem9/problem9.go | 10 ++++++++- 12 files changed, 169 insertions(+), 15 deletions(-) diff --git a/exercise2/problem1/problem1.go b/exercise2/problem1/problem1.go index 4763006c..bbb3e994 100644 --- a/exercise2/problem1/problem1.go +++ b/exercise2/problem1/problem1.go @@ -1,4 +1,12 @@ package problem1 -func isChangeEnough() { +func isChangeEnough(change [4]int, total float32) bool { + quarters := float32(change[0]) * 0.25 + dimes := float32(change[1]) * 0.10 + nickels := float32(change[2]) * 0.05 + pennies := float32(change[3]) * 0.01 + + totalChange := quarters + dimes + nickels + pennies + + return totalChange >= total } diff --git a/exercise2/problem10/problem10.go b/exercise2/problem10/problem10.go index 7142a022..01392330 100644 --- a/exercise2/problem10/problem10.go +++ b/exercise2/problem10/problem10.go @@ -1,3 +1,17 @@ package problem10 -func factory() {} +func factory() (map[string]int, func(brand string) func(int)) { + brands := make(map[string]int) + + makeBrand := func(brand string) func(int) { + if _, exists := brands[brand]; !exists { + brands[brand] = 0 + } + + return func(count int) { + brands[brand] += count + } + } + + return brands, makeBrand +} diff --git a/exercise2/problem11/problem11.go b/exercise2/problem11/problem11.go index 33988711..117f45f3 100644 --- a/exercise2/problem11/problem11.go +++ b/exercise2/problem11/problem11.go @@ -1,3 +1,14 @@ package problem11 -func removeDups() {} +func removeDups[T int | bool | string](items []T) []T { + var result []T + var check = make(map[T]bool) + for _, item := range items { + if !check[item] { + result = append(result, item) + check[item] = true + } + } + + return result +} diff --git a/exercise2/problem12/problem12.go b/exercise2/problem12/problem12.go index 4c1ae327..b3f0611e 100644 --- a/exercise2/problem12/problem12.go +++ b/exercise2/problem12/problem12.go @@ -1,3 +1,26 @@ package problem11 -func keysAndValues() {} +import ( + "fmt" + "sort" +) + +func keysAndValues[K comparable, V any](m map[K]V) ([]K, []V) { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + + // Sort keys + sort.Slice(keys, func(i, j int) bool { + return fmt.Sprintf("%v", keys[i]) < fmt.Sprintf("%v", keys[j]) + }) + + sortedValues := make([]V, len(keys)) + fmt.Println(m) + for i, k := range keys { + sortedValues[i] = m[k] + } + + return keys, sortedValues +} diff --git a/exercise2/problem2/problem2.go b/exercise2/problem2/problem2.go index fdb199f0..ca71c994 100644 --- a/exercise2/problem2/problem2.go +++ b/exercise2/problem2/problem2.go @@ -1,4 +1,18 @@ package problem2 -func capitalize() { +import "unicode" + +func capitalize(names []string) []string { + for i, name := range names { + if len(name) > 0 { + runes := []rune(name) + for j := range runes { + if j == 0 { + runes[j] = unicode.ToUpper(runes[j]) + } + } + names[i] = string(runes) + } + } + return names } diff --git a/exercise2/problem3/problem3.go b/exercise2/problem3/problem3.go index f183fafb..48c41b4a 100644 --- a/exercise2/problem3/problem3.go +++ b/exercise2/problem3/problem3.go @@ -9,5 +9,40 @@ const ( lr dir = "lr" ) -func diagonalize() { +func diagonalize(n int, direction dir) [][]int { + matrix := make([][]int, n) + for i := range matrix { + matrix[i] = make([]int, n) + } + + if direction == "ul" { + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + matrix[i][j] = i + j + } + } + } + if direction == "ur" { + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + matrix[i][j] = (n - 1 - j) + i + } + } + } + if direction == "ll" { + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + matrix[i][j] = (n - 1 - i) + j + } + } + } + if direction == "lr" { + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + matrix[i][j] = (n - 1 - i) + (n - 1 - j) + } + } + } + + return matrix } diff --git a/exercise2/problem4/problem4.go b/exercise2/problem4/problem4.go index 1f680a4d..df66a8a4 100644 --- a/exercise2/problem4/problem4.go +++ b/exercise2/problem4/problem4.go @@ -1,4 +1,13 @@ package problem4 -func mapping() { +import "strings" + +func mapping(inpt []string) map[string]string { + result := make(map[string]string) + + for _, in := range inpt { + result[in] = strings.ToUpper(in) + } + + return result } diff --git a/exercise2/problem5/problem5.go b/exercise2/problem5/problem5.go index 43fb96a4..3ea6c9fc 100644 --- a/exercise2/problem5/problem5.go +++ b/exercise2/problem5/problem5.go @@ -1,4 +1,24 @@ package problem5 -func products() { +import ( + "sort" +) + +func products(catalog map[string]int, minPrice int) []string { + var result []string + + for name, price := range catalog { + if price >= minPrice { + result = append(result, name) + } + } + + sort.Slice(result, func(i, j int) bool { + if catalog[result[i]] == catalog[result[j]] { + return result[i] < result[j] + } + return catalog[result[i]] > catalog[result[j]] + }) + + return result } diff --git a/exercise2/problem6/problem6.go b/exercise2/problem6/problem6.go index 89fc5bfe..54ff4568 100644 --- a/exercise2/problem6/problem6.go +++ b/exercise2/problem6/problem6.go @@ -1,4 +1,15 @@ package problem6 -func sumOfTwo() { +func sumOfTwo(a []int, b []int, sum int) bool { + result := false + + for i := 0; i < len(a); i++ { + for j := 0; j < len(b); j++ { + if a[i]+b[j] == sum { + result = true + } + } + } + + return result } diff --git a/exercise2/problem7/problem7.go b/exercise2/problem7/problem7.go index 32514209..48895f5d 100644 --- a/exercise2/problem7/problem7.go +++ b/exercise2/problem7/problem7.go @@ -1,4 +1,5 @@ package problem7 -func swap() { +func swap(x *int, y *int) { + *x, *y = *y, *x } diff --git a/exercise2/problem8/problem8.go b/exercise2/problem8/problem8.go index 9389d3b0..8c8eb869 100644 --- a/exercise2/problem8/problem8.go +++ b/exercise2/problem8/problem8.go @@ -4,13 +4,13 @@ func simplify(list []string) map[string]int { var indMap map[string]int indMap = make(map[string]int) - load(&indMap, &list) + load(indMap, list) return indMap } -func load(m *map[string]int, students *[]string) { - for i, name := range *students { - (*m)[name] = i +func load(m map[string]int, students []string) { + for i, name := range students { + m[name] = i } } diff --git a/exercise2/problem9/problem9.go b/exercise2/problem9/problem9.go index fc96d21a..66c8746b 100644 --- a/exercise2/problem9/problem9.go +++ b/exercise2/problem9/problem9.go @@ -1,3 +1,11 @@ package problem9 -func factory() {} +func factory(multiple int) func(...int) []int { + return func(nums ...int) []int { + result := make([]int, len(nums)) + for i, num := range nums { + result[i] = num * multiple + } + return result + } +} From acfbcaf207d383d298c6fcae025b8ede0762c78c Mon Sep 17 00:00:00 2001 From: "sh.kairatuly" Date: Sun, 6 Oct 2024 21:18:41 +0500 Subject: [PATCH 3/8] Shynggyskhan.Kairatuly exercise 3 --- exercise3/problem1/problem1.go | 34 +++++++++++- exercise3/problem2/problem2.go | 34 +++++++++++- exercise3/problem3/problem3.go | 98 +++++++++++++++++++++++++++++++++- exercise3/problem4/problem4.go | 96 ++++++++++++++++++++++++++++++++- exercise3/problem5/problem5.go | 17 +++++- exercise3/problem6/problem6.go | 30 +++++++++-- exercise3/problem7/problem7.go | 43 +++++++++++++++ 7 files changed, 344 insertions(+), 8 deletions(-) diff --git a/exercise3/problem1/problem1.go b/exercise3/problem1/problem1.go index d45605c6..2ed54d39 100644 --- a/exercise3/problem1/problem1.go +++ b/exercise3/problem1/problem1.go @@ -1,3 +1,35 @@ package problem1 -type Queue struct{} +import "errors" + +type Queue struct { + queue []any +} + +func (q *Queue) Enqueue(val any) { + q.queue = append(q.queue, val) +} + +func (q *Queue) Dequeue() (any, error) { + if len(q.queue) == 0 { + return nil, errors.New("queue is empty") + } + item := q.queue[0] + q.queue = q.queue[1:] + return item, nil +} + +func (q *Queue) Peek() (any, error) { + if len(q.queue) == 0 { + return nil, errors.New("queue is empty") + } + return q.queue[0], nil +} + +func (q *Queue) Size() int { + return len(q.queue) +} + +func (q *Queue) IsEmpty() bool { + return len(q.queue) == 0 +} diff --git a/exercise3/problem2/problem2.go b/exercise3/problem2/problem2.go index e9059889..650280f2 100644 --- a/exercise3/problem2/problem2.go +++ b/exercise3/problem2/problem2.go @@ -1,3 +1,35 @@ package problem2 -type Stack struct{} +import "errors" + +type Stack struct { + stack []any +} + +func (s *Stack) Push(val any) { + s.stack = append(s.stack, val) +} + +func (s *Stack) Pop() (any, error) { + if len(s.stack) == 0 { + return nil, errors.New("stack is empty") + } + item := s.stack[len(s.stack)-1] + s.stack = s.stack[:len(s.stack)-1] + return item, nil +} + +func (s *Stack) Peek() (any, error) { + if len(s.stack) == 0 { + return nil, errors.New("stack is empty") + } + return s.stack[len(s.stack)-1], nil +} + +func (s *Stack) Size() int { + return len(s.stack) +} + +func (s *Stack) IsEmpty() bool { + return len(s.stack) == 0 +} diff --git a/exercise3/problem3/problem3.go b/exercise3/problem3/problem3.go index d8d79ac0..b075759c 100644 --- a/exercise3/problem3/problem3.go +++ b/exercise3/problem3/problem3.go @@ -1,3 +1,99 @@ package problem3 -type Set struct{} +type Set struct { + items map[any]struct{} +} + +func NewSet() *Set { + return &Set{ + items: make(map[any]struct{}), + } +} + +func (s *Set) Add(item any) { + s.items[item] = struct{}{} +} + +func (s *Set) Remove(item any) { + delete(s.items, item) +} + +func (s *Set) Has(item any) bool { + _, exists := s.items[item] + return exists +} + +func (s *Set) Size() int { + return len(s.items) +} + +func (s *Set) IsEmpty() bool { + return len(s.items) == 0 +} + +func (s *Set) List() []any { + list := make([]any, 0, len(s.items)) + for item := range s.items { + list = append(list, item) + } + return list +} + +func (s *Set) Copy() *Set { + copySet := NewSet() + for item := range s.items { + copySet.Add(item) + } + return copySet +} + +func (s *Set) Difference(other *Set) *Set { + result := NewSet() + for item := range s.items { + if !other.Has(item) { + result.Add(item) + } + } + return result +} + +func (s *Set) IsSubset(other *Set) bool { + for item := range s.items { + if !other.Has(item) { + return false + } + } + return true +} + +func Union(sets ...*Set) *Set { + result := NewSet() + for _, set := range sets { + for item := range set.items { + result.Add(item) + } + } + return result +} + +func Intersect(sets ...*Set) *Set { + if len(sets) == 0 { + return NewSet() + } + + result := NewSet() + for item := range sets[0].items { + intersects := true + for _, set := range sets[1:] { + if !set.Has(item) { + intersects = false + break + } + } + if intersects { + result.Add(item) + } + } + + return result +} diff --git a/exercise3/problem4/problem4.go b/exercise3/problem4/problem4.go index ebf78147..7663e49e 100644 --- a/exercise3/problem4/problem4.go +++ b/exercise3/problem4/problem4.go @@ -1,3 +1,97 @@ package problem4 -type LinkedList struct{} +import ( + "errors" +) + +type Element[T comparable] struct { + value T + next *Element[T] +} + +type LinkedList[T comparable] struct { + head *Element[T] +} + +func (ll *LinkedList[T]) Add(el *Element[T]) { + if ll.head == nil { + ll.head = el + } else { + current := ll.head + for current.next != nil { + current = current.next + } + current.next = el + } +} + +func (ll *LinkedList[T]) Insert(el *Element[T], index int) error { + if index > ll.Size() { + return errors.New("index out of bounds") + } + + if index == 0 { + el.next = ll.head + ll.head = el + } else { + current := ll.head + for i := 0; i < index-1; i++ { + current = current.next + } + el.next = current.next + current.next = el + } + return nil +} + +func (ll *LinkedList[T]) Delete(el *Element[T]) error { + if ll.head == nil { + return errors.New("list is empty") + } + + if ll.head.value == el.value { + ll.head = ll.head.next + return nil + } + + current := ll.head + for current.next != nil && current.next.value != el.value { + current = current.next + } + + if current.next == nil { + return errors.New("element not found") + } + + current.next = current.next.next + return nil +} + +func (ll *LinkedList[T]) Find(value T) (*Element[T], error) { + current := ll.head + for current != nil { + if current.value == value { + return current, nil + } + current = current.next + } + return nil, errors.New("element not found") +} + +func (ll *LinkedList[T]) List() []T { + var elements []T + current := ll.head + for current != nil { + elements = append(elements, current.value) + current = current.next + } + return elements +} + +func (ll *LinkedList[T]) Size() int { + return len(ll.List()) +} + +func (ll *LinkedList[T]) IsEmpty() bool { + return ll.Size() == 0 +} diff --git a/exercise3/problem5/problem5.go b/exercise3/problem5/problem5.go index 4177599f..8ccb52a0 100644 --- a/exercise3/problem5/problem5.go +++ b/exercise3/problem5/problem5.go @@ -1,3 +1,18 @@ package problem5 -type Person struct{} +import "fmt" + +type Person struct { + Name string + Age int +} + +func (p1 Person) compareAge(p2 *Person) string { + if p1.Age > p2.Age { + return fmt.Sprintf("%s is younger than me.", p2.Name) + } + if p1.Age < p2.Age { + return fmt.Sprintf("%s is older than me.", p2.Name) + } + return fmt.Sprintf("%s is the same age as me.", p2.Name) +} diff --git a/exercise3/problem6/problem6.go b/exercise3/problem6/problem6.go index 4e8d1af8..e4289082 100644 --- a/exercise3/problem6/problem6.go +++ b/exercise3/problem6/problem6.go @@ -1,7 +1,31 @@ package problem6 -type Animal struct{} +type All interface { + legCount() int +} -type Insect struct{} +type Animal struct { + name string + legsNum int +} -func sumOfAllLegsNum() {} +func (a *Animal) legCount() int { + return a.legsNum +} + +type Insect struct { + name string + legsNum int +} + +func (i *Insect) legCount() int { + return i.legsNum +} + +func sumOfAllLegsNum(legged ...All) int { + sum := 0 + for _, a := range legged { + sum += a.legCount() + } + return sum +} diff --git a/exercise3/problem7/problem7.go b/exercise3/problem7/problem7.go index 26887151..a285cd9e 100644 --- a/exercise3/problem7/problem7.go +++ b/exercise3/problem7/problem7.go @@ -1,10 +1,53 @@ package problem7 +type Withdrawable interface { + Withdraw(amount int) +} + +type PackageSender interface { + SendPackage(to string) +} + type BankAccount struct { + name string + balance int +} + +func (b *BankAccount) Withdraw(amount int) { + b.balance -= amount } type FedexAccount struct { + name string + packages []string +} + +func (f *FedexAccount) SendPackage(to string) { + f.packages = append(f.packages, f.name+" send package to "+to) } type KazPostAccount struct { + name string + balance int + packages []string +} + +func (b *KazPostAccount) Withdraw(amount int) { + b.balance -= amount +} + +func (b *KazPostAccount) SendPackage(to string) { + b.packages = append(b.packages, b.name+" send package to "+to) +} + +func withdrawMoney(amount int, accounts ...Withdrawable) { + for _, account := range accounts { + account.Withdraw(amount) + } +} + +func sendPackagesTo(to string, senders ...PackageSender) { + for _, sender := range senders { + sender.SendPackage(to) + } } From 59592a236c33148c85089208824012f763cca531 Mon Sep 17 00:00:00 2001 From: "sh.kairatuly" Date: Sun, 3 Nov 2024 18:28:05 +0500 Subject: [PATCH 4/8] Shynggyskhan.Kairatuly exercise 5 --- exercise5/problem1/problem1.go | 8 ++++++-- exercise5/problem2/problem2.go | 31 +++++++++++++++++++++++++++++++ exercise5/problem3/problem3.go | 7 +++++-- exercise5/problem4/problem4.go | 1 + exercise5/problem5/problem5.go | 18 ++++++++++++++++-- exercise5/problem6/problem6.go | 30 +++++++++++++++++++++++++++--- exercise5/problem7/problem7.go | 34 +++++++++++++++++++++++++++++++++- exercise5/problem8/problem8.go | 12 +++++++++++- 8 files changed, 130 insertions(+), 11 deletions(-) diff --git a/exercise5/problem1/problem1.go b/exercise5/problem1/problem1.go index 4f514fab..7c04b186 100644 --- a/exercise5/problem1/problem1.go +++ b/exercise5/problem1/problem1.go @@ -1,9 +1,13 @@ package problem1 func incrementConcurrently(num int) int { - go func() { + ch := make(chan int) + go func(ch chan<- int) { num++ - }() + ch <- num + }(ch) + + <-ch return num } diff --git a/exercise5/problem2/problem2.go b/exercise5/problem2/problem2.go index 16d38e1d..d7ed7e37 100644 --- a/exercise5/problem2/problem2.go +++ b/exercise5/problem2/problem2.go @@ -1,5 +1,10 @@ package problem2 +import ( + "runtime" + "sync" +) + // add - sequential code to add numbers, don't update it, just to illustrate concept func add(numbers []int) int64 { var sum int64 @@ -9,8 +14,34 @@ func add(numbers []int) int64 { return sum } +// addConcurrently divides the work among available CPU cores and adds the numbers concurrently. func addConcurrently(numbers []int) int64 { var sum int64 + numCores := runtime.NumCPU() + chunkSize := (len(numbers) + numCores - 1) / numCores + var wg sync.WaitGroup + var mu sync.Mutex + + for i := 0; i < numCores; i++ { + start := i * chunkSize + end := start + chunkSize + if end > len(numbers) { + end = len(numbers) + } + + wg.Add(1) + go func(nums []int) { + defer wg.Done() + var localSum int64 + for _, n := range nums { + localSum += int64(n) + } + mu.Lock() + sum += localSum + mu.Unlock() + }(numbers[start:end]) + } + wg.Wait() return sum } diff --git a/exercise5/problem3/problem3.go b/exercise5/problem3/problem3.go index e085a51a..4791a224 100644 --- a/exercise5/problem3/problem3.go +++ b/exercise5/problem3/problem3.go @@ -2,10 +2,13 @@ package problem3 func sum(a, b int) int { var c int + ch := make(chan int) - go func(a, b int) { + go func(a, b int, ch chan int) { c = a + b - }(a, b) + ch <- c + }(a, b, ch) + <-ch return c } diff --git a/exercise5/problem4/problem4.go b/exercise5/problem4/problem4.go index b5899ddf..f8457faa 100644 --- a/exercise5/problem4/problem4.go +++ b/exercise5/problem4/problem4.go @@ -1,6 +1,7 @@ package problem4 func iter(ch chan<- int, nums []int) { + defer close(ch) for _, n := range nums { ch <- n } diff --git a/exercise5/problem5/problem5.go b/exercise5/problem5/problem5.go index ac192c58..37ecb3cb 100644 --- a/exercise5/problem5/problem5.go +++ b/exercise5/problem5/problem5.go @@ -1,9 +1,23 @@ package problem5 -func producer() {} +import "strings" -func consumer() {} +func producer(str []string, ch chan<- string) { + defer close(ch) + for _, s := range str { + ch <- s + } +} + +func consumer(ch <-chan string) string { + var str []string + for s := range ch { + str = append(str, s) + } + + return strings.Join(str, " ") +} func send( words []string, pr func([]string, chan<- string), diff --git a/exercise5/problem6/problem6.go b/exercise5/problem6/problem6.go index e1beea87..36bdee8d 100644 --- a/exercise5/problem6/problem6.go +++ b/exercise5/problem6/problem6.go @@ -2,8 +2,32 @@ package problem6 type pipe func(in <-chan int) <-chan int -var multiplyBy2 pipe = func() {} +var multiplyBy2 pipe = func(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for num := range in { + out <- num * 2 + } + }() + return out +} -var add5 pipe = func() {} +var add5 pipe = func(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for num := range in { + out <- num + 5 + } + }() + return out +} -func piper(in <-chan int, pipes []pipe) <-chan int {} +func piper(in <-chan int, pipes []pipe) <-chan int { + out := in + for _, p := range pipes { + out = p(out) + } + return out +} diff --git a/exercise5/problem7/problem7.go b/exercise5/problem7/problem7.go index c3c1d0c9..65958711 100644 --- a/exercise5/problem7/problem7.go +++ b/exercise5/problem7/problem7.go @@ -1,3 +1,35 @@ package problem7 -func multiplex(ch1 <-chan string, ch2 <-chan string) []string {} +func multiplex(ch1 <-chan string, ch2 <-chan string) []string { + ot := []string{} + str := make(chan string) + go func() { + for { + select { + case val, ok := <-ch1: + if ok { + str <- val + } else { + ch1 = nil + } + case val, ok := <-ch2: + if ok { + str <- val + } else { + ch2 = nil + } + } + + if ch1 == nil && ch2 == nil { + break + } + } + close(str) + }() + + for val := range str { + ot = append(ot, val) + } + + return ot +} diff --git a/exercise5/problem8/problem8.go b/exercise5/problem8/problem8.go index 3e951b3b..2341095d 100644 --- a/exercise5/problem8/problem8.go +++ b/exercise5/problem8/problem8.go @@ -4,4 +4,14 @@ import ( "time" ) -func withTimeout(ch <-chan string, ttl time.Duration) string {} +func withTimeout(ch <-chan string, ttl time.Duration) string { + select { + case msg, ok := <-ch: + if !ok { + return "fail" + } + return msg + case <-time.After(ttl): + return "timeout" + } +} From 5d700b25ea538c1d6ceac564015e717c7a34d848 Mon Sep 17 00:00:00 2001 From: "sh.kairatuly" Date: Sun, 17 Nov 2024 22:35:19 +0500 Subject: [PATCH 5/8] Shynggyskhan.Kairatuly exercise 6 --- exercise6/problem1/problem1.go | 21 ++++++++++++++++++++- exercise6/problem3/problem3.go | 14 ++++++++++++++ exercise6/problem4/problem4.go | 25 +++++++++++++++++-------- exercise6/problem5/problem5.go | 25 +++++++++++++++++-------- exercise6/problem7/problem7.go | 19 +++++++++++++++---- exercise6/problem8/problem8.go | 23 ++++++++++++++++++++++- 6 files changed, 105 insertions(+), 22 deletions(-) diff --git a/exercise6/problem1/problem1.go b/exercise6/problem1/problem1.go index ee453b24..bd445e96 100644 --- a/exercise6/problem1/problem1.go +++ b/exercise6/problem1/problem1.go @@ -1,9 +1,28 @@ package problem1 +import "sync" + type bankAccount struct { + mu sync.Mutex blnc int } func newAccount(blnc int) *bankAccount { - return &bankAccount{blnc} + return &bankAccount{blnc: blnc} +} + +func (a *bankAccount) deposit(amount int) { + a.mu.Lock() + defer a.mu.Unlock() + a.blnc += amount +} + +func (a *bankAccount) withdraw(amount int) bool { + a.mu.Lock() + defer a.mu.Unlock() + if a.blnc >= amount { + a.blnc -= amount + return true + } + return false } diff --git a/exercise6/problem3/problem3.go b/exercise6/problem3/problem3.go index b34b90bb..acb946e2 100644 --- a/exercise6/problem3/problem3.go +++ b/exercise6/problem3/problem3.go @@ -1,5 +1,7 @@ package problem3 +import "sync/atomic" + type counter struct { val int64 } @@ -9,3 +11,15 @@ func newCounter() *counter { val: 0, } } + +func (c *counter) inc() { + atomic.AddInt64(&c.val, 1) +} + +func (c *counter) dec() { + atomic.AddInt64(&c.val, -1) +} + +func (c *counter) value() int64 { + return atomic.LoadInt64(&c.val) +} diff --git a/exercise6/problem4/problem4.go b/exercise6/problem4/problem4.go index 793449c9..40755e1a 100644 --- a/exercise6/problem4/problem4.go +++ b/exercise6/problem4/problem4.go @@ -1,31 +1,40 @@ package problem4 import ( + "sync" "time" ) -func worker(id int, _ *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +func worker(id int, shoppingList *[]string, c *sync.Cond, ch chan<- int) { + c.L.Lock() + defer c.L.Unlock() + for len(*shoppingList) == 0 { + c.Wait() + } + ch <- id } - -func updateShopList(shoppingList *[]string) { +func updateShopList(shoppingList *[]string, c *sync.Cond) { time.Sleep(10 * time.Millisecond) - + c.L.Lock() + defer c.L.Unlock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + + c.Signal() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) - + m := sync.Mutex{} + newCond := sync.NewCond(&m) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) + go worker(i+1, shoppingList, newCond, notifier) time.Sleep(time.Millisecond) // order matters } - go updateShopList(shoppingList) + go updateShopList(shoppingList, newCond) return notifier } diff --git a/exercise6/problem5/problem5.go b/exercise6/problem5/problem5.go index 8e4a1703..f9d1bd97 100644 --- a/exercise6/problem5/problem5.go +++ b/exercise6/problem5/problem5.go @@ -1,31 +1,40 @@ package problem5 import ( + "sync" "time" ) -func worker(id int, shoppingList *[]string, ch chan<- int) { - // TODO wait for shopping list to be completed +func worker(id int, shoppingList *[]string, c *sync.Cond, ch chan<- int) { + c.L.Lock() + defer c.L.Unlock() + for len(*shoppingList) == 0 { + c.Wait() + } + ch <- id } - -func updateShopList(shoppingList *[]string) { +func updateShopList(shoppingList *[]string, c *sync.Cond) { time.Sleep(10 * time.Millisecond) - + c.L.Lock() + defer c.L.Unlock() *shoppingList = append(*shoppingList, "apples") *shoppingList = append(*shoppingList, "milk") *shoppingList = append(*shoppingList, "bake soda") + + c.Broadcast() } func notifyOnShopListUpdate(shoppingList *[]string, numWorkers int) <-chan int { notifier := make(chan int) - + m := sync.Mutex{} + newCond := sync.NewCond(&m) for i := range numWorkers { - go worker(i+1, shoppingList, notifier) + go worker(i+1, shoppingList, newCond, notifier) time.Sleep(time.Millisecond) // order matters } - go updateShopList(shoppingList) + go updateShopList(shoppingList, newCond) return notifier } diff --git a/exercise6/problem7/problem7.go b/exercise6/problem7/problem7.go index ef49497b..0ea037b2 100644 --- a/exercise6/problem7/problem7.go +++ b/exercise6/problem7/problem7.go @@ -1,21 +1,32 @@ package problem7 import ( - "fmt" "math/rand" + "sync" "time" ) func task() { - start := time.Now() + var m sync.Mutex var t *time.Timer + + resetTimer := func() { + m.Lock() + defer m.Unlock() + if t != nil { + t.Reset(randomDuration()) + } + } + + m.Lock() t = time.AfterFunc( randomDuration(), func() { - fmt.Println(time.Now().Sub(start)) - t.Reset(randomDuration()) + resetTimer() }, ) + m.Unlock() + time.Sleep(5 * time.Second) } diff --git a/exercise6/problem8/problem8.go b/exercise6/problem8/problem8.go index 949eb2d2..fea87ccc 100644 --- a/exercise6/problem8/problem8.go +++ b/exercise6/problem8/problem8.go @@ -1,3 +1,24 @@ package problem8 -func multiplex(chs []<-chan string) []string {} +import "sync" + +func multiplex(chs []<-chan string) []string { + var result []string + var wg sync.WaitGroup + var mux sync.Mutex + + for _, ch := range chs { + wg.Add(1) + go func(ch <-chan string) { + defer wg.Done() + for msg := range ch { + mux.Lock() + result = append(result, msg) + mux.Unlock() + } + }(ch) + } + + wg.Wait() + return result +} From 0775bd44d12c69eadba90f0aef61ccd0a3bbb121 Mon Sep 17 00:00:00 2001 From: Talgat Date: Thu, 23 Jan 2025 19:03:48 +0500 Subject: [PATCH 6/8] add exercise9 --- exercise9/README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ exercise9/go.mod | 3 +++ 2 files changed, 45 insertions(+) create mode 100644 exercise9/README.md create mode 100644 exercise9/go.mod diff --git a/exercise9/README.md b/exercise9/README.md new file mode 100644 index 00000000..5d54880e --- /dev/null +++ b/exercise9/README.md @@ -0,0 +1,42 @@ +# Exercise 9 + +Project + +## Teams + +Team 1 + +1. Имангали Аскар (controller) +2. Зернов Владислав (controller) +3. Курмашев Сабит (api) +4. Кабулов Нуртас (api) +5. Омаров Темирлан (db) +6. Сагиндиков Меирбек (db) + +Team 2 + +1. Тұрарова Айзада (controller) +2. Толеу Аян (controller) +3. Мырзаханов Алинур (api) +4. Еркибаев Зураб (api) +5. Бақатай Ақжол (db) +6. Бимаканова Мадина (db) + +Team 3 + +1. Кабдылкак Арнур (controller) +2. Калкин Ернар (controller) +3. Манкенов Арай (api) +4. Усербай Асылбек (api) +5. Камбаров Руслан (db) +6. Қайратұлы Шыңғысхан (db) + +Team 4 + +1. Жақуда Жарқынай (controller) +2. Жантасов Адлет (controller) +3. Туралин Аргын (api) +4. Алтынбек Жандос (api) +5. Жакупов Жандаулет (db) +6. Мұхаметқали Арайлым (db) +7. Кемалатдин Ғалымжан (your choice) diff --git a/exercise9/go.mod b/exercise9/go.mod new file mode 100644 index 00000000..72f28b6f --- /dev/null +++ b/exercise9/go.mod @@ -0,0 +1,3 @@ +module github.com/talgat-ruby/exercises-go/exercise9 + +go 1.23.5 From b3adc4424e9a58d500fa0d48cd8830cae7869172 Mon Sep 17 00:00:00 2001 From: "sh.kairatuly" Date: Mon, 10 Feb 2025 11:12:01 +0500 Subject: [PATCH 7/8] Shynggyskhan.Kairatuly exercise 7 --- exercise7/blogging-platform/README.md | 27 +++- exercise7/blogging-platform/go.mod | 6 +- exercise7/blogging-platform/go.sum | 4 + .../internal/api/handler/auth/access_token.go | 115 ++++++++++++++++ .../internal/api/handler/auth/login.go | 124 ++++++++++++++++++ .../internal/api/handler/auth/main.go | 29 ++++ .../internal/api/handler/auth/register.go | 107 +++++++++++++++ .../internal/api/handler/blogs/create_blog.go | 95 ++++++++++++++ .../internal/api/handler/blogs/gelete_blog.go | 56 ++++++++ .../internal/api/handler/blogs/get_blog.go | 65 +++++++++ .../internal/api/handler/blogs/get_blogs.go | 47 +++++++ .../internal/api/handler/blogs/main.go | 18 +++ .../internal/api/handler/blogs/update_blog.go | 75 +++++++++++ .../internal/api/handler/main.go | 20 +++ .../blogging-platform/internal/api/main.go | 58 ++++++++ .../internal/api/middleware/authenticator.go | 68 ++++++++++ .../internal/api/middleware/main.go | 15 +++ .../internal/api/router/auth.go | 11 ++ .../internal/api/router/blogs.go | 14 ++ .../internal/api/router/main.go | 31 +++++ .../blogging-platform/internal/auth/hash.go | 71 ++++++++++ .../blogging-platform/internal/auth/main.go | 11 ++ .../blogging-platform/internal/auth/tokens.go | 83 ++++++++++++ .../internal/db/auth/access_token.go | 45 +++++++ .../internal/db/auth/login.go | 48 +++++++ .../internal/db/auth/main.go | 18 +++ .../internal/db/auth/model.go | 14 ++ .../internal/db/auth/register.go | 40 ++++++ .../internal/db/blog/create_blog.go | 44 +++++++ .../internal/db/blog/delete_blog.go | 35 +++++ .../internal/db/blog/get_blog.go | 38 ++++++ .../internal/db/blog/get_blogs.go | 46 +++++++ .../internal/db/blog/main.go | 18 +++ .../internal/db/blog/model.go | 13 ++ .../internal/db/blog/update_blog.go | 36 +++++ .../blogging-platform/internal/db/main.go | 56 ++++++++ ...20250127143958_create_blogs_table.down.sql | 1 + .../20250127143958_create_blogs_table.up.sql | 7 + .../20250207050918_create_table_user.down.sql | 1 + .../20250207050918_create_table_user.up.sql | 8 ++ exercise7/blogging-platform/main.go | 34 +++-- 41 files changed, 1635 insertions(+), 17 deletions(-) create mode 100644 exercise7/blogging-platform/internal/api/handler/auth/access_token.go create mode 100644 exercise7/blogging-platform/internal/api/handler/auth/login.go create mode 100644 exercise7/blogging-platform/internal/api/handler/auth/main.go create mode 100644 exercise7/blogging-platform/internal/api/handler/auth/register.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/create_blog.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/gelete_blog.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/get_blog.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/main.go create mode 100644 exercise7/blogging-platform/internal/api/handler/blogs/update_blog.go create mode 100644 exercise7/blogging-platform/internal/api/handler/main.go create mode 100644 exercise7/blogging-platform/internal/api/main.go create mode 100644 exercise7/blogging-platform/internal/api/middleware/authenticator.go create mode 100644 exercise7/blogging-platform/internal/api/middleware/main.go create mode 100644 exercise7/blogging-platform/internal/api/router/auth.go create mode 100644 exercise7/blogging-platform/internal/api/router/blogs.go create mode 100644 exercise7/blogging-platform/internal/api/router/main.go create mode 100644 exercise7/blogging-platform/internal/auth/hash.go create mode 100644 exercise7/blogging-platform/internal/auth/main.go create mode 100644 exercise7/blogging-platform/internal/auth/tokens.go create mode 100644 exercise7/blogging-platform/internal/db/auth/access_token.go create mode 100644 exercise7/blogging-platform/internal/db/auth/login.go create mode 100644 exercise7/blogging-platform/internal/db/auth/main.go create mode 100644 exercise7/blogging-platform/internal/db/auth/model.go create mode 100644 exercise7/blogging-platform/internal/db/auth/register.go create mode 100644 exercise7/blogging-platform/internal/db/blog/create_blog.go create mode 100644 exercise7/blogging-platform/internal/db/blog/delete_blog.go create mode 100644 exercise7/blogging-platform/internal/db/blog/get_blog.go create mode 100644 exercise7/blogging-platform/internal/db/blog/get_blogs.go create mode 100644 exercise7/blogging-platform/internal/db/blog/main.go create mode 100644 exercise7/blogging-platform/internal/db/blog/model.go create mode 100644 exercise7/blogging-platform/internal/db/blog/update_blog.go create mode 100644 exercise7/blogging-platform/internal/db/main.go create mode 100644 exercise7/blogging-platform/internal/db/migrations/20250127143958_create_blogs_table.down.sql create mode 100644 exercise7/blogging-platform/internal/db/migrations/20250127143958_create_blogs_table.up.sql create mode 100644 exercise7/blogging-platform/internal/db/migrations/20250207050918_create_table_user.down.sql create mode 100644 exercise7/blogging-platform/internal/db/migrations/20250207050918_create_table_user.up.sql diff --git a/exercise7/blogging-platform/README.md b/exercise7/blogging-platform/README.md index e6ef7017..9850e33f 100644 --- a/exercise7/blogging-platform/README.md +++ b/exercise7/blogging-platform/README.md @@ -1,3 +1,26 @@ -# Blogging Platform +# Movie Reservation -Please check https://roadmap.sh/projects/blogging-platform-api. +docker compose run + +```shell +$ docker compose --env-file=./.env up --build +``` + +curl for api request + +```shell +$ curl 'http://127.0.0.1:4013/movies' +``` + +migrations + +```shell +docker compose --profile tools run --rm migrate create -ext sql -dir ./migrations NAME_OF_MIGRATION_FILE +docker compose --profile tools run --rm migrate {up,down} +``` + +seeds + +```shell +go run ./internal/cli/... seed +``` diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..d05fc000 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -2,4 +2,8 @@ module github.com/talgat-ruby/exercises-go/exercise7/blogging-platform go 1.23.3 -require github.com/lib/pq v1.10.9 +require ( + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 +) diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index aeddeae3..f0b2cdb0 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -1,2 +1,6 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/exercise7/blogging-platform/internal/api/handler/auth/access_token.go b/exercise7/blogging-platform/internal/api/handler/auth/access_token.go new file mode 100644 index 00000000..7be7774e --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/auth/access_token.go @@ -0,0 +1,115 @@ +package auth + +import ( + "fmt" + "net/http" + "os" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/auth" + dbAuth "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/auth" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +type AccessTokenRefreshTokenRequest struct { + RefreshToken string `json:"refresh_token"` +} + +type AccessTokenRequest struct { + Data *AccessTokenRefreshTokenRequest `json:"data"` +} + +type AccessTokenResponse struct { + Data *AuthTokenPair `json:"data"` +} + +func (h *Auth) AccessToken(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "AccessToken") + + // request parse + requestBody := &AccessTokenRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + userData, err := auth.ParseToken(requestBody.Data.RefreshToken, os.Getenv("TOKEN_SECRET")) + if err != nil { + log.ErrorContext( + ctx, + "fail authentication", + "error", err, + ) + http.Error( + w, + http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized, + ) + return + } + + // db request + dbResp, err := h.db.AccessToken(ctx, &dbAuth.AccessTokenInput{UserID: userData.ID}) + if err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + if dbResp == nil { + log.ErrorContext( + ctx, + "row is empty", + ) + http.Error(w, "row is empty", http.StatusInternalServerError) + return + } + + tokenPair, err := auth.GenerateTokenPair( + &auth.UserData{ + ID: fmt.Sprint(dbResp.ID), + Email: dbResp.Email, + }, + os.Getenv("TOKEN_SECRET"), + ) + if err != nil { + http.Error(w, fmt.Sprintf("invalid request %w", err), http.StatusBadRequest) + return + } + + // response + respBody := &AccessTokenResponse{ + Data: &AuthTokenPair{ + AccessToken: tokenPair.AccessToken, + RefreshToken: tokenPair.RefreshToken, + }, + } + if err := response.JSON( + w, + http.StatusOK, + respBody, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success generate access token", + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/auth/login.go b/exercise7/blogging-platform/internal/api/handler/auth/login.go new file mode 100644 index 00000000..0b661d29 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/auth/login.go @@ -0,0 +1,124 @@ +package auth + +import ( + "fmt" + "net/http" + "os" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/auth" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +type LoginRequest struct { + Data *AuthUser `json:"data"` +} + +type LoginResponse struct { + Data *AuthTokenPair `json:"data"` +} + +func (h *Auth) Login(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "Login") + + // request parse + requestBody := &LoginRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + // db request + dbResp, err := h.db.Login(ctx, requestBody.Data.Email) + if err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + + if dbResp == nil { + log.ErrorContext( + ctx, + "row is empty", + "email", requestBody.Data.Email, + ) + http.Error(w, "invalid credentials", http.StatusBadRequest) + return + } + + // passwordHash and salt + isValid, err := auth.VerifyPassword( + requestBody.Data.Password, + os.Getenv("TOKEN_PEPPER"), + dbResp.PasswordHash, + dbResp.Salt, + ) + if err != nil { + log.ErrorContext( + ctx, + "verify password failed", + "error", err, + ) + http.Error(w, "verify password failed", http.StatusInternalServerError) + return + } + if !isValid { + log.ErrorContext( + ctx, + "invalid credentials", + "email", requestBody.Data.Email, + ) + http.Error(w, "invalid credentials", http.StatusBadRequest) + return + } + + tokenPair, err := auth.GenerateTokenPair( + &auth.UserData{ + ID: fmt.Sprint(dbResp.ID), + Email: dbResp.Email, + }, + os.Getenv("TOKEN_SECRET"), + ) + if err != nil { + http.Error(w, fmt.Sprintf("invalid request %w", err), http.StatusBadRequest) + return + } + + // response + respBody := &LoginResponse{ + Data: &AuthTokenPair{ + AccessToken: tokenPair.AccessToken, + RefreshToken: tokenPair.RefreshToken, + }, + } + if err := response.JSON( + w, + http.StatusOK, + respBody, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success login user", + "email", dbResp.Email, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/auth/main.go b/exercise7/blogging-platform/internal/api/handler/auth/main.go new file mode 100644 index 00000000..05bcb5ec --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/auth/main.go @@ -0,0 +1,29 @@ +package auth + +import ( + "log/slog" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" +) + +type Auth struct { + logger *slog.Logger + db *db.DB +} + +func New(logger *slog.Logger, db *db.DB) *Auth { + return &Auth{ + logger: logger, + db: db, + } +} + +type AuthUser struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type AuthTokenPair struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} diff --git a/exercise7/blogging-platform/internal/api/handler/auth/register.go b/exercise7/blogging-platform/internal/api/handler/auth/register.go new file mode 100644 index 00000000..115048b3 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/auth/register.go @@ -0,0 +1,107 @@ +package auth + +import ( + "fmt" + "net/http" + "os" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/auth" + dbAuth "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/auth" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" +) + +type RegisterRequest struct { + Data *AuthUser `json:"data"` +} + +type RegisterResponse struct { + Data *AuthTokenPair `json:"data"` +} + +func (h *Auth) Register(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "Register") + + // request parse + requestBody := &RegisterRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + // passwordHash and salt + passHash, salt, err := auth.HashPassword(requestBody.Data.Password, os.Getenv("TOKEN_PEPPER")) + if err != nil { + log.ErrorContext( + ctx, + "hashing password failed", + "error", err, + ) + http.Error(w, "hashing password failed", http.StatusInternalServerError) + return + } + + // db request + userModel := &dbAuth.ModelUser{ + Email: requestBody.Data.Email, + PasswordHash: passHash, + Salt: salt, + } + dbResp, err := h.db.Register(ctx, &dbAuth.RegisterInput{userModel}) + if err != nil { + log.ErrorContext( + ctx, + "failed to insert user data", + "error", err, + ) + http.Error(w, "failed to insert user data", http.StatusInternalServerError) + return + } + + // create token pair + tokenPair, err := auth.GenerateTokenPair( + &auth.UserData{ + ID: fmt.Sprint(dbResp.ID), + Email: dbResp.Email, + }, + os.Getenv("TOKEN_SECRET"), + ) + + if err != nil { + http.Error(w, fmt.Sprintf("invalid request %w", err), http.StatusBadRequest) + return + } + + respBody := &RegisterResponse{ + Data: &AuthTokenPair{ + AccessToken: tokenPair.AccessToken, + RefreshToken: tokenPair.RefreshToken, + }, + } + if err := response.JSON( + w, + http.StatusOK, + respBody, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success register user", + "user email", userModel.Email, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/create_blog.go b/exercise7/blogging-platform/internal/api/handler/blogs/create_blog.go new file mode 100644 index 00000000..f11e92c7 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/create_blog.go @@ -0,0 +1,95 @@ +package blogs + +import ( + "fmt" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/auth" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" +) + +type CreateBlogRequest struct { + Data *blog.ModelBlogs `json:"data"` +} + +type CreateBlogResponse struct { + Data *blog.ModelBlogs `json:"data"` +} + +func (h *Blogs) CreateBlog(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "CreateBlog") + + user, ok := ctx.Value("user").(*auth.UserData) + if !ok { + log.ErrorContext( + ctx, + "failed to type cast user data", + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + fmt.Printf("user: %+v\n", *user) + + // request parse + requestBody := &CreateBlogRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + // db request + dbResp, err := h.db.CreateBlog(ctx, requestBody.Data) + + if err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, "failed to query from db", http.StatusInternalServerError) + return + } + + if dbResp == nil { + log.ErrorContext( + ctx, + "row is empty", + ) + http.Error(w, "row is empty", http.StatusInternalServerError) + return + } + + // response + resp := CreateBlogResponse{ + Data: dbResp, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success insert blog", + "blog id", resp.Data.ID, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/gelete_blog.go b/exercise7/blogging-platform/internal/api/handler/blogs/gelete_blog.go new file mode 100644 index 00000000..58a286fe --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/gelete_blog.go @@ -0,0 +1,56 @@ +package blogs + +import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" + "strconv" +) + +func (h *Blogs) DeleteBlog(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "DeleteBlog") + + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + log.ErrorContext( + ctx, + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + + // db request + if err := h.db.DeleteBlog(ctx, int64(id)); err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := response.JSON( + w, + http.StatusNoContent, + nil, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success delete blog", + "id", id, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/get_blog.go b/exercise7/blogging-platform/internal/api/handler/blogs/get_blog.go new file mode 100644 index 00000000..d65098f6 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/get_blog.go @@ -0,0 +1,65 @@ +package blogs + +import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" + "strconv" +) + +type GetBlogResponse struct { + Data *blog.ModelBlogs `json:"data"` +} + +func (h *Blogs) GetBlog(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "GetBlog") + + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + log.ErrorContext( + ctx, + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + dbResp, err := h.db.GetBlog(ctx, int64(id)) + + if err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + resp := GetBlogResponse{ + Data: dbResp, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success find blog", + "blog id", resp.Data.ID, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go b/exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go new file mode 100644 index 00000000..543292fe --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/get_blogs.go @@ -0,0 +1,47 @@ +package blogs + +import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" +) + +type GetBlogsResponse struct { + Data []blog.ModelBlogs `json:"data"` +} + +func (b *Blogs) GetBlogs(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := b.logger.With("method", "GetBlogs") + + dbResp, err := b.db.GetBlogs(ctx) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + resp := GetBlogsResponse{ + Data: dbResp, + } + + if err := response.JSON( + w, + http.StatusOK, + resp, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success find blogs", + "number_of_blogs", len(resp.Data), + ) + return + +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/main.go b/exercise7/blogging-platform/internal/api/handler/blogs/main.go new file mode 100644 index 00000000..7dc1f7d9 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/main.go @@ -0,0 +1,18 @@ +package blogs + +import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" + "log/slog" +) + +type Blogs struct { + logger *slog.Logger + db *db.DB +} + +func New(logger *slog.Logger, db *db.DB) *Blogs { + return &Blogs{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/api/handler/blogs/update_blog.go b/exercise7/blogging-platform/internal/api/handler/blogs/update_blog.go new file mode 100644 index 00000000..53381af7 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/blogs/update_blog.go @@ -0,0 +1,75 @@ +package blogs + +import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/request" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/pkg/httputils/response" + "net/http" + "strconv" +) + +type UpdateBlogRequest struct { + Data *blog.ModelBlogs `json:"data"` +} + +func (h *Blogs) UpdateBlog(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := h.logger.With("method", "UpdateBlog") + + idStr := r.PathValue("id") + + id, err := strconv.Atoi(idStr) + if err != nil { + log.ErrorContext( + ctx, + "failed to convert id to int", + "error", err, + ) + http.Error(w, "failed to convert id to int", http.StatusBadRequest) + return + } + + // request parse + requestBody := &UpdateBlogRequest{} + + if err := request.JSON(w, r, requestBody); err != nil { + log.ErrorContext( + ctx, + "failed to parse request body", + "error", err, + ) + http.Error(w, "failed to parse request body", http.StatusBadRequest) + return + } + + // db request + if err := h.db.UpdateBlog(ctx, int64(id), requestBody.Data); err != nil { + log.ErrorContext( + ctx, + "failed to query from db", + "error", err, + ) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := response.JSON( + w, + http.StatusNoContent, + nil, + ); err != nil { + log.ErrorContext( + ctx, + "fail json", + "error", err, + ) + return + } + + log.InfoContext( + ctx, + "success update blog", + "id", id, + ) + return +} diff --git a/exercise7/blogging-platform/internal/api/handler/main.go b/exercise7/blogging-platform/internal/api/handler/main.go new file mode 100644 index 00000000..79743cb0 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/handler/main.go @@ -0,0 +1,20 @@ +package handler + +import ( + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler/auth" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler/blogs" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" + "log/slog" +) + +type Handler struct { + *blogs.Blogs + *auth.Auth +} + +func New(logger *slog.Logger, db *db.DB) *Handler { + return &Handler{ + Blogs: blogs.New(logger, db), + Auth: auth.New(logger, db), + } +} diff --git a/exercise7/blogging-platform/internal/api/main.go b/exercise7/blogging-platform/internal/api/main.go new file mode 100644 index 00000000..f93c593b --- /dev/null +++ b/exercise7/blogging-platform/internal/api/main.go @@ -0,0 +1,58 @@ +package api + +import ( + "context" + "errors" + "fmt" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/middleware" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/router" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db" + "log/slog" + "net" + "net/http" + "os" + "strconv" +) + +type Api struct { + db *db.DB + logger *slog.Logger +} + +func New(logger *slog.Logger, db *db.DB) *Api { + + return &Api{logger: logger, db: db} +} + +func (api *Api) Start(ctx context.Context) error { + h := handler.New(slog.With("service", "handler"), api.db) + midd := middleware.New(api.logger) + r, err := router.New(h, midd) + + if err != nil { + return err + } + + mux := r.Start(ctx) + + port, err := strconv.Atoi(os.Getenv("API_PORT")) + if err != nil { + return err + } + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + BaseContext: func(_ net.Listener) context.Context { + return ctx + }, + } + + fmt.Printf("Starting server on :%d\n", port) + if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { + return err + } + + return nil +} diff --git a/exercise7/blogging-platform/internal/api/middleware/authenticator.go b/exercise7/blogging-platform/internal/api/middleware/authenticator.go new file mode 100644 index 00000000..c8a83028 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/middleware/authenticator.go @@ -0,0 +1,68 @@ +package middleware + +import ( + "context" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/auth" + "net/http" + "os" + "strings" +) + +func (m *Middleware) Authenticator( + next http.Handler, +) http.Handler { + h := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := m.log.With("middleware", "Authenticator") + + authorizationHeader := r.Header.Get("Authorization") + if authorizationHeader == "" { + log.ErrorContext( + ctx, + "authorization header is empty", + ) + http.Error( + w, + http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized, + ) + return + } + + if !strings.HasPrefix(authorizationHeader, "Bearer ") { + log.ErrorContext( + ctx, + "invalid authorization header", + ) + http.Error( + w, + http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized, + ) + return + } + + tokenString := authorizationHeader[len("Bearer "):] + + userData, err := auth.ParseToken(tokenString, os.Getenv("TOKEN_SECRET")) + if err != nil { + log.ErrorContext( + ctx, + "fail authentication", + "error", err, + ) + http.Error( + w, + http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized, + ) + return + } + + newCtx := context.WithValue(ctx, "user", userData) + + next.ServeHTTP(w, r.WithContext(newCtx)) + } + + return http.HandlerFunc(h) +} diff --git a/exercise7/blogging-platform/internal/api/middleware/main.go b/exercise7/blogging-platform/internal/api/middleware/main.go new file mode 100644 index 00000000..62556b94 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/middleware/main.go @@ -0,0 +1,15 @@ +package middleware + +import "log/slog" + +type Middleware struct { + log *slog.Logger +} + +func New( + log *slog.Logger, +) *Middleware { + return &Middleware{ + log: log, + } +} diff --git a/exercise7/blogging-platform/internal/api/router/auth.go b/exercise7/blogging-platform/internal/api/router/auth.go new file mode 100644 index 00000000..bec45c25 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/auth.go @@ -0,0 +1,11 @@ +package router + +import ( + "context" +) + +func (r *Router) auth(ctx context.Context) { + r.router.HandleFunc("POST /register", r.handler.Register) + r.router.HandleFunc("POST /login", r.handler.Login) + r.router.HandleFunc("POST /access-token", r.handler.AccessToken) +} diff --git a/exercise7/blogging-platform/internal/api/router/blogs.go b/exercise7/blogging-platform/internal/api/router/blogs.go new file mode 100644 index 00000000..1390bb3b --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/blogs.go @@ -0,0 +1,14 @@ +package router + +import ( + "context" + "net/http" +) + +func (r *Router) blogs(ctx context.Context) { + r.router.Handle("GET /blogs", http.HandlerFunc(r.handler.GetBlogs)) + r.router.Handle("GET /blogs/{id}", http.HandlerFunc(r.handler.GetBlog)) + r.router.Handle("POST /blogs" /*r.midd.Authenticator*/, http.HandlerFunc(r.handler.CreateBlog)) + r.router.Handle("PUT /blogs/{id}" /*r.midd.Authenticator*/, http.HandlerFunc(r.handler.UpdateBlog)) + r.router.Handle("DELETE /blogs/{id}", http.HandlerFunc(r.handler.DeleteBlog)) +} diff --git a/exercise7/blogging-platform/internal/api/router/main.go b/exercise7/blogging-platform/internal/api/router/main.go new file mode 100644 index 00000000..cbb1a8e4 --- /dev/null +++ b/exercise7/blogging-platform/internal/api/router/main.go @@ -0,0 +1,31 @@ +package router + +import ( + "context" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/handler" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/api/middleware" + "net/http" +) + +type Router struct { + router *http.ServeMux + handler *handler.Handler + midd *middleware.Middleware +} + +func New(h *handler.Handler, m *middleware.Middleware) (*Router, error) { + mux := http.NewServeMux() + + return &Router{ + router: mux, + handler: h, + midd: m, + }, nil +} + +func (r *Router) Start(ctx context.Context) *http.ServeMux { + r.auth(ctx) + r.blogs(ctx) + + return r.router +} diff --git a/exercise7/blogging-platform/internal/auth/hash.go b/exercise7/blogging-platform/internal/auth/hash.go new file mode 100644 index 00000000..016883aa --- /dev/null +++ b/exercise7/blogging-platform/internal/auth/hash.go @@ -0,0 +1,71 @@ +package auth + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "crypto/subtle" + "encoding/base64" + "fmt" +) + +const ( + // Recommended minimum lengths for security + SaltLength = 16 // 16 bytes = 128 bits + PepperLength = 32 // 32 bytes = 256 bits + HashLength = 64 // SHA-512 outputs 64 bytes + Iterations = 210000 // Recommended minimum iterations for PBKDF2 +) + +func HashPassword(password string, pepper string) (string, string, error) { + // Generate random salt + salt := make([]byte, SaltLength) + if _, err := rand.Read(salt); err != nil { + return "", "", fmt.Errorf("error generating salt: %w", err) + } + + // Hash the password + hash := hashWithSaltAndPepper([]byte(password), salt, []byte(pepper)) + + return base64.StdEncoding.EncodeToString(hash), base64.StdEncoding.EncodeToString(salt), nil +} + +func hashWithSaltAndPepper(password, salt, pepper []byte) []byte { + // Combine password and pepper + pepperedPassword := make([]byte, len(password)+len(pepper)) + copy(pepperedPassword, password) + copy(pepperedPassword[len(password):], pepper) + + // Initialize HMAC-SHA512 + hash := hmac.New(sha512.New, salt) + + // Perform multiple iterations of hashing + result := pepperedPassword + for i := 0; i < Iterations; i++ { + hash.Reset() + hash.Write(result) + result = hash.Sum(nil) + } + + return result +} + +// VerifyPassword checks if a password matches its hash +func VerifyPassword(password, pepper, hash, salt string) (bool, error) { + // Decode salt and hash from base64 + decodedSalt, err := base64.StdEncoding.DecodeString(salt) + if err != nil { + return false, fmt.Errorf("error decoding salt: %w", err) + } + + decodedHash, err := base64.StdEncoding.DecodeString(hash) + if err != nil { + return false, fmt.Errorf("error decoding hash: %w", err) + } + + // Hash the provided password with the same salt + newHash := hashWithSaltAndPepper([]byte(password), decodedSalt, []byte(pepper)) + + // Compare hashes in constant time to prevent timing attacks + return subtle.ConstantTimeCompare(newHash, decodedHash) == 1, nil +} diff --git a/exercise7/blogging-platform/internal/auth/main.go b/exercise7/blogging-platform/internal/auth/main.go new file mode 100644 index 00000000..3cf5f8ba --- /dev/null +++ b/exercise7/blogging-platform/internal/auth/main.go @@ -0,0 +1,11 @@ +package auth + +type Tokens struct { + RefreshToken string `json:"refresh_token"` + AccessToken string `json:"access_token"` +} + +type UserData struct { + ID string `json:"id"` + Email string `json:"email"` +} diff --git a/exercise7/blogging-platform/internal/auth/tokens.go b/exercise7/blogging-platform/internal/auth/tokens.go new file mode 100644 index 00000000..7276515d --- /dev/null +++ b/exercise7/blogging-platform/internal/auth/tokens.go @@ -0,0 +1,83 @@ +package auth + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +func GenerateTokenPair(user *UserData, secret string) (*Tokens, error) { + // Create token + token := jwt.New(jwt.SigningMethodHS256) + + // Set claims + // This is the information which frontend can use + // The backend can also decode the token and get admin etc. + claims := token.Claims.(jwt.MapClaims) + claims["sub"] = user.ID + claims["email"] = user.Email + claims["exp"] = time.Now().Add(time.Minute * 15).Unix() + + // Generate encoded token and send it as response. + // The signing string should be secret (a generated UUID works too) + t, err := token.SignedString([]byte(secret)) + if err != nil { + return nil, err + } + + refreshToken := jwt.New(jwt.SigningMethodHS256) + rtClaims := refreshToken.Claims.(jwt.MapClaims) + rtClaims["sub"] = user.ID + rtClaims["exp"] = time.Now().Add(time.Hour * 24).Unix() + + rt, err := refreshToken.SignedString([]byte(secret)) + if err != nil { + return nil, err + } + + return &Tokens{ + AccessToken: t, + RefreshToken: rt, + }, nil +} + +func ParseToken(token string, secret string) (*UserData, error) { + t, err := jwt.Parse( + token, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }, + ) + + switch { + case t == nil: + return nil, fmt.Errorf("invalid token") + case t.Valid: + claims, ok := t.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid token") + } + + id, err := claims.GetSubject() + if err != nil { + return nil, fmt.Errorf("invalid token") + } + email, ok := claims["email"].(string) + + return &UserData{ + ID: id, + Email: email, + }, nil + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, fmt.Errorf("invalid token") + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + // Invalid signature + return nil, fmt.Errorf("Invalid signature") + case errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet): + // Token is either expired or not active yet + return nil, err + default: + return nil, fmt.Errorf("invalid token") + } +} diff --git a/exercise7/blogging-platform/internal/db/auth/access_token.go b/exercise7/blogging-platform/internal/db/auth/access_token.go new file mode 100644 index 00000000..45b08676 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/auth/access_token.go @@ -0,0 +1,45 @@ +package auth + +import ( + "context" + "database/sql" + "errors" +) + +type AccessTokenInput struct { + UserID string +} + +func (m *Auth) AccessToken(ctx context.Context, inp *AccessTokenInput) (*ModelUser, error) { + log := m.logger.With("method", "AccessToken") + + stmt := ` +SELECT id, email +FROM user_ +WHERE id = $1; +` + + row := m.db.QueryRowContext(ctx, stmt, inp.UserID) + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table user", "error", err) + return nil, err + } + + user := ModelUser{} + + if err := row.Scan( + &user.ID, + &user.Email, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.ErrorContext(ctx, "no user found", "error", err) + return nil, nil + } + + log.ErrorContext(ctx, "fail to scan user", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success select user") + return &user, nil +} diff --git a/exercise7/blogging-platform/internal/db/auth/login.go b/exercise7/blogging-platform/internal/db/auth/login.go new file mode 100644 index 00000000..dd3034b1 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/auth/login.go @@ -0,0 +1,48 @@ +package auth + +import ( + "context" + "database/sql" + "errors" +) + +type LoginInput struct { + User *ModelUser +} + +func (m *Auth) Login(ctx context.Context, email string) (*ModelUser, error) { + log := m.logger.With("method", "Login") + + stmt := ` +SELECT id, email, password_hash, salt +FROM user_ +WHERE email = $1; +` + + row := m.db.QueryRowContext(ctx, stmt, email) + + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table user", "error", err) + return nil, err + } + + user := ModelUser{} + + if err := row.Scan( + &user.ID, + &user.Email, + &user.PasswordHash, + &user.Salt, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.WarnContext(ctx, "no user found", "error", err) + return nil, nil + } + + log.ErrorContext(ctx, "fail to scan user", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success login user") + return &user, nil +} diff --git a/exercise7/blogging-platform/internal/db/auth/main.go b/exercise7/blogging-platform/internal/db/auth/main.go new file mode 100644 index 00000000..bba67916 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/auth/main.go @@ -0,0 +1,18 @@ +package auth + +import ( + "database/sql" + "log/slog" +) + +type Auth struct { + logger *slog.Logger + db *sql.DB +} + +func New(db *sql.DB, logger *slog.Logger) *Auth { + return &Auth{ + logger: logger, + db: db, + } +} diff --git a/exercise7/blogging-platform/internal/db/auth/model.go b/exercise7/blogging-platform/internal/db/auth/model.go new file mode 100644 index 00000000..3d9b81f9 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/auth/model.go @@ -0,0 +1,14 @@ +package auth + +import ( + "time" +) + +type ModelUser struct { + ID int `json:"id"` + Email string `json:"email"` + PasswordHash string `json:"-"` + Salt string `json:"-"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/exercise7/blogging-platform/internal/db/auth/register.go b/exercise7/blogging-platform/internal/db/auth/register.go new file mode 100644 index 00000000..a06852b5 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/auth/register.go @@ -0,0 +1,40 @@ +package auth + +import ( + "context" +) + +type RegisterInput struct { + User *ModelUser +} + +func (m *Auth) Register(ctx context.Context, inp *RegisterInput) (*ModelUser, error) { + log := m.logger.With("method", "Register") + + stmt := ` +INSERT INTO user_ (email, password_hash, salt) +VALUES ($1, $2, $3) +RETURNING id, email, password_hash, salt; +` + + row := m.db.QueryRowContext(ctx, stmt, inp.User.Email, inp.User.PasswordHash, inp.User.Salt) + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table user", "error", err) + return nil, err + } + + user := ModelUser{} + + if err := row.Scan( + &user.ID, + &user.Email, + &user.PasswordHash, + &user.Salt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan user", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success register user") + return &user, nil +} diff --git a/exercise7/blogging-platform/internal/db/blog/create_blog.go b/exercise7/blogging-platform/internal/db/blog/create_blog.go new file mode 100644 index 00000000..702af65d --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/create_blog.go @@ -0,0 +1,44 @@ +package blog + +import ( + "context" + "database/sql" + "errors" +) + +func (b *Blog) CreateBlog(ctx context.Context, insertData *ModelBlogs) (*ModelBlogs, error) { + log := b.logger.With("method", "CreateBlog") + + stmt := ` +INSERT INTO blogs (title, description) +VALUES ($1, $2) +RETURNING id, title, description, created_at, updated_at +` + + row := b.db.QueryRowContext(ctx, stmt, insertData.Title, insertData.Description) + + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to insert to table blogs", "error", err) + return nil, err + } + + blog := ModelBlogs{} + + if err := row.Scan( + &blog.ID, + &blog.Title, + &blog.Description, + &blog.CreatedAt, + &blog.UpdatedAt, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.ErrorContext(ctx, "no values was found", "error", err) + return nil, nil + } + log.ErrorContext(ctx, "fail to scan blog", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success insert to table blogs") + return &blog, nil +} diff --git a/exercise7/blogging-platform/internal/db/blog/delete_blog.go b/exercise7/blogging-platform/internal/db/blog/delete_blog.go new file mode 100644 index 00000000..423fe3f8 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/delete_blog.go @@ -0,0 +1,35 @@ +package blog + +import ( + "context" + "fmt" +) + +func (b *Blog) DeleteBlog(ctx context.Context, id int64) error { + log := b.logger.With("method", "DeleteBlog", "id", id) + + stmt := ` +DELETE FROM blogs +WHERE id = $1 +` + + res, err := b.db.ExecContext(ctx, stmt, id) + if err != nil { + log.ErrorContext(ctx, "fail to delete from the table blogs", "error", err) + return err + } + + num, err := res.RowsAffected() + if err != nil { + log.ErrorContext(ctx, "fail to delete from the table blogs", "error", err) + return err + } + + if num == 0 { + log.WarnContext(ctx, "blog with id was not found", "id", id) + return fmt.Errorf("blog with id was not found") + } + + log.InfoContext(ctx, "success delete from the table blogs") + return nil +} diff --git a/exercise7/blogging-platform/internal/db/blog/get_blog.go b/exercise7/blogging-platform/internal/db/blog/get_blog.go new file mode 100644 index 00000000..3d9cbd3a --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/get_blog.go @@ -0,0 +1,38 @@ +package blog + +import ( + "context" +) + +func (b *Blog) GetBlog(ctx context.Context, id int64) (*ModelBlogs, error) { + log := b.logger.With("method", "GetBlog") + + stmt := ` +SELECT id, title, description, created_at, updated_at +FROM blogs +WHERE id = $1 +` + + row := b.db.QueryRowContext(ctx, stmt, id) + + if err := row.Err(); err != nil { + log.ErrorContext(ctx, "fail to query table blogs", "error", err) + return nil, err + } + + blog := ModelBlogs{} + + if err := row.Scan( + &blog.ID, + &blog.Title, + &blog.Description, + &blog.CreatedAt, + &blog.UpdatedAt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan blogs", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success query table blogs") + return &blog, nil +} diff --git a/exercise7/blogging-platform/internal/db/blog/get_blogs.go b/exercise7/blogging-platform/internal/db/blog/get_blogs.go new file mode 100644 index 00000000..00de308c --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/get_blogs.go @@ -0,0 +1,46 @@ +package blog + +import "context" + +func (b *Blog) GetBlogs(ctx context.Context) ([]ModelBlogs, error) { + log := b.logger.With("db", "GetBlogs") + + blogs := make([]ModelBlogs, 0) + + stmt := ` +SELECT id, title, description, created_at, updated_at +FROM blogs +` + + rows, err := b.db.QueryContext(ctx, stmt) + if err != nil { + log.ErrorContext(ctx, "fail to query table blogs", "error", err) + return nil, err + } + defer rows.Close() + + for rows.Next() { + blog := ModelBlogs{} + + if err := rows.Scan( + &blog.ID, + &blog.Title, + &blog.Description, + &blog.CreatedAt, + &blog.UpdatedAt, + ); err != nil { + log.ErrorContext(ctx, "fail to scan blog", "error", err) + return nil, err + } + + blogs = append(blogs, blog) + } + + if err := rows.Err(); err != nil { + log.ErrorContext(ctx, "fail to scan rows", "error", err) + return nil, err + } + + log.InfoContext(ctx, "success query table blogs", "blogs") + return blogs, nil +} diff --git a/exercise7/blogging-platform/internal/db/blog/main.go b/exercise7/blogging-platform/internal/db/blog/main.go new file mode 100644 index 00000000..71d7f49d --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/main.go @@ -0,0 +1,18 @@ +package blog + +import ( + "database/sql" + "log/slog" +) + +type Blog struct { + logger *slog.Logger + db *sql.DB +} + +func New(sql *sql.DB, logger *slog.Logger) *Blog { + return &Blog{ + db: sql, + logger: logger, + } +} diff --git a/exercise7/blogging-platform/internal/db/blog/model.go b/exercise7/blogging-platform/internal/db/blog/model.go new file mode 100644 index 00000000..8187b39c --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/model.go @@ -0,0 +1,13 @@ +package blog + +import ( + "time" +) + +type ModelBlogs struct { + ID int `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/exercise7/blogging-platform/internal/db/blog/update_blog.go b/exercise7/blogging-platform/internal/db/blog/update_blog.go new file mode 100644 index 00000000..9dfddde2 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/blog/update_blog.go @@ -0,0 +1,36 @@ +package blog + +import ( + "context" + "fmt" +) + +func (b *Blog) UpdateBlog(ctx context.Context, id int64, insertData *ModelBlogs) error { + log := b.logger.With("method", "UpdateBlog", "id", id) + + stmt := ` +UPDATE blogs +SET title = $2, description = $3 +WHERE id = $1 +` + + res, err := b.db.ExecContext(ctx, stmt, id, insertData.Title, insertData.Description) + if err != nil { + log.ErrorContext(ctx, "fail to update the table blogs", "error", err) + return err + } + + num, err := res.RowsAffected() + if err != nil { + log.ErrorContext(ctx, "fail to update from the table blogs", "error", err) + return err + } + + if num == 0 { + log.WarnContext(ctx, "blog with id was not found", "id", id) + return fmt.Errorf("blog with id was not found") + } + + log.InfoContext(ctx, "success update the table blogs") + return nil +} diff --git a/exercise7/blogging-platform/internal/db/main.go b/exercise7/blogging-platform/internal/db/main.go new file mode 100644 index 00000000..dcf4406d --- /dev/null +++ b/exercise7/blogging-platform/internal/db/main.go @@ -0,0 +1,56 @@ +package db + +import ( + "database/sql" + "fmt" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/auth" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/db/blog" + "log/slog" + "os" + "strconv" + + _ "github.com/lib/pq" +) + +type DB struct { + logger *slog.Logger + pg *sql.DB + *blog.Blog + *auth.Auth +} + +func New(logger *slog.Logger) (*DB, error) { + pgsql, err := newPgSQL() + if err != nil { + return nil, err + } + + return &DB{ + logger: logger, + pg: pgsql, + Blog: blog.New(pgsql, logger), + Auth: auth.New(pgsql, logger), + }, nil +} + +func newPgSQL() (*sql.DB, error) { + host := os.Getenv("DB_HOST") + port, err := strconv.Atoi(os.Getenv("DB_PORT")) + if err != nil { + return nil, err + } + user := os.Getenv("DB_USER") + password := os.Getenv("DB_PASSWORD") + dbname := os.Getenv("DB_NAME") + + psqlInfo := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname, + ) + db, err := sql.Open("postgres", psqlInfo) + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/exercise7/blogging-platform/internal/db/migrations/20250127143958_create_blogs_table.down.sql b/exercise7/blogging-platform/internal/db/migrations/20250127143958_create_blogs_table.down.sql new file mode 100644 index 00000000..f9c4cc46 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/migrations/20250127143958_create_blogs_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS blogs diff --git a/exercise7/blogging-platform/internal/db/migrations/20250127143958_create_blogs_table.up.sql b/exercise7/blogging-platform/internal/db/migrations/20250127143958_create_blogs_table.up.sql new file mode 100644 index 00000000..be497c3a --- /dev/null +++ b/exercise7/blogging-platform/internal/db/migrations/20250127143958_create_blogs_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS blogs ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +) diff --git a/exercise7/blogging-platform/internal/db/migrations/20250207050918_create_table_user.down.sql b/exercise7/blogging-platform/internal/db/migrations/20250207050918_create_table_user.down.sql new file mode 100644 index 00000000..7643471e --- /dev/null +++ b/exercise7/blogging-platform/internal/db/migrations/20250207050918_create_table_user.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS user_ diff --git a/exercise7/blogging-platform/internal/db/migrations/20250207050918_create_table_user.up.sql b/exercise7/blogging-platform/internal/db/migrations/20250207050918_create_table_user.up.sql new file mode 100644 index 00000000..34d14996 --- /dev/null +++ b/exercise7/blogging-platform/internal/db/migrations/20250207050918_create_table_user.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS user_ ( + id SERIAL PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + salt TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +) diff --git a/exercise7/blogging-platform/main.go b/exercise7/blogging-platform/main.go index 1ffa1477..2f119885 100644 --- a/exercise7/blogging-platform/main.go +++ b/exercise7/blogging-platform/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "fmt" + "github.com/joho/godotenv" "log/slog" "os" "os/signal" @@ -13,8 +15,10 @@ import ( func main() { ctx, cancel := context.WithCancel(context.Background()) + _ = godotenv.Load() + // db - _, err := db.New() + d, err := db.New(slog.With("service", "db")) if err != nil { slog.ErrorContext( ctx, @@ -26,24 +30,26 @@ func main() { } // api - a := api.New() - if err := a.Start(ctx); err != nil { - slog.ErrorContext( - ctx, - "initialize service error", - "service", "api", - "error", err, - ) - panic(err) - } + a := api.New(slog.With("service", "api"), d) + go func(ctx context.Context, cancelFunc context.CancelFunc) { + if err := a.Start(ctx); err != nil { + slog.ErrorContext(ctx, "failed to start api", "error", err.Error()) + } + + cancelFunc() + }(ctx, cancel) - go func() { + go func(cancelFunc context.CancelFunc) { shutdown := make(chan os.Signal, 1) // Create channel to signify s signal being sent signal.Notify(shutdown, os.Interrupt) // When an interrupt is sent, notify the channel sig := <-shutdown slog.WarnContext(ctx, "signal received - shutting down...", "signal", sig) - cancel() - }() + cancelFunc() + }(cancel) + + <-ctx.Done() + + fmt.Println("shutting down gracefully") } From c4a56c741f4bb22623d0b814d8e693b42248163f Mon Sep 17 00:00:00 2001 From: "sh.kairatuly" Date: Mon, 10 Feb 2025 11:12:25 +0500 Subject: [PATCH 8/8] Shynggyskhan.Kairatuly exercise 7 --- exercise7/blogging-platform/.dockerignore | 33 +++++++++ exercise7/blogging-platform/Dockerfile | 78 ++++++++++++++++++++ exercise7/blogging-platform/README.Docker.md | 22 ++++++ exercise7/blogging-platform/compose.yaml | 70 ++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 exercise7/blogging-platform/.dockerignore create mode 100644 exercise7/blogging-platform/Dockerfile create mode 100644 exercise7/blogging-platform/README.Docker.md create mode 100644 exercise7/blogging-platform/compose.yaml diff --git a/exercise7/blogging-platform/.dockerignore b/exercise7/blogging-platform/.dockerignore new file mode 100644 index 00000000..def5394b --- /dev/null +++ b/exercise7/blogging-platform/.dockerignore @@ -0,0 +1,33 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +/db-data diff --git a/exercise7/blogging-platform/Dockerfile b/exercise7/blogging-platform/Dockerfile new file mode 100644 index 00000000..ed4e193c --- /dev/null +++ b/exercise7/blogging-platform/Dockerfile @@ -0,0 +1,78 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +################################################################################ +# Create a stage for building the application. +ARG GO_VERSION=1.23 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build +WORKDIR /src + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. +# Leverage bind mounts to go.sum and go.mod to avoid having to copy them into +# the container. +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,source=go.sum,target=go.sum \ + --mount=type=bind,source=go.mod,target=go.mod \ + go mod download -x + +# This is the architecture you're building for, which is passed in by the builder. +# Placing it here allows the previous steps to be cached across architectures. +ARG TARGETARCH + +# Build the application. +# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds. +# Leverage a bind mount to the current directory to avoid having to copy the +# source code into the container. +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,target=. \ + CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server . + +################################################################################ +# Create a new stage for running the application that contains the minimal +# runtime dependencies for the application. This often uses a different base +# image from the build stage where the necessary files are copied from the build +# stage. +# +# The example below uses the alpine image as the foundation for running the app. +# By specifying the "latest" tag, it will also use whatever happens to be the +# most recent version of that image when you build your Dockerfile. If +# reproducability is important, consider using a versioned tag +# (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff). +FROM alpine:latest AS final + +# Install any runtime dependencies that are needed to run your application. +# Leverage a cache mount to /var/cache/apk/ to speed up subsequent builds. +RUN --mount=type=cache,target=/var/cache/apk \ + apk --update add \ + ca-certificates \ + tzdata \ + && \ + update-ca-certificates + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser +USER appuser + +# Copy the executable from the "build" stage. +COPY --from=build /bin/server /bin/ + +# Expose the port that the application listens on. +EXPOSE 80 + +# What the container should run when it is started. +ENTRYPOINT [ "/bin/server" ] diff --git a/exercise7/blogging-platform/README.Docker.md b/exercise7/blogging-platform/README.Docker.md new file mode 100644 index 00000000..3b643020 --- /dev/null +++ b/exercise7/blogging-platform/README.Docker.md @@ -0,0 +1,22 @@ +### Building and running your application + +When you're ready, start your application by running: +`docker compose up --build`. + +Your application will be available at http://localhost:80. + +### Deploying your application to the cloud + +First, build your image, e.g.: `docker build -t myapp .`. +If your cloud uses a different CPU architecture than your development +machine (e.g., you are on a Mac M1 and your cloud provider is amd64), +you'll want to build the image for that platform, e.g.: +`docker build --platform=linux/amd64 -t myapp .`. + +Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. + +Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) +docs for more detail on building and pushing. + +### References +* [Docker's Go guide](https://docs.docker.com/language/golang/) \ No newline at end of file diff --git a/exercise7/blogging-platform/compose.yaml b/exercise7/blogging-platform/compose.yaml new file mode 100644 index 00000000..6e16e568 --- /dev/null +++ b/exercise7/blogging-platform/compose.yaml @@ -0,0 +1,70 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "server". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: +# server: +# build: +# context: . +# target: final +# environment: +# - API_PORT=80 +# - DB_HOST=db +# - DB_PORT=5432 +# - DB_NAME=$DB_NAME +# - DB_USER=$DB_USER +# - DB_PASSWORD=$DB_PASSWORD +# ports: +# - ${API_PORT}:80 +# depends_on: +# db: +# condition: service_healthy + + db: + image: postgres + restart: always + volumes: + - ./db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=$DB_NAME + - POSTGRES_USER=$DB_USER + - POSTGRES_PASSWORD=$DB_PASSWORD + ports: + - ${DB_PORT}:5432 + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + + migrate: + image: migrate/migrate + profiles: [ "tools" ] + entrypoint: [ + "migrate", + "-path", + "./migrations", + "-database", + "postgres://$DB_USER:$DB_PASSWORD@db:5432/$DB_NAME?sslmode=disable" + ] + volumes: + - ./internal/db/migrations:/migrations + depends_on: + db: + condition: service_healthy + +volumes: + db-data: + +# The commented out section below is an example of how to define a PostgreSQL +# database that your application can use. `depends_on` tells Docker Compose to +# start the database before your application. The `db-data` volume persists the +# database data between container restarts. The `db-password` secret is used +# to set the database password. You must create `db/password.txt` and add +# a password of your choosing to it before running `docker compose up`. +