Squashed commit of the following:
ci.vdhsn.com/push Build is failing Details

commit e891ada9e8
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri Jul 7 20:10:40 2023 -0500

    added login and checking of roles to admin page

commit abb8e565cd
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri Jul 7 18:23:24 2023 -0500

    analytics + login ux updates

commit 4ab08d20b8
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat Jul 1 03:07:36 2023 -0500

    grpc reflection

commit fe329d2336
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat Jul 1 03:07:22 2023 -0500

    tests for auth

commit 043f387224
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Tue Jun 20 09:43:03 2023 -0500

    first pass at login/logout and sign up form

commit 5a191a2c72
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Mon Jun 19 10:30:24 2023 -0500

    implement auth

commit 649bcefbef
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sun Jun 18 18:29:20 2023 -0500

    added login control to page

commit 4227fc048a
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Wed Jun 14 19:10:54 2023 -0500

    early pass at auth service
feat/swagger
Adam Veldhousen 11 months ago
parent 898ec6ec3d
commit ad7d811c35
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

@ -144,14 +144,18 @@ k8s_resource(
)
bh_backend_service(service="runner", migrateDB=True, port_forwards=[
bh_backend_service(service="auth", migrateDB=True, port_forwards=[
port_forward(2345, name='Delve port')
])
bh_backend_service(service="catalog", migrateDB=True, port_forwards=[
bh_backend_service(service="runner", migrateDB=True, port_forwards=[
port_forward(2346, 2345, name='Delve port')
])
bh_backend_service(service="catalog", migrateDB=True, port_forwards=[
port_forward(2347, 2345, name='Delve port')
])
bh_backend_service(service="proxy-admin", port_forwards=[
port_forward(8082, 80, name="HTTP API @ localhost:8082")
], deps=['ingress'])
@ -160,5 +164,5 @@ bh_backend_service(service="proxy-web", port_forwards=[
port_forward(8081, 80, name="HTTP API @ localhost:8081")
], deps=['ingress'])
bh_client(service='web')
bh_client(service='admin')
bh_client(service='web', deps=["proxy-web-local"])
bh_client(service='admin', deps=["proxy-admin-local"])

@ -0,0 +1,71 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
spec:
replicas: 1
selector:
matchLabels:
service: auth
template:
metadata:
labels:
service: auth
spec:
serviceAccountName: barretthousen-service
containers:
- name: auth
image: barretthousen/service-auth:latest
imagePullPolicy: Always
ports:
- containerPort: 5001
name: grpc
command:
- /opt/auth
args:
- -migrate
resources:
limits:
cpu: "250m"
memory: "128Mi"
volumeMounts:
- mountPath: /config/
name: auth-config
volumes:
- name: auth-config
secret:
secretName: auth-config
---
apiVersion: v1
kind: Service
metadata:
name: auth
spec:
selector:
service: auth
ports:
- port: 5001
targetPort: 5001
---
apiVersion: v1
kind: Secret
metadata:
name: auth-config
stringData:
config.yaml: |
log_level: 2
port: 5001
db_service:
scheme: postgres
port: 5432
host: bh-db
name: bh
user: auth-service
password: auth-service
db_migrate:
scheme: postgres
port: 5432
host: bh-db
name: bh
user: postgres
password: bh-admin

@ -1,6 +1,7 @@
resources:
- ./image-pull-secret.yaml
- ./namespace.yaml
- ./auth-deployment.yaml
- ./catalog-deployment.yaml
- ./runner-deployment.yaml
- ./proxy-admin-deployment.yaml

@ -54,3 +54,4 @@ stringData:
port: 80
endpoints:
runner: runner:5001
auth: auth:5001

@ -54,3 +54,4 @@ stringData:
port: 80
endpoints:
catalog: catalog:5001
auth: auth:5001

@ -9,3 +9,4 @@ stringData:
access_control_allow_origin: "beta.admin.barretthousen.com"
endpoints:
runner: runner-beta:5001
auth: auth-beta:5001

@ -0,0 +1,11 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
spec:
template:
spec:
containers:
- name: auth
ports:
- containerPort: 2345

@ -8,6 +8,7 @@ nameSuffix: -local
namespace: barretthousen-local
patchesStrategicMerge:
- debug-auth.yaml
- debug-catalog.yaml
- debug-runner.yaml
- runner-secret.yaml

@ -8,3 +8,4 @@ stringData:
port: 80
endpoints:
runner: runner-local:5001
auth: auth-local:5001

@ -8,3 +8,4 @@ stringData:
port: 80
endpoints:
catalog: catalog-local:5001
auth: auth-local:5001

@ -1,9 +1,10 @@
go 1.19
use (
./src/catalog
./src/lib
./src/auth
./src/catalog
./src/runner
./src/proxy-admin
./src/proxy-web
./src/runner
)

@ -0,0 +1,95 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
interface FormValidations {
email?: boolean;
general?: boolean;
}
export const validations: FormValidations = {};
let email: string = '';
let password: string = '';
let passwordConfirmation: string = '';
const dispatch = createEventDispatcher();
const execLogin = () =>
dispatch('login', {
email,
password
});
const execRegister = () => {
if (password !== passwordConfirmation) {
console.log("password and confirm don't match");
return;
}
dispatch('register', {
email,
password
});
};
let showPassword = false;
let showRegistration = false;
const revealPass = () => (showPassword = !showPassword);
const toggleFormType = () => (showRegistration = !showRegistration);
$: ctaTxt = showRegistration ? 'Up' : 'In';
$: toggleCtaTxt = !showRegistration ? 'Up' : 'In';
$: ctaFunc = showRegistration ? execRegister : execLogin;
</script>
<form class="flex" style="max-width: 600px;" on:submit|preventDefault={ctaFunc}>
<span class="flex flex-col justify-around w-20">
<label for="email"> Email </label>
<label for="password"> Password&nbsp;</label>
</span>
<span class="flex flex-col">
<span class="flex">
<input
class="px-2 py-1 border-r-0 grow invalid:border-red-500 invalid:border-2"
type="email"
required
bind:value={email}
/>
<button class="border-l-0 px-2 py-1 disabled:text-gray-500">
Sign {ctaTxt}
</button>
</span>
<span class="flex">
<input
class="px-2 py-1 w-full"
name="password"
type={showPassword ? 'text' : 'password'}
required
placeholder={showRegistration ? 'Password' : ''}
on:blur={() => (showPassword = false)}
on:change={(evt) => (password = evt?.target?.value)}
/>
{#if showRegistration}
<input
class="px-2 py-1 border-r-0 invalid:border-red-500 invalid:border-2"
name="confirm_password"
type={showPassword ? 'text' : 'password'}
pattern={password}
required
placeholder="Confirm Password"
on:blur={() => (showPassword = false)}
on:change={(evt) => (passwordConfirmation = evt?.target?.value)}
/>
<button
class="border-l-0 px-2 py-1 w-16"
tabindex="-1"
on:click|stopPropagation={revealPass}
>
{showPassword ? 'Hide' : 'Show'}
</button>
{/if}
</span>
</span>
</form>

@ -0,0 +1,119 @@
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import { writable } from 'svelte/store';
const API_HOST = `${browser ? '' : env.BH_CLIENT_INTERNAL_API_HOST}/api/v1`;
interface SessionInfo {
sessionId?: string;
account?: {
id: string;
email: string;
role: 'BIDDER' | 'USER' | 'ADMINISTRATOR' | 'ANONYMOUS';
createdTs: string;
};
}
export const getSession = () => {
if (browser) {
const strSession = localStorage.getItem('bh-session');
if (strSession) {
const data = JSON.parse(strSession);
if (data?.sessionId) {
return data;
}
}
}
return null;
}
export const session = writable<SessionInfo>(getSession(), (set) => {
const session = getSession();
if (session) {
set(session);
}
});
interface LoginAction {
email: string;
password: string;
}
export const loginAction = async ({ email, password }: LoginAction): Promise<SessionInfo> => {
try {
const sessionStr = localStorage.getItem('bh-session');
if (sessionStr) {
const data = JSON.parse(sessionStr);
if (data.sessionId) {
console.log('already authenticated');
session.set(data);
return data;
}
localStorage.removeItem('bh-session');
}
const response = await fetch(
new Request(`${API_HOST}/user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password
})
})
);
const data = await response.json();
console.log(data);
if (data.sessionId) {
localStorage.setItem('bh-session', JSON.stringify(data));
return data;
}
console.trace("got this on login attempt:", data);
} catch (e) {
console.error(e);
}
return {};
};
interface RegisterAction {
email: string;
password: string;
role?: string;
}
export const registerAction = async ({ email, password, role }: RegisterAction) => {
try {
const response = await fetch(
new Request(`${API_HOST}/user`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password,
role: role || 'USER'
})
})
);
const data = await response.json();
console.log(data);
} catch (e) {
console.error(e);
}
};
export const logoutAction = () => {
localStorage.removeItem('bh-session');
session.set({});
};

@ -1,5 +1,34 @@
<script lang="ts">
import '../app.css';
import AuthForm from '$lib/AuthForm.svelte';
import { loginAction, logoutAction, registerAction, session } from '$lib/state';
async function onLogin(evt: CustomEvent) {
const { email, password } = evt.detail;
await loginAction({ email, password });
}
async function onLogout() {
logoutAction();
}
interface SessionInfo {
sessionId?: string;
account?: {
id: string;
email: string;
role: 'BIDDER' | 'USER' | 'ADMINISTRATOR' | 'ANONYMOUS';
createdTs: string;
};
}
let sessionVal: SessionInfo;
session.subscribe((v) => (sessionVal = v));
$: isLoggedIn = !!sessionVal?.sessionId;
</script>
<svelte:head>
@ -26,8 +55,19 @@
<li class="flex grow justify-center">
<span class="flex grow" style="max-width: 75%;"> Admin Stuff </span>
</li>
<li class="px-6">
<li class="pr-5">
<!-- <span>I want email alerts!</span> -->
{#if !isLoggedIn}
<AuthForm on:login={onLogin} />
{:else}
<span>
{sessionVal.account?.email}
</span>
<button
class="bg-bh-gold border-bh-black px-2 py-1 text-bh-black"
on:click|stopPropagation={onLogout}>Log out</button
>
{/if}
</li>
</ul>
</nav>

@ -7,6 +7,7 @@
import { fade } from 'svelte/transition';
import { onDestroy, onMount } from 'svelte';
import { invalidateAll } from '$app/navigation';
import { getSession } from '$lib/state';
export let data: PageData;
@ -27,7 +28,8 @@
new Request('/api/v1/sync', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'bh-session-id': getSession()?.sessionId
},
body: JSON.stringify({ targetSite: detail.target })
})

@ -1,6 +1,7 @@
import { browser } from '$app/environment';
import type { PageLoad } from './$types';
import { env } from '$env/dynamic/public';
import { getSession } from '$lib/state';
const API_HOST = `${browser ? '' : env.BH_CLIENT_INTERNAL_API_HOST}/api/v1`;
@ -26,8 +27,21 @@ interface ScrapeStatusPageData {
export const load = (async ({ fetch, url }): Promise<ScrapeStatusPageData> => {
const searchParams = new SearchParameters(url);
const limit = searchParams.getLimit();
console.log(getSession());
try {
const response = await fetch(API_HOST + `/sync${searchParams.toQueryString()}`);
const response = await fetch(
new Request(API_HOST + `/sync${searchParams.toQueryString()}`,
{
method: 'GET',
headers: {
'bh-session-id': getSession()?.sessionId,
}
}
)
);
const { active, complete, activeTotal, completeTotal, total, page } = await response.json();
return {

@ -0,0 +1,25 @@
tests:
- description: "Register User"
request:
path: "/v1/user"
method: "PUT"
response:
statusCodes: [200]
- description: "Login"
request:
path: "/v1/user"
method: "POST"
response:
statusCodes: [200]
- description: "Account Details"
request:
path: "/v1/user"
method: "GET"
response:
statusCodes: [200]
- description: "Verify Account"
request:
path: "/v1/user/verify"
method: "GET"
response:
statusCodes: [200]

@ -0,0 +1,64 @@
package api
import (
"context"
"strconv"
"time"
aapi "git.vdhsn.com/barretthousen/barretthousen/src/auth/api/grpc"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/domain"
"google.golang.org/grpc"
)
func NewAuthServiceClient(conn grpc.ClientConnInterface) *AuthServiceClient {
return &AuthServiceClient{
AuthClient: aapi.NewAuthClient(conn),
}
}
type AuthServiceClient struct {
aapi.AuthClient
}
type CheckSessionParams struct {
SessionID string
}
func (asc *AuthServiceClient) CheckSession(ctx context.Context, in CheckSessionParams) (out Account, err error) {
if in.SessionID == "" {
err = domain.ErrInvalidSession
return
}
var sessionIDInt int
if sessionIDInt, err = strconv.Atoi(in.SessionID); err != nil {
return
}
var accountResult *aapi.Account
if accountResult, err = asc.GetSession(ctx, &aapi.SessionFilter{
SessionId: int32(sessionIDInt),
}); err != nil {
return
}
out = Account{
ID: int(accountResult.Id),
Created: accountResult.CreatedTs.AsTime(),
Email: accountResult.Email,
Verified: accountResult.VerifiedTs.AsTime(),
Role: accountResult.Role,
}
return
}
type Account struct {
ID int `json:"id"`
Created time.Time `json:"created"`
Verified time.Time `json:"verified,omitempty"`
Email string `json:"email"`
PasswordHash string `json:"password_hash,omitempty"`
Role string `json:"role"`
Enabled bool `json:"enabled"`
}

@ -0,0 +1,78 @@
syntax = "proto3";
package main;
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option go_package = "git.vdhsn.com/barretthousen/barretthousen/src/auth/api/grpc";
service Auth {
rpc CreateAccount(CreateAccountInput) returns (Account) {
option (google.api.http) = {
put: "/v1/user"
body: "*"
};
}
rpc Login(LoginInput) returns (LoginResult) {
option (google.api.http) = {
post: "/v1/user"
body: "*"
};
}
rpc VerifyAccount(VerificationParameters) returns (AccountVerifiedResult) {
option (google.api.http) = {
get: "/v1/user/verify"
};
}
rpc GetAccount(AccountFilter) returns (Account) {
option (google.api.http) = {
get: "/v1/user"
};
}
rpc GetSession(SessionFilter) returns (Account) { }
}
message SessionFilter {
int32 sessionId = 1;
}
message CreateAccountInput {
string email = 1;
string password = 2;
string role = 3;
}
message LoginInput {
string email = 1;
string password = 2;
}
message LoginResult {
string sessionId = 1;
Account account = 2;
}
message AccountFilter {
int32 id = 1;
string email = 2;
}
message VerificationParameters {
string token = 1;
string email = 2;
}
message AccountVerifiedResult {}
message Account {
int32 id = 1;
string email = 2;
string role = 3;
google.protobuf.Timestamp createdTs = 4;
google.protobuf.Timestamp verifiedTs = 5;
}

@ -0,0 +1,42 @@
module git.vdhsn.com/barretthousen/barretthousen/src/auth
go 1.19
require (
git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
go.uber.org/dig v1.16.1
golang.org/x/sync v0.2.0
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1
google.golang.org/protobuf v1.30.0
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/ilyakaznacheev/cleanenv v1.4.2 // indirect
github.com/jackc/puddle v1.3.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/pressly/goose/v3 v3.11.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
)
require (
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.0
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.1
go.uber.org/automaxprocs v1.5.2 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/grpc v1.55.0
)
replace git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0 => ../lib

@ -0,0 +1,262 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08=
github.com/ilyakaznacheev/cleanenv v1.4.2 h1:nRqiriLMAC7tz7GzjzUTBHfzdzw6SQ7XvTagkFqe/zU=
github.com/ilyakaznacheev/cleanenv v1.4.2/go.mod h1:i0owW+HDxeGKE0/JPREJOdSCPIyOnmh6C0xhWAkF/xA=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
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/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/pressly/goose/v3 v3.11.0 h1:krazmHhfT6SxJGqtTjddwTsL2Xwje2piTQYRH8KLECI=
github.com/pressly/goose/v3 v3.11.0/go.mod h1:ofR04pV2CYY1q/y7CNjoFQuzW4lGSVKwMI/m9lnAjVo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8=
go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=

@ -0,0 +1,111 @@
-- +goose Up
START TRANSACTION;
CREATE SCHEMA IF NOT EXISTS auth;
CREATE TABLE IF NOT EXISTS auth.account (
id SERIAL PRIMARY KEY,
createdTs TIMESTAMP NOT NULL DEFAULT NOW(),
verifiedTs TIMESTAMP NULL,
email VARCHAR(512) NOT NULL,
passwordHash VARCHAR(512) NOT NULL,
role VARCHAR(64) NOT NULL DEFAULT 'BIDDER',
enabled BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE IF NOT EXISTS auth.account_verification (
id SERIAL PRIMARY KEY,
accountId INT NOT NULL,
createdTs TIMESTAMP NOT NULL DEFAULT NOW(),
verificationToken VARCHAR(128) NOT NULL UNIQUE,
CONSTRAINT fk_account_id FOREIGN KEY (accountId) REFERENCES auth.account(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS auth.sessions (
id SERIAL PRIMARY KEY,
accountId INT NOT NULL,
createdTs TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_account_id FOREIGN KEY (accountId) REFERENCES auth.account(id) ON DELETE CASCADE
);
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION auth.bh_register_account(
p_email VARCHAR(512),
p_passwordHash VARCHAR(512),
p_role VARCHAR(64))
RETURNS INTEGER
LANGUAGE plpgsql AS $BODY$
DECLARE
account_id INTEGER;
BEGIN
SELECT acc.id INTO account_id FROM auth.account acc WHERE acc.email = p_email;
IF account_id IS NULL OR account_id = 0 THEN
INSERT INTO auth.account (
email,
passwordHash,
role
) VALUES (
p_email,
p_passwordHash,
p_role
) RETURNING id INTO account_id;
ELSE
-- 0 means there is a duplicate account
account_id = 0;
END IF;
RETURN account_id;
END;
$BODY$;
-- +goose StatementEnd
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION auth.bh_verify_account(
p_verification_token VARCHAR(128))
RETURNS INTEGER
LANGUAGE plpgsql AS $BODY$
DECLARE
account_id INTEGER;
verification_id INTEGER;
BEGIN
SELECT
id, accountid INTO verification_id, account_id
FROM auth.account_verification
WHERE verificationtoken = p_verification_token;
IF account_id IS NULL OR account_id = 0 THEN
-- 0 means there is a duplicate account
account_id = 0;
ELSE
UPDATE auth.account SET verifiedTs=NOW() WHERE id = account_id;
DELETE FROM auth.account_verification WHERE id = verification_id;
END IF;
RETURN account_id;
END;
$BODY$;
-- +goose StatementEnd
-- +goose StatementBegin
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_catalog.pg_roles
WHERE rolname = 'auth-service') THEN
CREATE USER "auth-service" WITH PASSWORD 'auth-service';
END IF;
END
$do$;
-- +goose StatementEnd
GRANT CONNECT ON DATABASE bh to "auth-service";
GRANT USAGE ON SCHEMA auth TO "auth-service";
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA auth TO "auth-service";
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA auth TO "auth-service";
COMMIT;
-- +goose Down

@ -0,0 +1,46 @@
-- name: RegisterAccount :one
SELECT auth.bh_register_account(
sqlc.arg(email),
sqlc.arg(passwordHash),
sqlc.arg(role)
);
-- name: GetAccountByEmailOrId :one
SELECT
id,
createdTs,
verifiedTs,
email,
role,
enabled
FROM auth.account
WHERE
email = sqlc.arg(email) OR id = sqlc.arg(accountId);
-- name: GetSessionAccount :one
SELECT
acc.id,
acc.createdTs,
acc.verifiedTs,
acc.email,
acc.role,
acc.enabled
FROM auth.account acc
JOIN auth.sessions sess ON sess.accountId = acc.id
WHERE sess.id = sqlc.arg(sessionId) AND acc.enabled = TRUE;
-- name: VerifyAccount :one
SELECT auth.bh_verify_account(
sqlc.arg(token)
);
-- name: CreateSession :one
INSERT INTO auth.sessions (
accountId,
createdTs
) SELECT acc.id, NOW() FROM auth.account acc WHERE
(acc.email = sqlc.arg(email) OR acc.id = sqlc.arg(accountId))
AND acc.passwordHash = sqlc.arg(passwordHash)
RETURNING *;

@ -0,0 +1,152 @@
package data
import (
"context"
"errors"
"fmt"
"time"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data/postgres"
"github.com/jackc/pgx/v4"
)
type (
PGRunnerStorage struct {
*postgres.Queries
}
RegisterAccountInput struct {
Email string
PasswordHash string
Role string
}
)
var (
ErrSessionNotFound = errors.New("session not found")
ErrAccountNotFound = errors.New("account not found by the provided email or id")
ErrCouldNotCreateSession = errors.New("could not create session")
)
func (db *PGRunnerStorage) RegisterAccount(ctx context.Context, in RegisterAccountInput) (accountId int, err error) {
var accId int32
if accId, err = db.Queries.RegisterAccount(ctx, postgres.RegisterAccountParams{
Email: in.Email,
Passwordhash: in.PasswordHash,
Role: in.Role,
}); err != nil {
err = fmt.Errorf("could not register a new account with DB: %w", err)
return
}
accountId = int(accId)
return
}
type (
GetAccountInput struct {
AccountID int
Email string
}
AccountResult struct {
ID int `json:"id"`
Created time.Time `json:"created"`
Verified time.Time `json:"verified,omitempty"`
Email string `json:"email"`
PasswordHash string `json:"password_hash,omitempty"`
Role string `json:"role"`
Enabled bool `json:"enabled"`
}
)
func (db *PGRunnerStorage) GetAccount(ctx context.Context, in GetAccountInput) (out AccountResult, err error) {
var row postgres.GetAccountByEmailOrIdRow
if row, err = db.Queries.GetAccountByEmailOrId(ctx, postgres.GetAccountByEmailOrIdParams{
Email: in.Email,
Accountid: int32(in.AccountID),
}); errors.Is(err, pgx.ErrNoRows) {
err = ErrAccountNotFound
return
} else if err != nil {
err = fmt.Errorf("could not execute GetSessionAccount query: %w", err)
return
}
out = AccountResult{
ID: int(row.ID),
Email: row.Email,
Role: row.Role,
Created: row.Createdts,
Verified: row.Verifiedts.Time,
Enabled: true,
}
return
}
func (db *PGRunnerStorage) GetAccountBySession(ctx context.Context, sessionID int) (out AccountResult, err error) {
var row postgres.GetSessionAccountRow
if row, err = db.Queries.GetSessionAccount(ctx, int32(sessionID)); errors.Is(err, pgx.ErrNoRows) {
err = ErrSessionNotFound
return
} else if err != nil {
err = fmt.Errorf("could not execute GetSessionAccount query: %w", err)
return
}
out = AccountResult{
ID: int(row.ID),
Email: row.Email,
Role: row.Role,
Created: row.Createdts,
Verified: row.Verifiedts.Time,
Enabled: true,
}
return
}
type (
CreateSessionInput struct {
Email string
PasswordHash string
}
Session struct {
ID int
AccountID int
Created time.Time
}
)
func (db *PGRunnerStorage) CreateSession(ctx context.Context, in CreateSessionInput) (out Session, err error) {
var session postgres.AuthSession
if session, err = db.Queries.CreateSession(ctx, postgres.CreateSessionParams{
Email: in.Email,
Passwordhash: in.PasswordHash,
}); errors.Is(err, pgx.ErrNoRows) {
err = ErrCouldNotCreateSession
return
} else if err != nil {
err = fmt.Errorf("could not execute query: %w", err)
return
}
out = Session{
ID: int(session.ID),
AccountID: int(session.Accountid),
Created: session.Createdts,
}
return
}
type VerifyAccountInput struct {
Token string
}
func (db *PGRunnerStorage) VerifyAccount(ctx context.Context, in VerifyAccountInput) (err error) {
err = errors.New("Unimplemented")
return
}

@ -0,0 +1,187 @@
package domain
import (
"context"
"errors"
"fmt"
"time"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
)
type (
Domain struct {
Storage
PasswordHasher
}
HashedPassword []byte
PasswordHasher interface {
Hash(string) HashedPassword
Compare(string, HashedPassword) bool
}
Storage interface {
RegisterAccount(context.Context, data.RegisterAccountInput) (int, error)
GetAccountBySession(context.Context, int) (data.AccountResult, error)
GetAccount(context.Context, data.GetAccountInput) (data.AccountResult, error)
CreateSession(context.Context, data.CreateSessionInput) (data.Session, error)
VerifyAccount(context.Context, data.VerifyAccountInput) error
}
)
type (
CreateAccountCommand struct {
Email string
Password string
Role Role
}
AccountCreated struct {
Account
}
)
func (d *Domain) CreateAccount(ctx context.Context, in CreateAccountCommand) (out AccountCreated, err error) {
if in.Email == "" {
err = errors.New("email cannot be empty")
return
}
if in.Password == "" {
err = errors.New("password cannot be empty")
return
}
if in.Role == EmptyRole {
in.Role = UserRole
}
var accountID int
if accountID, err = d.Storage.RegisterAccount(ctx, data.RegisterAccountInput{
Email: in.Email,
PasswordHash: in.Password,
Role: in.Role.String(),
}); err != nil {
err = fmt.Errorf("could not register account: %w", err)
return
}
var ar data.AccountResult
if ar, err = d.Storage.GetAccount(ctx, data.GetAccountInput{
Email: in.Email,
AccountID: accountID,
}); err != nil {
err = fmt.Errorf("could not find account by ID: %w", err)
return
}
out = AccountCreated{
Account: Account{
ID: ar.ID,
Created: ar.Created,
Verified: ar.Verified,
Email: ar.Email,
PasswordHash: ar.PasswordHash,
Role: Role(ar.Role),
Enabled: ar.Enabled,
},
}
return
}
type (
LoginCommand struct {
Email string
Password string
}
LoggedIn struct {
Account
SessionID int
Created time.Time
}
)
var (
ErrInvalidLogin = errors.New("invalid credentials provided")
ErrInvalidSession = errors.New("invalid session id provided")
)
func (d *Domain) Login(ctx context.Context, in LoginCommand) (out LoggedIn, err error) {
kernel.TraceLog.Printf("%q login attempt", in.Email)
var sess data.Session
if sess, err = d.Storage.CreateSession(ctx, data.CreateSessionInput{
Email: in.Email,
PasswordHash: in.Password,
}); errors.Is(err, data.ErrCouldNotCreateSession) {
err = ErrInvalidLogin
return
} else if err != nil {
kernel.ErrorLog.Printf("Error creating session in storage for %q: %w", in.Email, err)
err = ErrInvalidLogin
return
}
var ar data.AccountResult
if ar, err = d.Storage.GetAccount(ctx, data.GetAccountInput{
AccountID: sess.AccountID,
}); err != nil {
err = fmt.Errorf("could not find account by ID: %w", err)
return
}
out = LoggedIn{
Account: Account{
ID: ar.ID,
Created: ar.Created,
Verified: ar.Verified,
Email: ar.Email,
Role: Role(ar.Role),
Enabled: ar.Enabled,
},
Created: sess.Created,
SessionID: sess.ID,
}
return
}
type (
GetAccountCommand struct {
SessionID int
}
)
func (d *Domain) GetAccount(ctx context.Context, in GetAccountCommand) (out Account, err error) {
var ar data.AccountResult
if ar, err = d.Storage.GetAccountBySession(ctx, in.SessionID); errors.Is(err, data.ErrSessionNotFound) {
err = ErrInvalidSession
return
} else if err != nil {
kernel.ErrorLog.Printf("Error getting account by session id: %w", err)
err = fmt.Errorf("could not get account: %w", err)
return
}
out = Account{
ID: ar.ID,
Created: ar.Created,
Verified: ar.Verified,
Email: ar.Email,
Role: Role(ar.Role),
Enabled: ar.Enabled,
}
return
}
type (
VerifyAccountCommand struct{}
AccountVerified struct{}
)
func (d *Domain) VerifyAccount(ctx context.Context, in VerifyAccountCommand) (out AccountVerified, err error) {
err = errors.New("Unimplemented")
return
}

@ -0,0 +1,166 @@
package domain
import (
"context"
"errors"
"reflect"
"strings"
"testing"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data"
)
func TestDomain_CreateAccount(t *testing.T) {
SetupTest()
tests := map[string]struct {
input CreateAccountCommand
wantOut AccountCreated
wantErr bool
}{
"new account": {
input: CreateAccountCommand{
Email: "test@example.com",
Password: "password",
},
wantOut: AccountCreated{
Account: Account{
ID: 0,
Email: "test@example.com",
PasswordHash: "password",
Enabled: true,
Role: UserRole,
},
},
},
"no email": {
input: CreateAccountCommand{
Password: "password",
Role: UserRole,
},
wantErr: true,
},
"no password": {
input: CreateAccountCommand{
Email: "test@example.com",
Role: UserRole,
},
wantErr: true,
},
"no inputs": {
wantErr: true,
},
}
c := ioc.Scope("CreateAccountTest")
Must(c.Provide(func(s Storage, ms *MockStorage) *Domain {
return &Domain{Storage: s}
}))
for name, v := range tests {
params := v
t.Run(name, func(t *testing.T) {
Must(c.Invoke(func(sut *Domain, ms *MockStorage) {
var a Account
ms.RegisterAccountFunc = func(ctx context.Context, rai data.RegisterAccountInput) (int, error) {
a = Account{
Email: rai.Email,
PasswordHash: rai.PasswordHash,
Role: Role(rai.Role),
Enabled: true,
}
return 0, nil
}
ms.GetAccountFunc = func(ctx context.Context, gai data.GetAccountInput) (data.AccountResult, error) {
if gai.Email != a.Email {
return data.AccountResult{}, errors.New("error")
}
return data.AccountResult{
Email: a.Email,
PasswordHash: a.PasswordHash,
Role: a.Role.String(),
Enabled: true,
}, nil
}
out, err := sut.CreateAccount(context.Background(), params.input)
if (err != nil) != params.wantErr {
t.Errorf("Domain.CreateAccount() error = %v, wantErr %v", err, params.wantErr)
return
}
if !reflect.DeepEqual(out, params.wantOut) {
t.Errorf("Domain.CreateAccount():\n%+v\n, want:\n%+v", out, params.wantOut)
}
}))
})
}
}
func TestDomain_Login(t *testing.T) {
tests := map[string]struct {
input LoginCommand
wantOut LoggedIn
wantErr bool
}{
"Login Valid User": {
input: LoginCommand{
Email: "test@example.com",
Password: "password",
},
wantOut: LoggedIn{
Account: Account{
ID: 1,
Email: "test@example.com",
PasswordHash: "",
Enabled: true,
Role: UserRole,
},
},
},
}
c := ioc.Scope("LoginTest")
Must(c.Provide(func(s Storage) *Domain {
return &Domain{Storage: s}
}))
for name, v := range tests {
params := v
t.Run(name, func(t *testing.T) {
Must(c.Invoke(func(sut *Domain, ms *MockStorage) {
ms.CreateSessionFunc = func(ctx context.Context, in data.CreateSessionInput) (data.Session, error) {
if strings.EqualFold(in.Email, "test@example.com") {
return data.Session{AccountID: 1}, nil
}
return data.Session{}, data.ErrCouldNotCreateSession
}
ms.GetAccountFunc = func(ctx context.Context, gai data.GetAccountInput) (data.AccountResult, error) {
if strings.EqualFold(gai.Email, "test@example.com") || gai.AccountID == 1 {
return data.AccountResult{
ID: 1,
Email: "test@example.com",
Role: UserRole.String(),
Enabled: true,
}, nil
}
return data.AccountResult{}, data.ErrSessionNotFound
}
out, err := sut.Login(context.Background(), params.input)
if (err != nil) != params.wantErr {
t.Errorf("Domain.Login() error = %v, wantErr %v", err, params.wantErr)
return
}
if !reflect.DeepEqual(out, params.wantOut) {
t.Errorf("Domain.Login() = %v, want %v", out, params.wantOut)
}
}))
})
}
}

@ -0,0 +1,32 @@
package domain
import "time"
const (
UserRole = Role("BIDDER")
AdminRole = Role("ADMINISRATOR")
AnonymousRole = Role("ANONYMOUS")
EmptyRole = Role("")
)
type Role string
func (r Role) String() string {
return string(r)
}
type Account struct {
ID int `json:"id"`
Created time.Time `json:"created"`
Verified time.Time `json:"verified,omitempty"`
Email string `json:"email"`
PasswordHash string `json:"password_hash,omitempty"`
Role Role `json:"role"`
Enabled bool `json:"enabled"`
}
type Session struct {
ID int
AccountID int
Created time.Time
}

@ -0,0 +1,70 @@
package domain
import (
"context"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data"
"go.uber.org/dig"
)
var ioc dig.Container
func SetupTest() {
ioc = *dig.New()
if err := ioc.Provide(func() (Storage, *MockStorage) {
ms := &MockStorage{}
return ms, ms
}); err != nil {
panic(err)
}
}
// Must panics is err is not nil
func Must(err error) {
if err != nil {
panic(err)
}
}
type MockStorage struct {
RegisterAccountFunc func(context.Context, data.RegisterAccountInput) (int, error)
GetAccountFunc func(context.Context, data.GetAccountInput) (data.AccountResult, error)
CreateSessionFunc func(context.Context, data.CreateSessionInput) (data.Session, error)
}
// CreateSession implements Storage.
func (m *MockStorage) CreateSession(ctx context.Context, in data.CreateSessionInput) (data.Session, error) {
if m.CreateSessionFunc == nil {
panic("CreateSessionFunc is unset")
}
return m.CreateSessionFunc(ctx, in)
}
// GetAccount implements Storage.
func (m *MockStorage) GetAccount(ctx context.Context, in data.GetAccountInput) (data.AccountResult, error) {
if m.GetAccountFunc == nil {
panic("GetAccountFunc is unset")
}
return m.GetAccountFunc(ctx, in)
}
// GetAccountBySession implements Storage.
func (m *MockStorage) GetAccountBySession(ctx context.Context, in int) (data.AccountResult, error) {
panic("GetAccountBySession not implemented")
}
// RegisterAccount implements Storage.
func (m *MockStorage) RegisterAccount(ctx context.Context, in data.RegisterAccountInput) (int, error) {
if m.RegisterAccountFunc == nil {
panic("RegisterAccountFunc is unset")
}
return m.RegisterAccountFunc(ctx, in)
}
// VerifyAccount implements Storage.
func (*MockStorage) VerifyAccount(context.Context, data.VerifyAccountInput) error {
panic("unimplemented")
}

@ -0,0 +1,108 @@
package internal
import (
"context"
"errors"
"fmt"
api "git.vdhsn.com/barretthousen/barretthousen/src/auth/api/grpc"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/domain"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
func NewAuthServer(d *domain.Domain) func(grpcServer grpc.ServiceRegistrar, endpoint string) {
return func(grpcServer grpc.ServiceRegistrar, endpoint string) {
api.RegisterAuthServer(grpcServer, &authHandler{domain: d})
}
}
type authHandler struct {
api.UnimplementedAuthServer
domain *domain.Domain
}
func (a *authHandler) CreateAccount(ctx context.Context, in *api.CreateAccountInput) (*api.Account, error) {
kernel.TraceLog.Printf("Attempting to create user: %q - %q", in.Email, in.Password)
account, err := a.domain.CreateAccount(ctx, domain.CreateAccountCommand{
Email: in.Email,
Password: in.Password,
Role: domain.Role(in.Role),
})
if err != nil {
return nil, err
}
return &api.Account{
Id: int32(account.ID),
Email: account.Email,
Role: account.Role.String(),
CreatedTs: timestamppb.New(account.Created),
VerifiedTs: timestamppb.New(account.Verified),
}, nil
}
func (a *authHandler) Login(ctx context.Context, in *api.LoginInput) (out *api.LoginResult, err error) {
var result domain.LoggedIn
if result, err = a.domain.Login(ctx, domain.LoginCommand{
Email: in.Email,
Password: in.Password,
}); errors.Is(err, domain.ErrInvalidLogin) {
err = status.Errorf(codes.PermissionDenied, "Email or password invalid.")
return
} else if err != nil {
err = fmt.Errorf("could not login: %w", err)
return
}
if err = grpc.SetHeader(ctx, metadata.New(map[string]string{
"bh-session-id": fmt.Sprintf("%d", result.SessionID),
})); err != nil {
err = status.Errorf(codes.Internal, "could not set header")
return
}
out = &api.LoginResult{
SessionId: fmt.Sprintf("%d", result.SessionID),
Account: &api.Account{
Id: int32(result.ID),
Email: result.Email,
Role: result.Role.String(),
CreatedTs: timestamppb.New(result.Created),
VerifiedTs: nil,
},
}
return
}
func (a *authHandler) VerifyAccount(ctx context.Context, in *api.VerificationParameters) (*api.AccountVerifiedResult, error) {
return nil, status.Errorf(codes.Unimplemented, "method VerifyAccount not implemented")
}
func (a *authHandler) GetAccount(ctx context.Context, in *api.AccountFilter) (*api.Account, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAccount not implemented")
}
func (a *authHandler) GetSession(ctx context.Context, in *api.SessionFilter) (out *api.Account, err error) {
var result domain.Account
if result, err = a.domain.GetAccount(ctx, domain.GetAccountCommand{
SessionID: int(in.SessionId),
}); err != nil {
err = status.Error(codes.Unauthenticated, "forbidden")
return
}
out = &api.Account{
Id: int32(result.ID),
Email: result.Email,
Role: result.Role.String(),
CreatedTs: timestamppb.New(result.Created),
VerifiedTs: nil,
}
return
}

@ -0,0 +1,100 @@
package main
import (
"context"
"embed"
"flag"
"fmt"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data/postgres"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/domain"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
"github.com/jackc/pgx/v4/pgxpool"
"go.uber.org/dig"
)
type (
authApp struct {
LogLevel kernel.LogLevel `yaml:"log_level" yaml-default:"0"`
Port int `yaml:"port" `
DB_Service kernel.PostgresConnection `yaml:"db_service"`
DB_Migrate kernel.PostgresConnection `yaml:"db_migrate"`
}
)
var (
migrate = flag.Bool("migrate", false, "migrates postgres db")
client = flag.String("client", "", "Runs this service in client mode, to invoke sync job. Takes GRPC endpoint")
target = flag.String("target", "", "To be used with client mode. The target to sync")
//go:embed internal/data/postgres/migrations/*.sql
dbMigrateScript embed.FS
)
func main() {
flag.Parse()
kernel.Run(context.Background(), &authApp{
LogLevel: kernel.LevelTrace,
Port: 5001,
})
}
func (app *authApp) Start(ctx context.Context) error {
if *migrate {
if err := kernel.MigrateDB(ctx, app.DB_Migrate, dbMigrateScript, "auth"); err != nil {
return fmt.Errorf("could not execute db migration: %w", err)
}
}
ioc := dig.New()
var err error
if err = ioc.Provide(func() kernel.PostgresConnection {
return app.DB_Service
}); err != nil {
return err
}
if err = ioc.Provide(func(pgCfg kernel.PostgresConnection) (*pgxpool.Pool, error) {
return kernel.NewDBConnection(ctx, pgCfg)
}); err != nil {
return err
}
if err = ioc.Provide(func(pgConn *pgxpool.Pool) *postgres.Queries {
return postgres.New(pgConn)
}); err != nil {
return err
}
if err = ioc.Provide(func(queries *postgres.Queries) domain.Storage {
return &data.PGRunnerStorage{Queries: queries}
}); err != nil {
return err
}
if err = ioc.Provide(func(rs domain.Storage) *domain.Domain {
return &domain.Domain{
Storage: rs,
}
}); err != nil {
return err
}
return ioc.Invoke(func(d *domain.Domain) error {
authService := internal.NewAuthServer(d)
if _, err := kernel.StartGRPCServer(ctx, app.Port, authService); err != nil {
return err
}
return nil
})
}
func (app *authApp) OnStop(ctx context.Context) {
}
func (app *authApp) GetLogLevel() kernel.LogLevel { return app.LogLevel }

@ -10,8 +10,10 @@ import (
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
)
type ServerBuilder func(grpc.ServiceRegistrar, string)
@ -38,6 +40,8 @@ func StartGRPCServer(ctx context.Context, port int, sb ServerBuilder, opts ...gr
grpcServerInstance = grpc.NewServer(opts...)
reflection.Register(grpcServerInstance)
sb(grpcServerInstance, endpoint)
if err = grpcServerInstance.Serve(listener); err != nil {

@ -5,6 +5,7 @@ go 1.19
require (
git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0
git.vdhsn.com/barretthousen/barretthousen/src/runner v1.0.0
git.vdhsn.com/barretthousen/barretthousen/src/auth v1.0.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
google.golang.org/grpc v1.55.0
)
@ -39,3 +40,5 @@ require (
replace git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0 => ../lib
replace git.vdhsn.com/barretthousen/barretthousen/src/runner v1.0.0 => ../runner
replace git.vdhsn.com/barretthousen/barretthousen/src/auth v1.0.0 => ../auth

@ -7,9 +7,12 @@ import (
"strings"
"time"
authApi "git.vdhsn.com/barretthousen/barretthousen/src/auth/api"
aapi "git.vdhsn.com/barretthousen/barretthousen/src/auth/api/grpc"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
api "git.vdhsn.com/barretthousen/barretthousen/src/runner/api/grpc"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"go.uber.org/dig"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/credentials/insecure"
@ -20,43 +23,108 @@ type ProxyAdminApp struct {
Port int `yaml:"port" `
Endpoints struct {
Runner string `yaml:"runner" `
Auth string `yaml:"auth"`
} `yaml:"endpoints"`
}
type AuthService interface {
CheckSession(context.Context, authApi.CheckSessionParams) (authApi.Account, error)
}
func (app *ProxyAdminApp) Start(ctx context.Context) error {
grpcMux := runtime.NewServeMux()
err := api.RegisterRunnerHandlerFromEndpoint(ctx, grpcMux, app.Endpoints.Runner, []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.Config{
MaxDelay: time.Second * 3,
},
MinConnectTimeout: time.Second,
}),
})
if err != nil {
ioc := dig.New()
var err error
if err = ioc.Provide(func() *runtime.ServeMux {
return runtime.NewServeMux()
}); err != nil {
return err
}
kernel.TraceLog.Printf("%+v", app)
if err = ioc.Provide(func() (grpc.ClientConnInterface, error) {
return kernel.DialGRPC(app.Endpoints.Auth)
}); err != nil {
return err
}
httpServer := &http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", app.Port),
ReadHeaderTimeout: time.Second,
Handler: http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
kernel.TraceLog.Printf("{ \"Client\": \"%s\", \"Path\":\"%s\", \"User-Agent\":\"%s\", \"Host\":\"%s\", \"Origin\":\"%s\"} ", r.RemoteAddr, r.URL, r.UserAgent(), r.Host, r.Header.Get("Origin"))
if err = ioc.Provide(func(conn grpc.ClientConnInterface) AuthService {
return authApi.NewAuthServiceClient(conn)
}); err != nil {
return err
}
if strings.HasPrefix(r.Host, "proxy-") {
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
}
if err = ioc.Invoke(func(grpcMux *runtime.ServeMux, authClient AuthService) error {
// TODO: refactor into kernel package
if err := api.RegisterRunnerHandlerFromEndpoint(ctx, grpcMux, app.Endpoints.Runner, []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.Config{
MaxDelay: time.Second * 3,
},
MinConnectTimeout: time.Second,
}),
}); err != nil {
return err
}
grpcMux.ServeHTTP(w, r)
})),
}
// TODO: refactor into kernel package
if err := aapi.RegisterAuthHandlerFromEndpoint(ctx, grpcMux, app.Endpoints.Auth, []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.Config{
MaxDelay: time.Second * 3,
},
MinConnectTimeout: time.Second,
}),
}); err != nil {
return err
}
kernel.InfoLog.Printf("Starting HTTP proxy @ %q", httpServer.Addr)
kernel.TraceLog.Printf("%+v", app)
httpServer := &http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", app.Port),
ReadHeaderTimeout: time.Second,
Handler: http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
kernel.TraceLog.Printf("{ \"Client\": \"%s\", \"Path\":\"%s\", \"User-Agent\":\"%s\", \"Host\":\"%s\", \"Origin\":\"%s\"} ", r.RemoteAddr, r.URL, r.UserAgent(), r.Host, r.Header.Get("Origin"))
// TODO: move to a middleware package
if strings.HasPrefix(r.Host, "proxy-") {
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
}
// TODO: move to a middleware package
if !(strings.HasSuffix(r.URL.Path, "user") && r.Method == http.MethodPost) {
sessIdStr := r.Header.Get("bh-session-id")
kernel.TraceLog.Printf("session %s", sessIdStr)
account, err := authClient.CheckSession(r.Context(), authApi.CheckSessionParams{
SessionID: sessIdStr,
})
if err != nil {
kernel.ErrorLog.Printf("error calling auth service: %v", err)
http.Error(w, "must be logged in as admin", http.StatusForbidden)
return
}
kernel.TraceLog.Printf("{ \"session-id\":\"%s\", \"email\":\"%s\", \"accountId\":\"%d\", \"role\":\"%s\"}", sessIdStr, account.Email, account.ID, account.Role)
if account.Role != "ADMINISTRATOR" {
http.Error(w, "must be administrator", http.StatusUnauthorized)
return
}
}
grpcMux.ServeHTTP(w, r)
})),
}
kernel.InfoLog.Printf("Starting HTTP proxy @ %q", httpServer.Addr)
return httpServer.ListenAndServe()
}); err != nil {
return err
}
return httpServer.ListenAndServe()
return nil
}
func (app *ProxyAdminApp) OnStop(ctx context.Context) {

@ -7,6 +7,7 @@ import (
"strings"
"time"
auth_api "git.vdhsn.com/barretthousen/barretthousen/src/auth/api/grpc"
api "git.vdhsn.com/barretthousen/barretthousen/src/catalog/api/grpc"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
@ -19,13 +20,14 @@ type ProxyClientApp struct {
LogLevel kernel.LogLevel `yaml:"log_level" yaml-default:"0"`
Port int `yaml:"port"`
Endpoints struct {
Catalog string `yaml:"catalog" env:"CATALOG_ENDPOINT"`
Catalog string `yaml:"catalog"`
Auth string `yaml:"auth"`
} `yaml:"endpoints" env:"PROXY_CLIENT_SERVICES"`
}
func (app *ProxyClientApp) Start(ctx context.Context) error {
grpcMux := runtime.NewServeMux()
err := api.RegisterCatalogHandlerFromEndpoint(ctx, grpcMux, app.Endpoints.Catalog, []grpc.DialOption{
grpcOpts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.Config{
@ -33,8 +35,13 @@ func (app *ProxyClientApp) Start(ctx context.Context) error {
},
MinConnectTimeout: time.Second,
}),
})
if err != nil {
}
if err := api.RegisterCatalogHandlerFromEndpoint(ctx, grpcMux, app.Endpoints.Catalog, grpcOpts); err != nil {
return err
}
if err := auth_api.RegisterAuthHandlerFromEndpoint(ctx, grpcMux, app.Endpoints.Auth, grpcOpts); err != nil {
return err
}

@ -71,15 +71,13 @@ type GetUpcomingSaleIDsInput struct {
}
func LAGetUpcomingSaleIDs(ctx context.Context, in GetUpcomingSaleIDsInput) (ids LACatalogIDs, total int, err error) {
if in.Limit == 0 {
in.Limit = 128
}
in.Limit = 128
req, _ := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf(
"https://search-party-prod.liveauctioneers.com/search/catalogsearch?page=%d&sort=saleStart&pageSize=%d",
"https://search-party-prod.liveauctioneers.com/search/catalogsearch?client_version=5.0&client=web&offset=300&sort=saleStart&page=%d&pageSize=%d&",
in.Page,
in.Limit,
),

@ -1,5 +1,14 @@
version: "2"
sql:
- queries: "auth/internal/data/postgres/queries.sql"
schema: "auth/internal/data/postgres/migrations"
engine: "postgresql"
gen:
go:
sql_package: pgx/v4
package: "postgres"
out: "auth/internal/data/postgres"
- queries: "runner/internal/data/postgres/queries.sql"
schema: "runner/internal/data/postgres/migrations"
engine: "postgresql"

@ -8,6 +8,10 @@
"name": "web-client",
"version": "0.0.1",
"dependencies": {
"@analytics/google-analytics": "^1.0.7",
"@segment/snippet": "^4.16.2",
"analytics": "^0.8.9",
"analytics-plugin-do-not-track": "^0.1.5",
"luxon": "^3.3.0"
},
"devDependencies": {
@ -46,6 +50,76 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@analytics/cookie-utils": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@analytics/cookie-utils/-/cookie-utils-0.2.12.tgz",
"integrity": "sha512-2h/yuIu3kmu+ZJlKmlT6GoRvUEY2k1BbQBezEv5kGhnn9KpmzPz715Y3GmM2i+m7Y0QmBdVUoA260dQZkofs2A==",
"dependencies": {
"@analytics/global-storage-utils": "^0.1.7"
}
},
"node_modules/@analytics/core": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/@analytics/core/-/core-0.12.7.tgz",
"integrity": "sha512-etmIPCoxWLoUZ/o1o2zvIk4cdVHa8I1xUQtTuLA+YXQ4SsFbm75ZoMXJBqWrNSENpqCJgoL6hizl5uTbkNN+1Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/davidwells"
}
],
"dependencies": {
"@analytics/global-storage-utils": "^0.1.7",
"@analytics/type-utils": "^0.6.2",
"analytics-utils": "^1.0.12"
}
},
"node_modules/@analytics/global-storage-utils": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@analytics/global-storage-utils/-/global-storage-utils-0.1.7.tgz",
"integrity": "sha512-V+spzGLZYm4biZT4uefaylm80SrLXf8WOTv9hCgA46cLcyxx3LD4GCpssp1lj+RcWLl/uXJQBRO4Mnn/o1x6Gw==",
"dependencies": {
"@analytics/type-utils": "^0.6.2"
}
},
"node_modules/@analytics/google-analytics": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@analytics/google-analytics/-/google-analytics-1.0.7.tgz",
"integrity": "sha512-KZ69NaMIi5kOcouzqI8cu7tZgQl7ziGiRahfU6zniUf32G8bv7wQDh73JFz1NwO6gBPloUc+5BzEoWzScM5Rgw=="
},
"node_modules/@analytics/localstorage-utils": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/@analytics/localstorage-utils/-/localstorage-utils-0.1.10.tgz",
"integrity": "sha512-uJS+Jp1yLG5VFCgA5T82ZODYBS0xuDQx0NtAZrgbqt9j51BX3TcgmOez5LVkrUNu/lpbxjCLq35I4TKj78VmOQ==",
"dependencies": {
"@analytics/global-storage-utils": "^0.1.7"
}
},
"node_modules/@analytics/session-storage-utils": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@analytics/session-storage-utils/-/session-storage-utils-0.0.7.tgz",
"integrity": "sha512-PSv40UxG96HVcjY15e3zOqU2n8IqXnH8XvTkg1X43uXNTKVSebiI2kUjA3Q7ESFbw5DPwcLbJhV7GforpuBLDw==",
"dependencies": {
"@analytics/global-storage-utils": "^0.1.7"
}
},
"node_modules/@analytics/storage-utils": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@analytics/storage-utils/-/storage-utils-0.4.2.tgz",
"integrity": "sha512-AXObwyVQw9h2uJh1t2hUgabtVxzYpW+7uKVbdHQK80vr3Td5rrmCxrCxarh7HUuAgSDZ0bZWqmYxVgmwKceaLg==",
"dependencies": {
"@analytics/cookie-utils": "^0.2.12",
"@analytics/global-storage-utils": "^0.1.7",
"@analytics/localstorage-utils": "^0.1.10",
"@analytics/session-storage-utils": "^0.0.7",
"@analytics/type-utils": "^0.6.2"
}
},
"node_modules/@analytics/type-utils": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@analytics/type-utils/-/type-utils-0.6.2.tgz",
"integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg=="
},
"node_modules/@esbuild/android-arm": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
@ -541,6 +615,27 @@
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@ndhoule/each": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@ndhoule/each/-/each-2.0.1.tgz",
"integrity": "sha512-wHuJw6x+rF6Q9Skgra++KccjBozCr9ymtna0FhxmV/8xT/hZ2ExGYR8SV8prg8x4AH/7mzDYErNGIVHuzHeybw==",
"dependencies": {
"@ndhoule/keys": "^2.0.0"
}
},
"node_modules/@ndhoule/keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@ndhoule/keys/-/keys-2.0.0.tgz",
"integrity": "sha512-vtCqKBC1Av6dsBA8xpAO+cgk051nfaI+PnmTZep2Px0vYrDvpUmLxv7z40COlWH5yCpu3gzNhepk+02yiQiZNw=="
},
"node_modules/@ndhoule/map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@ndhoule/map/-/map-2.0.1.tgz",
"integrity": "sha512-WOEf2An9mL4DVY6NHgaRmFC82pZGrmzW4I0hpPPdczDP4Gp5+Q1Nny77x3w0qzENA8+cbgd9+Lx2ClSTLvkB0g==",
"dependencies": {
"@ndhoule/each": "^2.0.1"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -745,6 +840,14 @@
}
}
},
"node_modules/@segment/snippet": {
"version": "4.16.2",
"resolved": "https://registry.npmjs.org/@segment/snippet/-/snippet-4.16.2.tgz",
"integrity": "sha512-2fgsrt4U+vKv14ohOAsViCEzeZotaawF2Il7YUbmYVrhPn8Hq7xuGznHKRdZeoxScQ87X36xDX2Fzh5bAYRN7g==",
"dependencies": {
"@ndhoule/map": "^2.0.1"
}
},
"node_modules/@sveltejs/adapter-node": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.2.4.tgz",
@ -834,6 +937,12 @@
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
"node_modules/@types/dlv": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/dlv/-/dlv-1.1.2.tgz",
"integrity": "sha512-OyiZ3jEKu7RtGO1yp9oOdK0cTwZ/10oE9PDJ6fyN3r9T5wkyOcvr6awdugjYdqF6KVO5eUvt7jx7rk2Eylufow==",
"peer": true
},
"node_modules/@types/estree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
@ -1110,6 +1219,38 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/analytics": {
"version": "0.8.9",
"resolved": "https://registry.npmjs.org/analytics/-/analytics-0.8.9.tgz",
"integrity": "sha512-oTbUzQpncMTslakqfK70GgB6bopk5hY+uuekwnadMkDyqNLgcD02KRzteTnO7q5Ko6wDECVtT8xi/6OuAMZykA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/davidwells"
}
],
"dependencies": {
"@analytics/core": "^0.12.7",
"@analytics/storage-utils": "^0.4.2"
}
},
"node_modules/analytics-plugin-do-not-track": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/analytics-plugin-do-not-track/-/analytics-plugin-do-not-track-0.1.5.tgz",
"integrity": "sha512-Bnadur6Y8UB2GrZD11SXAmIVraPpsVINcU8cgbq6ynJIE00xrcJ/RhqFdqqIM7Heyuze93FhcEKLl5WGV9N9mA=="
},
"node_modules/analytics-utils": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/analytics-utils/-/analytics-utils-1.0.12.tgz",
"integrity": "sha512-WvV2YWgsnXLxaY0QYux0crpBAg/0JA763NmbMVz22jKhMPo7dpTBet8G2IlF7ixTjLDzGlkHk1ZaKqqQmjJ+4w==",
"dependencies": {
"@analytics/type-utils": "^0.6.2",
"dlv": "^1.1.3"
},
"peerDependencies": {
"@types/dlv": "^1.0.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -1600,8 +1741,7 @@
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
"node_modules/doctrine": {
"version": "3.0.0",

@ -39,6 +39,10 @@
},
"type": "module",
"dependencies": {
"analytics": "^0.8.9",
"analytics-plugin-do-not-track": "^0.1.5",
"@analytics/google-analytics": "^1.0.7",
"@segment/snippet": "^4.16.2",
"luxon": "^3.3.0"
}
}

@ -1,3 +1,4 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {

@ -0,0 +1,101 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
interface FormValidations {
email?: boolean;
general?: boolean;
}
export const validations: FormValidations = {};
let email: string = '';
let password: string = '';
let passwordConfirmation: string = '';
const dispatch = createEventDispatcher();
const execLogin = () =>
dispatch('login', {
email,
password
});
const execRegister = () => {
if (password !== passwordConfirmation) {
console.log("password and confirm don't match");
return;
}
dispatch('register', {
email,
password
});
};
let showPassword = false;
let showRegistration = false;
const revealPass = () => (showPassword = !showPassword);
const toggleFormType = () => (showRegistration = !showRegistration);
$: ctaTxt = showRegistration ? 'Up' : 'In';
$: toggleCtaTxt = !showRegistration ? 'Up' : 'In';
$: ctaFunc = showRegistration ? execRegister : execLogin;
</script>
<form class="flex" style="max-width: 600px;" on:submit|preventDefault={ctaFunc}>
<span class="flex flex-col justify-around w-20">
<label for="email"> Email </label>
<label for="password"> Password&nbsp;</label>
</span>
<span class="flex flex-col">
<span class="flex">
<input
class="px-2 py-1 border-r-0 grow invalid:border-red-500 invalid:border-2"
type="email"
required
bind:value={email}
/>
<button class="border-l-0 px-2 py-1 disabled:text-gray-500">
Sign {ctaTxt}
</button>
</span>
<span class="flex">
<input
class="px-2 py-1 w-full"
name="password"
type={showPassword ? 'text' : 'password'}
required
placeholder={showRegistration ? 'Password' : ''}
on:blur={() => (showPassword = false)}
on:change={(evt) => (password = evt?.target?.value)}
/>
{#if showRegistration}
<input
class="px-2 py-1 border-r-0 invalid:border-red-500 invalid:border-2"
name="confirm_password"
type={showPassword ? 'text' : 'password'}
pattern={password}
required
placeholder="Confirm Password"
on:blur={() => (showPassword = false)}
on:change={(evt) => (passwordConfirmation = evt?.target?.value)}
/>
<button
class="border-l-0 px-2 py-1 w-16"
tabindex="-1"
on:click|stopPropagation={revealPass}
>
{showPassword ? 'Hide' : 'Show'}
</button>
{/if}
</span>
</span>
<button
class="w-20 bg-bh-gold text-bh-black border-bh-black"
on:click|stopPropagation={toggleFormType}
>
I Want to Sign {toggleCtaTxt}
</button>
</form>

@ -12,7 +12,7 @@
}
</script>
<form class="flex bg-bh-black rounded-md grow" on:submit|preventDefault={() => execSearch()}>
<input class="border-none grow py-1 px-2 mr-1 rounded-l-md" name="terms" bind:value={query} />
<button class="border-none rounded-r-md py-1 px-2">Submit</button>
<form class="flex bg-bh-black grow" on:submit|preventDefault={() => execSearch()}>
<input class="border-none grow py-1 px-2 mr-1" name="terms" bind:value={query} />
<button class="border-none py-1 px-2">Submit</button>
</form>

@ -0,0 +1,116 @@
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import { writable } from 'svelte/store';
const API_HOST = `${browser ? '' : env.BH_CLIENT_INTERNAL_API_HOST}/api/v1`;
interface SessionInfo {
sessionId?: string;
account?: {
id: string;
email: string;
role: 'BIDDER' | 'USER' | 'ADMINISTRATOR' | 'ANONYMOUS';
createdTs: string;
};
}
export const getSession = () => {
const strSession = localStorage.getItem('bh-session');
if (strSession) {
const data = JSON.parse(strSession);
if (data?.sessionId) {
return data;
}
}
return null;
}
export const session = writable<SessionInfo>(undefined, (set) => {
const session = getSession();
if (session) {
set(session);
}
});
interface LoginAction {
email: string;
password: string;
}
export const loginAction = async ({ email, password }: LoginAction): Promise<SessionInfo> => {
try {
const sessionStr = localStorage.getItem('bh-session');
if (sessionStr) {
const data = JSON.parse(sessionStr);
if (data.sessionId) {
console.log('already authenticated');
session.set(data);
return data;
}
localStorage.removeItem('bh-session');
}
const response = await fetch(
new Request(`${API_HOST}/user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password
})
})
);
const data = await response.json();
console.log(data);
if (data.sessionId) {
localStorage.setItem('bh-session', JSON.stringify(data));
return data;
}
console.trace("got this on login attempt:", data);
} catch (e) {
console.error(e);
}
return {};
};
interface RegisterAction {
email: string;
password: string;
role?: string;
}
export const registerAction = async ({ email, password, role }: RegisterAction) => {
try {
const response = await fetch(
new Request(`${API_HOST}/user`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password,
role: role || 'USER'
})
})
);
const data = await response.json();
console.log(data);
} catch (e) {
console.error(e);
}
};
export const logoutAction = () => {
localStorage.removeItem('bh-session');
session.set({});
};

