first pass at login/logout and sign up form
ci.vdhsn.com/push Build is failing
Details
ci.vdhsn.com/push Build is failing
Details
parent
5a191a2c72
commit
043f387224
8
Tiltfile
8
Tiltfile
|
|
@ -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 </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…
Reference in New Issue