first commit
This commit is contained in:
commit
eef066b179
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode
|
||||
|
||||
### Go ###
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
*.code-workspace
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM golang:1.16.5 as builder
|
||||
|
||||
WORKDIR /go-microservice/
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 go build -o microservice /go-microservice/main.go
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /go-microservice
|
||||
|
||||
COPY --from=builder /go-microservice/ /go-microservice/
|
||||
|
||||
EXPOSE 9090
|
||||
|
||||
CMD ./microservice
|
16
README.md
Normal file
16
README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Product Catalog API
|
||||
|
||||
Blogs for Detailed Tutorial:
|
||||
|
||||
* <a href="https://learnai1.home.blog/2021/03/15/microservices-in-go/">Introductory Blog.</a>
|
||||
* <a href="https://learnai1.home.blog/2021/03/18/microservices-in-go-part-2/">Adding More API's.</a>
|
||||
* <a href="https://learnai1.home.blog/2021/06/27/authentication-in-go-microservices/">Basic Authentication in Microservices.</a>
|
||||
* <a href="https://learnai1.home.blog/2021/07/08/microservices-in-go-part-iv-docker-and-go-microservices/">Docker and Go Microservices.</a>
|
||||
* <a href="https://learnai1.home.blog/2021/08/05/https-server-in-go/">HTTPS Server in Go.</a>
|
||||
## Run API
|
||||
|
||||
``` bash
|
||||
go run .\main.go
|
||||
```
|
||||
|
||||
Use the above command to run API on your local machine on port 9090.
|
30
data/data.json
Normal file
30
data/data.json
Normal file
@ -0,0 +1,30 @@
|
||||
[
|
||||
{
|
||||
"id": "e7f31219-7986-11eb-bd65-98fa9b64e75b",
|
||||
"name": "iPhone 12 Pro",
|
||||
"description": "Pro variant of iPhone 12",
|
||||
"price": 100000,
|
||||
"isAvailable": false
|
||||
},
|
||||
{
|
||||
"id": "f82fa5ec-7986-11eb-a8e3-98fa9b64e75b",
|
||||
"name": "iPhone 12 Pro Max",
|
||||
"description": "Pro max variant of iPhone 12",
|
||||
"price": 120000,
|
||||
"isAvailable": true
|
||||
},
|
||||
{
|
||||
"id": "e7f35219-7986-11eb-bd65-98fa9b64e75b",
|
||||
"name": "iPhone 12 Mini",
|
||||
"description": "Mini variant of iPhone 12",
|
||||
"price": 60000,
|
||||
"isAvailable": true
|
||||
},
|
||||
{
|
||||
"id": "e7f35219-8986-11eb-bd65-98fa9b64e75b",
|
||||
"name": "iPhone 12",
|
||||
"description": "Base variant of iPhone 12",
|
||||
"price": 78000,
|
||||
"isAvailable": true
|
||||
}
|
||||
]
|
123
entity/product.go
Normal file
123
entity/product.go
Normal file
@ -0,0 +1,123 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
//Product defines a structure for an item in product catalog
|
||||
type Product struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Price float64 `json:"price"`
|
||||
IsAvailable bool `json:"isAvailable"`
|
||||
}
|
||||
|
||||
// ErrNoProduct is used if no product found
|
||||
var ErrNoProduct = errors.New("no product found")
|
||||
|
||||
// GetProducts returns the JSON file content if available else returns an error.
|
||||
func GetProducts() ([]byte, error) {
|
||||
// Read JSON file
|
||||
data, err := ioutil.ReadFile("./data/data.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetProduct takes id as input and returns the corresponding product, else it returns ErrNoProduct error.
|
||||
func GetProduct(id string) (Product, error) {
|
||||
// Read JSON file
|
||||
data, err := ioutil.ReadFile("./data/data.json")
|
||||
if err != nil {
|
||||
return Product{}, err
|
||||
}
|
||||
// read products
|
||||
var products []Product
|
||||
err = json.Unmarshal(data, &products)
|
||||
if err != nil {
|
||||
return Product{}, err
|
||||
}
|
||||
// iterate through product array
|
||||
for i := 0; i < len(products); i++ {
|
||||
// if we find one product with the given ID
|
||||
if products[i].ID == id {
|
||||
// return product
|
||||
return products[i], nil
|
||||
}
|
||||
}
|
||||
return Product{}, ErrNoProduct
|
||||
}
|
||||
|
||||
// DeleteProduct takes id as input and deletes the corresponding product, else it returns ErrNoProduct error.
|
||||
func DeleteProduct(id string) error {
|
||||
// Read JSON file
|
||||
data, err := ioutil.ReadFile("./data/data.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// read products
|
||||
var products []Product
|
||||
err = json.Unmarshal(data, &products)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// iterate through product array
|
||||
for i := 0; i < len(products); i++ {
|
||||
// if we find one product with the given ID
|
||||
if products[i].ID == id {
|
||||
products = removeElement(products, i)
|
||||
// Write Updated JSON file
|
||||
updatedData, err := json.Marshal(products)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile("./data/data.json", updatedData, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrNoProduct
|
||||
}
|
||||
|
||||
// AddProduct adds an input product to the product list in JSON document.
|
||||
func AddProduct(product Product) error {
|
||||
// Load existing products and append the data to product list
|
||||
var products []Product
|
||||
data, err := ioutil.ReadFile("./data/data.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Load our JSON file to memory using array of products
|
||||
err = json.Unmarshal(data, &products)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add new Product to our list
|
||||
products = append(products, product)
|
||||
|
||||
// Write Updated JSON file
|
||||
updatedData, err := json.Marshal(products)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile("./data/data.json", updatedData, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeElement is used to remove element from product array at given index
|
||||
func removeElement(arr []Product, index int) []Product {
|
||||
ret := make([]Product, 0)
|
||||
ret = append(ret, arr[:index]...)
|
||||
return append(ret, arr[index+1:]...)
|
||||
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/HelloWorld/goProductAPI
|
||||
|
||||
go 1.16
|
||||
|
||||
require github.com/gorilla/mux v1.8.0
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
166
handlers/handlers.go
Normal file
166
handlers/handlers.go
Normal file
@ -0,0 +1,166 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/HelloWorld/goProductAPI/entity"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// GetProductsHandler is used to get data inside the products defined on our product catalog
|
||||
func GetProductsHandler() http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
data, err := entity.GetProducts()
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Write the body with JSON data
|
||||
rw.Header().Add("content-type", "application/json")
|
||||
rw.WriteHeader(http.StatusFound)
|
||||
rw.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
// GetProductHandler is used to get data inside the products defined on our product catalog
|
||||
func GetProductHandler() http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Read product ID
|
||||
productID := mux.Vars(r)["id"]
|
||||
product, err := entity.GetProduct(productID)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
responseData, err := json.Marshal(product)
|
||||
if err != nil {
|
||||
// Check if it is No product error or any other error
|
||||
if errors.Is(err, entity.ErrNoProduct) {
|
||||
// Write Header if no related product found.
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Write body with found product
|
||||
rw.Header().Add("content-type", "application/json")
|
||||
rw.WriteHeader(http.StatusFound)
|
||||
rw.Write(responseData)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateProductHandler is used to create a new product and add to our product store.
|
||||
func CreateProductHandler() http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Read incoming JSON from request body
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
// If no body is associated return with StatusBadRequest
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check if data is proper JSON (data validation)
|
||||
var product entity.Product
|
||||
err = json.Unmarshal(data, &product)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusExpectationFailed)
|
||||
rw.Write([]byte("Invalid Data Format"))
|
||||
return
|
||||
}
|
||||
err = entity.AddProduct(product)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// return after writing Body
|
||||
rw.WriteHeader(http.StatusCreated)
|
||||
rw.Write([]byte("Added New Product"))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteProductHandler deletes the product with given ID.
|
||||
func DeleteProductHandler() http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Read product ID
|
||||
productID := mux.Vars(r)["id"]
|
||||
err := entity.DeleteProduct(productID)
|
||||
if err != nil {
|
||||
// Check if it is No product error or any other error
|
||||
if errors.Is(err, entity.ErrNoProduct) {
|
||||
// Write Header if no related product found.
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Write Header with Accepted Status (done operation)
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateProductHandler updates the product with given ID.
|
||||
func UpdateProductHandler() http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Read product ID
|
||||
productID := mux.Vars(r)["id"]
|
||||
err := entity.DeleteProduct(productID)
|
||||
if err != nil {
|
||||
if errors.Is(err, entity.ErrNoProduct) {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Read incoming JSON from request body
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
// If no body is associated return with StatusBadRequest
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check if data is proper JSON (data validation)
|
||||
var product entity.Product
|
||||
err = json.Unmarshal(data, &product)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusExpectationFailed)
|
||||
rw.Write([]byte("Invalid Data Format"))
|
||||
return
|
||||
}
|
||||
// Addproduct with the requested body
|
||||
err = entity.AddProduct(product)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Write Header if no related product found.
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthHandler(h http.Handler) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
username := sha256.Sum256([]byte(os.Getenv("USER_NAME")))
|
||||
password := sha256.Sum256([]byte(os.Getenv("USER_PASS")))
|
||||
userHash := sha256.Sum256([]byte(user))
|
||||
passHash := sha256.Sum256([]byte(pass))
|
||||
validUser := subtle.ConstantTimeCompare(userHash[:],username[:]) == 1
|
||||
validPass := subtle.ConstantTimeCompare(passHash[:],password[:]) == 1
|
||||
if validPass && validUser{
|
||||
h.ServeHTTP(rw,r)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Error(rw, "No/Invalid Credentials", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
32
main.go
Normal file
32
main.go
Normal file
@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/HelloWorld/goProductAPI/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create new Router
|
||||
router := mux.NewRouter()
|
||||
// route properly to respective handlers
|
||||
router.Handle("/products", handlers.GetProductsHandler()).Methods("GET")
|
||||
router.Handle("/products", handlers.CreateProductHandler()).Methods("POST")
|
||||
router.Handle("/products/{id}", handlers.GetProductHandler()).Methods("GET")
|
||||
router.Handle("/products/{id}", handlers.DeleteProductHandler()).Methods("DELETE")
|
||||
router.Handle("/products/{id}", handlers.UpdateProductHandler()).Methods("PUT")
|
||||
|
||||
// Create new server and assign the router
|
||||
server := http.Server{
|
||||
Addr: ":9090",
|
||||
Handler: handlers.AuthHandler(router),
|
||||
}
|
||||
fmt.Println("Staring Product Catalog server on Port 9090")
|
||||
// Start Server on defined port/host.
|
||||
err := server.ListenAndServeTLS("server.crt", "server.key")
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to start HTTPS server: %s", err.Error())
|
||||
}
|
||||
}
|
22
server.crt
Normal file
22
server.crt
Normal file
@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDtTCCAp2gAwIBAgIUDQgu9+HrJrc7TEelsB1TYp+zhPkwDQYJKoZIhvcNAQEL
|
||||
BQAwajELMAkGA1UEBhMCSU4xDjAMBgNVBAgMBURlbGhpMRIwEAYDVQQHDAlOZXcg
|
||||
RGVsaGkxFDASBgNVBAoMC0hlbGxvIFdvcmxkMQ0wCwYDVQQLDARCbG9nMRIwEAYD
|
||||
VQQDDAlsb2NhbGhvc3QwHhcNMjEwODA1MDMzNDQ0WhcNMzEwODAzMDMzNDQ0WjBq
|
||||
MQswCQYDVQQGEwJJTjEOMAwGA1UECAwFRGVsaGkxEjAQBgNVBAcMCU5ldyBEZWxo
|
||||
aTEUMBIGA1UECgwLSGVsbG8gV29ybGQxDTALBgNVBAsMBEJsb2cxEjAQBgNVBAMM
|
||||
CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL8emPJ1
|
||||
tw0TrfJZQeLzHO2x0O36UetzeJKdf//IKt3P89B+2GGL6DzPdntTBM9J4PAzLzXe
|
||||
uR8uL53XDWdfvhCd9oMYsgJ4L1XE6q+oZA6zWGC4voUwUOJgpJyXt8fdDdn1G6X/
|
||||
7Lyjw4u6KOkrFcwIPaM+MxgZGwICi1JDj7sYCOEw1gWsLrgD4XpN0Fky9QH1Uue+
|
||||
sxCZrrXFxiSXt0Xq51sT5BXrCkPhRrHUyhBay+VtVa+Lr2Yq+UfT3VUWERgcoJkx
|
||||
YluYXMbV4YYZ53bbgrW+29UT+NKNKTF2dFJazZ4r5OzZAD3ihikvoCIZUaVtgOpf
|
||||
fctWqG5bKmVpSaMCAwEAAaNTMFEwHQYDVR0OBBYEFPiCPV5yetrvDF5caB0t4XWx
|
||||
S+NeMB8GA1UdIwQYMBaAFPiCPV5yetrvDF5caB0t4XWxS+NeMA8GA1UdEwEB/wQF
|
||||
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBALiBoaqc9KxscNuU2mXTfXD4kv1bLy3C
|
||||
zzGn3bBHMQuRfwkclkEZG0Cbmu+k30DtMhUt9DREM6fVhClUlR6my/0vC/EgUIm2
|
||||
yYOF/dBIySH/JkZz0qRst5NvicwPaqUQoyIIzk93Da8eE0uGHW7eV4tsAVkB0RHT
|
||||
i4/EkGKZ/vJKyj/RI4zWvMCEFZcGtDtKf4rYzmcA3t0P1gbWt2+Gkhz1/+i52oIr
|
||||
w2M2CQ9qpgv3jvwCfOpZVQDRMZnLVpP/s8KyOwnVu+n8c/z+zJe+U3x5Sl0eamDA
|
||||
7CQ6RcorzWWzAhAQkrH0qWnF+BfF7GJ8VoLku0dnTzYsAQ5L4XaYf/I=
|
||||
-----END CERTIFICATE-----
|
27
server.key
Normal file
27
server.key
Normal file
@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAvx6Y8nW3DROt8llB4vMc7bHQ7fpR63N4kp1//8gq3c/z0H7Y
|
||||
YYvoPM92e1MEz0ng8DMvNd65Hy4vndcNZ1++EJ32gxiyAngvVcTqr6hkDrNYYLi+
|
||||
hTBQ4mCknJe3x90N2fUbpf/svKPDi7oo6SsVzAg9oz4zGBkbAgKLUkOPuxgI4TDW
|
||||
BawuuAPhek3QWTL1AfVS576zEJmutcXGJJe3RernWxPkFesKQ+FGsdTKEFrL5W1V
|
||||
r4uvZir5R9PdVRYRGBygmTFiW5hcxtXhhhnndtuCtb7b1RP40o0pMXZ0UlrNnivk
|
||||
7NkAPeKGKS+gIhlRpW2A6l99y1aoblsqZWlJowIDAQABAoIBAQClfS0a5Ws3246H
|
||||
h1pR1gl6mLo9Fr/QjRAehFrNdNoJb4PDSdK7xJW38jy51M0ZYPNxiiCbGNxbb3az
|
||||
yf9FP9YoNV+7bKrXEJKMRhKhP8JEKG+icNYoJgoju2NOZOEyIutXi7IBL3Yicftl
|
||||
BjFelXwuTARzUeyUNUj5mJJjDTVr3oi7A/HyoEn66PmWSGgNB618tYxA55PwRgJO
|
||||
hd+OakpW90kLb3RsySts9cpIh1eEJLTKafTR4uY7SrAvMjFa9Zn4MpRYXMEq43DC
|
||||
382dlqYBPXMPN4YJEKZ5CmxFMaZ3a1Nr0G60RyQqwmB/fRWg+OZUOjjFOVllnOOY
|
||||
5jnSIy9BAoGBAONwsIpaze2TeM8Ij4NxBqwRAh/2zb5t0okD/zbhf2sPyrEP8+hI
|
||||
egG10kQh7t0MhRklSOmv3NfDxr998XfBjswXdnsoIvP+BmlLG01zRh/49cdBnOty
|
||||
Za/zu5msyYspopPrDjwF+RtMHOXpXNuzFhxMYWdfOBO7/lOSka8ABFqZAoGBANce
|
||||
VzQW6OFLAE5fwwkf5W4cjOGaSpkKq6KwBkQayvRrGQMf3pu7Q2pdlcoJIhGOy8Bc
|
||||
N2iI7qE4P52MmAg51TyIDcrhTrMLkoMXLmeyBC1M6JT6t1yGTpPkf6ZdVFAZZEAp
|
||||
3l2dv8pyPRpr5juE8YcabgF5deo3zxieB6bYlkebAoGAS63goHjsksQCa+luT49Z
|
||||
aAHU0iv+dAH5DyxsTKemDUrY6CflwgHzzwPgLlmYMKeM1jwo0dF5y7XSOT/ADFg0
|
||||
msan3v0Q/F0nZvvd3tyfld3yclXr0BBls7GHV/A9s/erqEqLlv9pz2J5LyuCgXxK
|
||||
vCnSM2Jkt3RTgR2BKlj4GekCgYAxUxilrfcZ6WuZjOWYiwK9W7iF5i3ip4qxU/Er
|
||||
3oTYxFHI4J7XUHnlwq2c1LlGE1rusXZW9sbYmqAjjOAzSqd1KLEY6s5zyVx/yGnw
|
||||
huXkSTUvK8mtYnJUANmwGMhDUX8mIzOEfa5DSixuiX0R+qqy0sGUfvgli0RmHZ4d
|
||||
iJ30rwKBgDuW1L0DJnH2n8e3uhJF8W1rKDynQdzGdPc4Bl8oSfvDMpIGz1cTecKV
|
||||
+30AWz1ui5u1qlg7gsjnt6Z7g3ztXfHNP1gHws5lC8YsjAEOyZ2f6JHOGAmHOsRb
|
||||
G1T9WS4wq4N1DJVJ1aQcgvF1BuejzUwyWTABgpbSQzCIaUXAeX9I
|
||||
-----END RSA PRIVATE KEY-----
|
Loading…
x
Reference in New Issue
Block a user