@ -1,27 +1,93 @@
<script lang="ts">
import type { LayoutData } from './$types';
import { goto } from '$app/navigation';
import Analytics from 'analytics';
// import doNotTrack from 'analytics-plugin-do-not-track';
import googleAnalytics from '@analytics/google-analytics';
const analytics = Analytics({
app: 'Barretthousen - Web Client',
version: '1.0',
debug: process.env.NODE_ENV !== 'production',
plugins: [
googleAnalytics({
measurementIds: ['UA-143763293-1']
})
// doNotTrack()
]
});
/* Track a page view */
analytics.page();
import SearchBox from '$lib/SearchBox.svelte';
import AuthForm from '$lib/AuthForm.svelte';
import '../app.css';
import { loginAction, logoutAction, registerAction, session } from '$lib/state';
export let data: LayoutData;
async function onLogin(evt: CustomEvent) {
const { email, password } = evt.detail;
await loginAction({ email, password });
}
async function onLogout() {
analytics.track('user.logout', {});
logoutAction();
}
async function onRegister(evt: CustomEvent) {
const { email, password } = evt.detail;
analytics.track('user.register', { email });
await registerAction({ email, password });
}
async function onSubmit(evt: CustomEvent) {
let { query } = evt.detail;
// TODO: refactor to one source of truth for building query string parameters
await goto(query ? `/?query=${query}` : '/', {
invalidateAll: true
});
analytics.track('user.query', { query });
}
interface SessionInfo {
sessionId?: string;
account?: {
id: string;
email: string;
role: 'BIDDER' | 'USER' | 'ADMINISTRATOR' | 'ANONYMOUS';
createdTs: string;
};
}
let sessionVal: SessionInfo;
session.subscribe((v) => {
sessionVal = v;
if (v && v.sessionId && v.account) {
const { id, email, role } = v.account;
analytics.identify(id, { email, role });
}
});
$: isLoggedIn = !!sessionVal?.sessionId;
// $: pageTitle =
// data.query !== ''
// ? `'${data.query}' Search`
// : 'Barretthousen: The best rare collectibles from all over the web';
</script>
<svelte:head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>
Barretthousen: "{data.query}" results
</title>
<title>Barretthousen: The best rare collectibles from all over the web</title>
<meta name="description" content="Search results for '{data.query}'" />
</svelte:head>
@ -43,8 +109,19 @@
<SearchBox on:search={onSubmit} query={data.query} page={data.page} />
</span>
</li>
<li class="px-6">
<li class="pr-5">
<!-- <span>I want email alerts!</span> -->
{#if !isLoggedIn}
<AuthForm on:login={onLogin} on:register={onRegister} />
{:else}
<span>
{sessionVal.account?.email}
</span>
<button
class="bg-bh-gold border-bh-black px-2 py-1 text-bh-black"
on:click|stopPropagation={onLogout}>Log out</button
>
{/if}
</li>
</ul>
</nav>

Loading…
Cancel
Save