initial commit

This commit is contained in:
2026-04-02 01:55:44 -04:00
commit e905f1b003
7 changed files with 343 additions and 0 deletions

37
Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# ---- Build Stage ----
FROM golang:1.26 AS builder
WORKDIR /app
# ---- Step 1: Copy go.mod/go.sum for caching ----
# Copy only the minimal files first for layer caching
COPY libshared/go.mod libshared/go.sum ./libshared/
COPY account-manager/go.mod account-manager/go.sum ./account-manager/
# ---- Step 2: Download dependencies ----
RUN go -C libshared mod download
RUN go -C account-manager mod download
# ---- Step 3: Copy full source code ----
COPY libshared ./libshared
COPY account-manager ./account-manager
# ---- Step 4: Create Go workspace inside container ----
RUN go work init ./account-manager ./libshared
## Optional: verify workspace
#RUN go work list
# ---- Step 5: Build binary ----
WORKDIR /app/account-manager
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server
# ---- Runtime Stage ----
FROM gcr.io/distroless/base-debian12
WORKDIR /app
COPY --from=builder /app/account-manager/server .
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app/server"]

71
crypto.go Normal file
View File

@@ -0,0 +1,71 @@
package main
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
const (
memory = 64 * 1024 // 64 MB
iterations = 3
parallelism = 2
saltLength = 16
keyLength = 32
)
func generateSalt() ([]byte, error) {
salt := make([]byte, saltLength)
_, err := rand.Read(salt)
return salt, err
}
func verifyPassword(password, encodedHash string) bool {
parts := strings.Split(encodedHash, "$")
params := parts[3]
salt := parts[4]
hash := parts[5]
var memory uint32
var iterations uint32
var parallelism uint8
fmt.Sscanf(params, "m=%d,t=%d,p=%d", &memory, &iterations, &parallelism)
saltBytes, _ := base64.RawStdEncoding.DecodeString(salt)
hashBytes, _ := base64.RawStdEncoding.DecodeString(hash)
comparisonHash := argon2.IDKey(
[]byte(password),
saltBytes,
iterations,
memory,
parallelism,
uint32(len(hashBytes)),
)
return subtle.ConstantTimeCompare(hashBytes, comparisonHash) == 1
}
func hashPassword(password string, salt []byte) string {
hash := argon2.IDKey(
[]byte(password),
salt,
iterations,
memory,
parallelism,
keyLength,
)
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
memory, iterations, parallelism, b64Salt, b64Hash)
}

17
go.mod Normal file
View File

@@ -0,0 +1,17 @@
module pcloud/new-account
go 1.25.0
require (
github.com/jackc/pgx/v5 v5.8.0
golang.org/x/crypto v0.49.0
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
)

30
go.sum Normal file
View File

@@ -0,0 +1,30 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

77
main.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
"crypto/rand"
"errors"
"libshared"
"log"
"math/big"
"net/http"
"regexp"
"strings"
)
type NewAccountRequest struct {
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Address string `json:"address"`
CountryCode string `json:"country_code"`
Password string `json:"password"`
}
type APIResponse struct {
Success bool `json:"success"`
AccountID int64 `json:"account_id,omitempty"`
Error string `json:"error,omitempty"`
}
func generateSecureNumber(digits int) (int64, error) {
upperBound := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(digits)), nil)
n, err := rand.Int(rand.Reader, upperBound)
if err != nil {
return 0, err
}
return n.Int64(), nil
}
func validateRequest(req *NewAccountRequest) error {
if strings.TrimSpace(req.Email) == "" {
return errors.New("email is required")
}
if strings.TrimSpace(req.FirstName) == "" {
return errors.New("first_name is required")
}
if strings.TrimSpace(req.LastName) == "" {
return errors.New("last_name is required")
}
if strings.TrimSpace(req.Address) == "" {
return errors.New("address is required")
}
if strings.TrimSpace(req.CountryCode) == "" {
return errors.New("country_code is required")
}
// Basic email validation
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(req.Email) {
return errors.New("invalid email format")
}
// Optional: enforce ISO country code length (2 letters)
if len(req.CountryCode) != 2 {
return errors.New("country_code must be 2 characters (ISO code)")
}
return nil
}
func main() {
libshared.Pool = libshared.GetDbPool()
http.HandleFunc("/account/new", accountNew)
log.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

97
new-account.go Normal file
View File

@@ -0,0 +1,97 @@
package main
import (
"context"
"encoding/json"
"libshared"
"log"
"net/http"
)
func accountNew(w http.ResponseWriter, r *http.Request) {
var accountID int64
var err error
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req NewAccountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
apiresponse := libshared.NewAPIResponse("fail", "Invalid JSON request", APIResponse{})
json.NewEncoder(w).Encode(apiresponse)
return
}
if err := validateRequest(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
apiresponse := libshared.NewAPIResponse("fail", err.Error(), APIResponse{})
json.NewEncoder(w).Encode(apiresponse)
return
}
for {
accountID, err = generateSecureNumber(13)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
apiresponse := libshared.NewAPIResponse("fail", "failed to generate account ID", APIResponse{})
json.NewEncoder(w).Encode(apiresponse)
return
}
accountExist := libshared.Pool.QueryRow(context.Background(), "SELECT FROM accounts WHERE accountid = $1", accountID)
err = accountExist.Scan()
if err != nil {
break
}
}
_, err = libshared.Pool.Exec(
context.Background(),
"INSERT INTO accounts (accountID, email, first_name, last_name, address, country_code) VALUES ($1, $2, $3, $4, $5, $6)",
accountID, req.Email, req.FirstName, req.LastName, req.Address, req.CountryCode)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
apiresponse := libshared.NewAPIResponse("fail", "failed to create account", APIResponse{})
json.NewEncoder(w).Encode(apiresponse)
return
}
salt, _ := generateSalt()
hashText := hashPassword(req.Password, salt)
_, err = libshared.Pool.Exec(context.Background(),
"INSERT INTO identities (accountid, provider, provider_user_id, password_hash) VALUES ($1, $2, $3, $4)",
accountID, "local", "root", hashText)
if err != nil {
log.Println("Failed to create root identity for new account:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = libshared.Pool.Exec(context.Background(),
"INSERT INTO roles (accountid, rolename, description) VALUES ($1, $2, $3)",
accountID, "admin", "Administrative Role with full permissions")
if err != nil {
log.Println("Failed to create administrative role:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Println("Just Created new root account")
log.Println(accountID, "local", "root", hashText)
w.Header().Set("Content-Type", "application/json")
apiresponse := libshared.NewAPIResponse("success", "Account created successfully", APIResponse{
Success: true,
AccountID: accountID,
})
json.NewEncoder(w).Encode(apiresponse)
log.Println("New account created with ID:", accountID, req.Email, req.FirstName, req.LastName, req.Address, req.CountryCode)
}

14
notes.md Normal file
View File

@@ -0,0 +1,14 @@
## Example query
```
curl -X POST http://localhost:8080/account/new \
-H "Content-Type: application/json" \
-d '{
"email":"john@example.com",
"first_name":"John",
"last_name":"Smith",
"address":"123 Main St",
"country_code":"US",
"password":"letmein"
}'
```