first pass at login/logout and sign up form
ci.vdhsn.com/push Build is failing Details

feat/auth
Adam Veldhousen 11 months ago
parent 5a191a2c72
commit 043f387224
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

@ -149,11 +149,11 @@ bh_backend_service(service="auth", migrateDB=True, port_forwards=[
])
bh_backend_service(service="runner", migrateDB=True, port_forwards=[
port_forward(2345, name='Delve port')
port_forward(2346, 2345, name='Delve port')
])
bh_backend_service(service="catalog", migrateDB=True, port_forwards=[
port_forward(2346, 2345, name='Delve port')
port_forward(2347, 2345, name='Delve port')
])
bh_backend_service(service="proxy-admin", port_forwards=[
@ -164,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,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

@ -9,8 +9,8 @@ CREATE TABLE IF NOT EXISTS auth.account (
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 1
role VARCHAR(64) NOT NULL DEFAULT 'BIDDER',
enabled BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE IF NOT EXISTS auth.account_verification (
@ -43,14 +43,12 @@ BEGIN
IF account_id IS NULL OR account_id = 0 THEN
INSERT INTO auth.account (
email,
password,
role,
enabled
passwordHash,
role
) VALUES (
p_email,
p_passwordHash,
p_role,
p_enabled
p_role
) RETURNING id INTO account_id;
ELSE
-- 0 means there is a duplicate account
@ -72,11 +70,9 @@ DECLARE
verification_id INTEGER;
BEGIN
SELECT
acc.id INTO account_id,
ev.id INTO verification_id
FROM auth.account acc
JOIN auth.account_verification ev ON acc.id = ev.accountId
WHERE ev.verificationToken = p_verification_token;
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

@ -27,7 +27,7 @@ SELECT auth.bh_verify_account(
INSERT INTO auth.sessions (
accountId,
createdTs
) VALUES (
sqlc.arg(accountId),
NOW()
) RETURNING *;
) 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 *;

@ -7,6 +7,7 @@ import (
"time"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data/postgres"
"github.com/jackc/pgx/v4"
)
type (
@ -77,8 +78,8 @@ func (db *PGRunnerStorage) GetAccount(ctx context.Context, in GetAccountInput) (
type (
CreateSessionInput struct {
Email string
AccountID int
Email string
PasswordHash string
}
Session struct {
@ -90,7 +91,13 @@ type (
func (db *PGRunnerStorage) CreateSession(ctx context.Context, in CreateSessionInput) (out Session, err error) {
var session postgres.AuthSession
if session, err = db.Queries.CreateSession(ctx, int32(in.AccountID)); err != nil {
if session, err = db.Queries.CreateSession(ctx, postgres.CreateSessionParams{
Email: in.Email,
Passwordhash: in.PasswordHash,
}); errors.Is(err, pgx.ErrNoRows) {
return
} else if err != nil {
err = fmt.Errorf("could not execute query: %w", err)
return
}

@ -7,6 +7,8 @@ import (
"time"
"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
"github.com/jackc/pgx/v4"
)
type (
@ -90,17 +92,29 @@ func (d *Domain) CreateAccount(ctx context.Context, in CreateAccountCommand) (ou
}
type (
LoginCommand struct{}
LoggedIn struct {
LoginCommand struct {
Email string
Password string
}
LoggedIn struct {
Account
SessionID int
Created time.Time
}
)
var ErrInvalidLogin = errors.New("invalid credentials 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{}); err != nil {
if sess, err = d.Storage.CreateSession(ctx, data.CreateSessionInput{
Email: in.Email,
PasswordHash: in.Password,
}); errors.Is(err, pgx.ErrNoRows) {
err = ErrInvalidLogin
return
} else if err != nil {
err = fmt.Errorf("could not create session: %w", err)
return
}

@ -3,7 +3,7 @@ package domain
import "time"
const (
UserRole = Role("USER")
UserRole = Role("BIDDER")
AdminRole = Role("ADMINISRATOR")
AnonymousRole = Role("ANONYMOUS")
EmptyRole = Role("")

@ -2,11 +2,15 @@ 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"
)
@ -23,6 +27,7 @@ type authHandler struct {
}
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,
@ -41,8 +46,37 @@ func (a *authHandler) CreateAccount(ctx context.Context, in *api.CreateAccountIn
}, nil
}
func (a *authHandler) Login(ctx context.Context, in *api.LoginInput) (*api.LoginResult, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
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) {

@ -44,7 +44,7 @@ func main() {
func (app *authApp) Start(ctx context.Context) error {
if *migrate {
if err := kernel.MigrateDB(ctx, app.DB_Migrate, dbMigrateScript, "runner"); err != nil {
if err := kernel.MigrateDB(ctx, app.DB_Migrate, dbMigrateScript, "auth"); err != nil {
return fmt.Errorf("could not execute db migration: %w", err)
}
}

@ -1,15 +1,49 @@
<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);
function execLogin() {
console.log('LOGGING IN');
}
$: ctaTxt = showRegistration ? 'Up' : 'In';
$: toggleCtaTxt = !showRegistration ? 'Up' : 'In';
$: ctaFunc = showRegistration ? execRegister : execLogin;
$: confirmPasswordValid = password === passwordConfirmation;
</script>
<form class="flex" on:submit|preventDefault={execLogin}>
<form class="flex" on:submit|preventDefault={ctaFunc}>
<span class="flex flex-col justify-around w-20">
<label for="email"> Email </label>
<label for="password"> Password&nbsp;</label>
@ -17,9 +51,14 @@
<span class="flex flex-col">
<span class="flex">
<input class="py-1" type="text" />
<button class="w-20"> Login </button>
<button class="w-20"> Sign Up </button>
<input class="py-1" type="text" bind:value={email} />
<button class="w-20"> Sign {ctaTxt} </button>
<button
class="w-20 bg-bh-gold text-bh-black border-bh-black"
on:click|stopPropagation={toggleFormType}
>
Sign {toggleCtaTxt}
</button>
</span>
<span class="flex">
<input
@ -27,13 +66,16 @@
name="password"
on:blur={() => (showPassword = false)}
type={showPassword ? 'text' : 'password'}
on:change|nonpassive={(evt) => (password = evt?.target?.value)}
/>
{#if showRegistration}
<input
class="px-2 py-1 border-r-0"
name="confirm_password"
placeholder="Confirm Password"
on:blur={() => (showPassword = false)}
type={showPassword ? 'text' : 'password'}
on:change={(evt) => (passwordConfirmation = evt?.target?.value)}
/>
{/if}
<button class="border-l-0 px-2 py-1 w-16" on:click|stopPropagation={revealPass}>

@ -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,96 @@
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 session = writable<SessionInfo>(undefined, (set) => {
const strSession = localStorage.getItem('bh-session');
if (strSession) {
const data = JSON.parse(strSession);
if (data?.sessionId) {
set(data);
}
}
});
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);
console.log('already authenticated');
session.set(data);
return data;
}
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();
localStorage.setItem('bh-session', JSON.stringify(data));
return 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({});
};

@ -5,9 +5,24 @@
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() {
logoutAction();
}
async function onRegister(evt: CustomEvent) {
const { email, password } = evt.detail;
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
@ -15,6 +30,22 @@
invalidateAll: true
});
}
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>
@ -47,7 +78,17 @@
</li>
<li class="pr-5">
<!-- <span>I want email alerts!</span> -->
<AuthForm />
{#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