figuring out svelte woo
parent
1f84edf535
commit
7d32805098
|
|
@ -97,12 +97,30 @@ func (s Stats) putLog(res http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if in.Value == 0 {
|
||||
fail.Payload = "Must specify non zero value"
|
||||
fail.Status = http.StatusExpectationFailed
|
||||
fail.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
if in.Type == "" {
|
||||
fail.Payload = "Must specify 'Weight', 'Calories', or 'Height' for type"
|
||||
fail.Status = http.StatusExpectationFailed
|
||||
fail.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
if in.RecordedTS.Equal(time.Time{}) {
|
||||
in.RecordedTS = time.Now().UTC()
|
||||
}
|
||||
|
||||
if err := s.Conn.AddLog(userId, in.StatLog); err != nil {
|
||||
log.Println(err)
|
||||
fail.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
APIResp{Success: true}.Write(res)
|
||||
APIResp{Success: true, Payload: in.StatLog}.Write(res)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,10 +139,12 @@ func (u UserHandler) login(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
res.Header().Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
http.SetCookie(res, &http.Cookie{
|
||||
Name: "jwt",
|
||||
Value: token,
|
||||
Path: "/v1/api/",
|
||||
Expires: time.Now().Add(duration),
|
||||
Name: "jwt",
|
||||
Value: token,
|
||||
Domain: "localhost",
|
||||
Path: "/",
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Now().Add(duration),
|
||||
})
|
||||
|
||||
APIResp{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package services
|
|||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
|
|
@ -36,9 +37,13 @@ func NewJWTVerifier() func(http.Handler) http.Handler {
|
|||
|
||||
func GenerateJWT(u User) string {
|
||||
_, tokenString, _ := tokenEncoder.Encode(map[string]interface{}{
|
||||
"id": u.ID,
|
||||
"x-Hasura-Role": "user",
|
||||
"x-Hasura-User-Id": u.ID,
|
||||
"id": u.ID,
|
||||
"https://hasura.io/jwt/claims": map[string]interface{}{
|
||||
"x-hasura-allowed-roles": []string{"user"},
|
||||
"X-hasura-role": "user",
|
||||
"x-hasura-default-role": "user",
|
||||
"x-hasura-user-id": fmt.Sprintf("%d", u.ID),
|
||||
},
|
||||
})
|
||||
return tokenString
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
BIN := .bin
|
||||
|
||||
start: $(BIN)/api
|
||||
start: clean $(BIN)/api
|
||||
BODYTRACK_API_HOST="localhost" \
|
||||
BODYTRACK_API_JWT_SECRET="ducks123" \
|
||||
BODYTRACK_API_JWT_SECRET="D*zvWkiDHi#j&Rx7c#NaPbd*pk%ayjsX" \
|
||||
BODYTRACK_API_PG_HOST="localhost" \
|
||||
BODYTRACK_API_PG_USERNAME="api" \
|
||||
BODYTRACK_API_PG_PASSWORD="api-user" \
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ services:
|
|||
- HASURA_GRAPHQL_ENABLE_CONSOLE=true
|
||||
- HASURA_GRAPHQL_ADMIN_SECRET=bodytrack-graphql
|
||||
- HASURA_GRAPHQL_UNAUTHORIZED_ROLE=anonymous
|
||||
- "HASURA_GRAPHQL_JWT_SECRET={\"header\": { \"type\": \"cookie\", \"name\":\"jwt\"}, \"type\": \"HS256\", \"key\":\"D*zvWkiDHi#j&Rx7c#NaPbd*pk%ayjsX\"}"
|
||||
ports:
|
||||
- 8080:8080
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
export const doFetch = async function({ url, method = "GET", body = null, headers = {} }) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
body: !!body ? JSON.stringify(body) : undefined,
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...headers,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('jwt')}`
|
||||
}
|
||||
})
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return { success: false, payload: error }
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
"@urql/svelte": "^1.2.1",
|
||||
"d3": "^6.7.0",
|
||||
"graphql": "^15.5.0",
|
||||
"js-cookie": "^2.2.1",
|
||||
"svelte-routing": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { initClient } from '@urql/svelte';
|
||||
import { Router, Route } from 'svelte-routing';
|
||||
import user from './stores/user.js';
|
||||
import Nav from './components/Nav.svelte';
|
||||
import Authenticate from './pages/Authenticate.svelte';
|
||||
import Exercises from './pages/Exercises.svelte';
|
||||
|
|
@ -10,6 +11,7 @@
|
|||
url: 'http://localhost:8080/v1/graphql',
|
||||
});
|
||||
|
||||
user.load();
|
||||
export let url = "";
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
export let exercise;
|
||||
|
||||
let instructions = exercise.instructions.replaceAll('\\n', '<br>');
|
||||
console.log(instructions);
|
||||
</script>
|
||||
<article id="{exercise.name}" class="bg-gray-100 rounded-l p-8 animate-">
|
||||
<header class="text-lg my-1">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
<script>
|
||||
import Link from './Link.svelte';
|
||||
import Button from './Button.svelte';
|
||||
import user from '../stores/user.js';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
|
||||
let isLoggedIn = false;
|
||||
const unsubscribe = user.subscribe(u => { isLoggedIn = u.loggedIn });
|
||||
onDestroy(unsubscribe);
|
||||
</script>
|
||||
<style>
|
||||
nav {
|
||||
|
|
@ -22,7 +29,11 @@
|
|||
<li><Link href="/">Exercise Index</Link></li>
|
||||
</ul>
|
||||
<section>
|
||||
<Button><Link href="login">Log in/Sign up</Link></Button>
|
||||
{#if isLoggedIn}
|
||||
<Button on:button-click={user.logout}><Link href="/">Log out</Link></Button>
|
||||
{:else}
|
||||
<Button><Link href="login">Log in/Sign up</Link></Button>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,64 +1,71 @@
|
|||
<script>
|
||||
import { fade } from 'svelte/transition';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Button from '../components/Button.svelte';
|
||||
import user from '../stores/user.js';
|
||||
|
||||
export let showLogin = true;
|
||||
let email= "";
|
||||
let password= "";
|
||||
let confirmPassword= "";
|
||||
let label = showLogin ? "Login" : "Sign up"
|
||||
let actionLabel = showLogin ? "Login" : "Sign up"
|
||||
let inverseLabel = !showLogin ? "Login" : "Sign up"
|
||||
let error = null;
|
||||
|
||||
function authenticate(){
|
||||
const toggle = () => showLogin = !showLogin;
|
||||
|
||||
|
||||
async function authenticate(){
|
||||
error = null;
|
||||
fetch("http://localhost:3000/v1/api/user/auth", {
|
||||
method: "POST",
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then(response => response.json())
|
||||
.then(({success, payload}) => {
|
||||
if (!success){
|
||||
error = payload;
|
||||
}
|
||||
})
|
||||
.catch(e => error = e.message);
|
||||
await user.login({ email, password });
|
||||
console.log(user);
|
||||
location.replace("/stats");
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
section {
|
||||
width: 33%;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply border-solid border-2 border-gray-700 ;
|
||||
@apply border-solid border-2 border-gray-700 flex-grow;
|
||||
}
|
||||
section div {
|
||||
@apply flex justify-between py-2 w-4/5;
|
||||
|
||||
form label {
|
||||
@apply flex flex-col py-2;
|
||||
}
|
||||
|
||||
form label span {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="w-full h-full flex justify-center flex-col content-center items-center" >
|
||||
<h2>{label} </h2>
|
||||
<section transition:fade class="flex flex-col p-5">
|
||||
<div >
|
||||
Email: <input id="email" bind:value={email}>
|
||||
</div>
|
||||
<div >
|
||||
Password: <input id="password" bind:value={password} type="password" >
|
||||
</div>
|
||||
<h2>{actionLabel}</h2>
|
||||
<form on:submit|preventDefault={authenticate} transition:fade class="flex flex-col p-5">
|
||||
<label>
|
||||
<span>Email:</span>
|
||||
<input id="email" bind:value={email}>
|
||||
</label>
|
||||
<label>
|
||||
<span>Password:</span>
|
||||
<input id="password" bind:value={password} type="password">
|
||||
</label>
|
||||
{#if !showLogin}
|
||||
<div>
|
||||
Confirm Password: <input id="confirm-password" bind:value={confirmPassword} type="password" >
|
||||
</div>
|
||||
<label>
|
||||
<span>Confirm Password:</span>
|
||||
<input id="confirm-password" bind:value={confirmPassword} type="password">
|
||||
</label>
|
||||
{/if}
|
||||
<Button on:button-click={authenticate}>{label}</Button>
|
||||
</section>
|
||||
{#if error}
|
||||
<section class="text-red-700">
|
||||
{error}
|
||||
</section>
|
||||
{/if}
|
||||
<section class="flex justify-end">
|
||||
<span class="pr-2"><Button>{actionLabel}</Button></span>
|
||||
<span ><Button on:button-click={toggle}>{inverseLabel}</Button></span>
|
||||
</section>
|
||||
{#if error}
|
||||
<section class="text-red-700">
|
||||
{error}
|
||||
</section>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -1,47 +1,39 @@
|
|||
<script>
|
||||
import { query, operationStore } from '@urql/svelte';
|
||||
import { onMount } from 'svelte'
|
||||
import { onMount } from 'svelte';
|
||||
import stats from '../stores/stats.js';
|
||||
import * as d3 from 'd3';
|
||||
import { query } from '@urql/svelte';
|
||||
|
||||
let statsQuery = operationStore(`
|
||||
query GetStats($types: [logtype!] = ["Weight"], $start: timestamp! = "2020-01-01T00:00:00") {
|
||||
stats_log(where: {
|
||||
logtype: {_in: $types },
|
||||
recordedts: {_gte: $start}
|
||||
},
|
||||
order_by: { recordedts: desc }) {
|
||||
value
|
||||
logtype
|
||||
recordedts
|
||||
id
|
||||
}
|
||||
}`,
|
||||
{
|
||||
"start": "2020-01-01T00:00:00",
|
||||
"types": ["Weight", "Calories"]
|
||||
},
|
||||
{ requestPolicy: "cache-and-network" });
|
||||
const statsQ = stats.getStat({ start: '2020-01-01T00:00:00', types: ['Weight'] });
|
||||
|
||||
let statsData = query(statsQuery);
|
||||
|
||||
let data = [30, 86, 168, 281, 303, 365];
|
||||
|
||||
let el;
|
||||
|
||||
onMount(() => {
|
||||
query(statsQ);
|
||||
|
||||
let mounted = false;
|
||||
function render(data, isLoading){
|
||||
if (isLoading || !mounted)
|
||||
return;
|
||||
|
||||
d3.select(el)
|
||||
.selectAll("div")
|
||||
.data(data)
|
||||
.data(data.stats_log)
|
||||
.enter()
|
||||
.append("div")
|
||||
.style("width", function(d) {
|
||||
return d + "px";
|
||||
return d.value + "px";
|
||||
})
|
||||
.text(function(d) {
|
||||
return d;
|
||||
return d.recordedts;
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$: render($statsQ.data, $statsQ.fetching);
|
||||
|
||||
onMount(async () => {
|
||||
mounted = true;
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.chart :global(div) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { query, operationStore, subscription } from '@urql/svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { doFetch } from '../../helpers';
|
||||
const defaultState = {loggedIn:false, loading:false, profile: null, error: null}
|
||||
const { subscribe, set, update } = writable(defaultState);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function logStat({ type = "Weight", recordedTs, value }) {
|
||||
return await doFetch({
|
||||
url: 'http://localhost/v1/api/stats',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
type,
|
||||
recordedTs,
|
||||
value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getStat = ({ start = '2020-01-01T00:00:00',types = ['Weight', 'Calories', 'Height'] }) =>
|
||||
operationStore(`
|
||||
query GetStats($types: [logtype!] = ["Weight"], $start: timestamp! = "2020-01-01T00:00:00") {
|
||||
stats_log(where: {
|
||||
logtype: {_in: $types },
|
||||
recordedts: {_gte: $start}
|
||||
},
|
||||
order_by: { recordedts: desc }) {
|
||||
value
|
||||
logtype
|
||||
recordedts
|
||||
id
|
||||
}
|
||||
}`,
|
||||
{ start, types },
|
||||
{
|
||||
requestPolicy: "cache-and-network",
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('jwt')}`
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
export default { subscribe, logStat, getStat };
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import Cookies from 'js-cookie';
|
||||
import { writable } from 'svelte/store';
|
||||
import { doFetch } from '../../helpers';
|
||||
|
||||
const defaultState = { loggedIn: false, loading: false, profile: null, error: null, token: null };
|
||||
|
||||
const { subscribe, set, update } = writable(defaultState);
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('jwt');
|
||||
Cookies.remove('jwt', { path: '/', domain: 'localhost' });
|
||||
set(defaultState);
|
||||
location.pathname !== "/" && location.replace("/");
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
update(p => ({ ...p, loading: true}));
|
||||
|
||||
const {success, payload} = await doFetch({ url:"http://localhost:3000/v1/api/user" });
|
||||
|
||||
if (!success) {
|
||||
set({ ...defaultState, error: payload });
|
||||
logout();
|
||||
return
|
||||
}
|
||||
|
||||
update(() => ({ ...defaultState, loggedIn:true, profile: payload, token: localStorage.getItem('jwt') }));
|
||||
} catch (e) {
|
||||
set({ ...defaultState, error: e.message });
|
||||
logout();
|
||||
}
|
||||
}
|
||||
|
||||
const login = async ({ email, password }) => {
|
||||
try {
|
||||
update(p => ({ ...p, loading: true}));
|
||||
const { success, payload } = await doFetch({
|
||||
url: "http://localhost:3000/v1/api/user/auth",
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: { email, password }
|
||||
});
|
||||
|
||||
|
||||
if (!success) {
|
||||
set({ ...defaultState, error: payload });
|
||||
logout();
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
set({ ...defaultState, loggedIn:true, profile: payload.profile, token: payload.token });
|
||||
localStorage.setItem('jwt', payload.token)
|
||||
} catch (e) {
|
||||
set({ ...defaultState, error: e.message });
|
||||
console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default { subscribe, login, logout, load };
|
||||
|
|
@ -3414,6 +3414,11 @@ isstream@~0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
|
||||
|
||||
js-cookie@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
|
||||
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
|
|
|||
Loading…
Reference in New Issue