Browse Source

add frontend basics with working auth

master
Gisle Aune 2 years ago
parent
commit
9e2f8d3515
  1. 4
      entities/user.go
  2. 8
      frontend/.gitignore
  3. 1
      frontend/.npmrc
  4. 38
      frontend/README.md
  5. 33660
      frontend/package-lock.json
  6. 30
      frontend/package.json
  7. 17
      frontend/src/app.d.ts
  8. 26
      frontend/src/app.html
  9. 52
      frontend/src/hooks.ts
  10. 74
      frontend/src/lib/clients/sl3.ts
  11. 38
      frontend/src/lib/components/common/Card.svelte
  12. 29
      frontend/src/lib/components/common/CardHeader.svelte
  13. 14
      frontend/src/lib/components/common/LinkCard.svelte
  14. 11
      frontend/src/lib/components/frontpage/ScopeLink.svelte
  15. 64
      frontend/src/lib/components/layout/Background.svelte
  16. 11
      frontend/src/lib/components/layout/Column.svelte
  17. 26
      frontend/src/lib/components/layout/Columns.svelte
  18. 46
      frontend/src/lib/components/layout/Header.svelte
  19. 22
      frontend/src/lib/components/layout/Row.svelte
  20. 24
      frontend/src/lib/css/colors.sass
  21. 16
      frontend/src/lib/models/scope.ts
  22. 12
      frontend/src/lib/models/stat.ts
  23. 12
      frontend/src/lib/models/user.ts
  24. 20
      frontend/src/routes/__layout.svelte
  25. 35
      frontend/src/routes/api/[...any].ts
  26. 56
      frontend/src/routes/index.svelte
  27. 42
      frontend/src/routes/login.svelte
  28. BIN
      frontend/static/background.jpg
  29. BIN
      frontend/static/favicon.png
  30. 15
      frontend/svelte.config.js
  31. 36
      frontend/tsconfig.json
  32. 3
      go.mod
  33. 2
      go.sum
  34. 106
      ports/cognitoauth/client.go
  35. 32
      ports/httpapi/auth.go
  36. 1
      usecases/auth/provider.go
  37. 4
      usecases/auth/service.go

4
entities/user.go

@ -1,12 +1,14 @@
package entities
type User struct {
ID string `json:"id"`
ID string `json:"id"`
Name string `json:"name"`
}
type AuthResult struct {
User *User `json:"user"`
Token string `json:"token,omitempty"`
RefreshToken string `json:"refreshToken,omitempty"`
Session string `json:"session,omitempty"`
PasswordChangeRequired bool `json:"passwordChangeRequired"`
}

8
frontend/.gitignore

@ -0,0 +1,8 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example

1
frontend/.npmrc

@ -0,0 +1 @@
engine-strict=true

38
frontend/README.md

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm init svelte
# create a new project in my-app
npm init svelte my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

33660
frontend/package-lock.json
File diff suppressed because it is too large
View File

30
frontend/package.json

@ -0,0 +1,30 @@
{
"name": "webui",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"package": "svelte-kit package",
"preview": "svelte-kit preview",
"prepare": "svelte-kit sync",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@aws-amplify/auth": "^4.5.6",
"@aws-amplify/core": "^4.5.6",
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"@types/cookie": "^0.5.1",
"aws-amplify": "^4.3.24",
"cookie": "^0.5.0",
"node-sass": "^7.0.1",
"sass": "^1.52.3",
"svelte": "^3.44.0",
"svelte-check": "^2.7.1",
"svelte-preprocess": "^4.10.6",
"tslib": "^2.3.1",
"typescript": "^4.7.2"
},
"type": "module"
}

17
frontend/src/app.d.ts

@ -0,0 +1,17 @@
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
interface Locals {
user?: import("$lib/models/user").default
idToken?: string
scopes?: import("$lib/models/scope").default[]
}
// interface Platform {}
interface Session {
idToken?: string
req?: any
}
// interface Stuff {}
}

26
frontend/src/app.html

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
background-color: hsl(240, 12%, 5%);
color: hsl(240, 5%, 70%);
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
</style>
<script>
if (global === undefined) {
var global = window;
}
</script>
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

52
frontend/src/hooks.ts

@ -0,0 +1,52 @@
import cookie from 'cookie';
import { Amplify, withSSRContext } from 'aws-amplify';
import type { GetSession, Handle } from '@sveltejs/kit';
Amplify.configure({
Auth: {
region: import.meta.env.VITE_STUFFLOG3_AWS_POOL_REGION,
userPoolId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_ID,
userPoolWebClientId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_CLIENT_ID,
},
ssr: true,
});
export const getSession: GetSession = ({ locals, request }) => {
return {
idToken: locals.idToken,
req: { headers: { cookie: request.headers.get("Cookie") } }
};
};
export const handle: Handle = async({ event, resolve }) => {
const {Auth} = withSSRContext({
req: { headers: { cookie: event.request.headers.get("Cookie") } }
});
const newCookies = {};
try {
const curr = await Auth.currentAuthenticatedUser();
event.locals.idToken = curr.signInUserSession?.idToken?.jwtToken;
const store = curr?.storage?.store || {};
const cookies = cookie.parse(event.request.headers.get("Cookie"));
for (const key in store) {
if (store[key] != cookies[key]) {
newCookies[key] = store[key]
console.log("Updating", key, store[key]?.slice(-8), cookies[key]?.slice(-8));
}
}
} catch(err) {
console.warn(err)
}
const response = await resolve(event);
for (const key in newCookies) {
response.headers.append("Set-Cookie", cookie.serialize(key, newCookies[key]))
}
return response;
}

74
frontend/src/lib/clients/sl3.ts

@ -0,0 +1,74 @@
import type Scope from "$lib/models/scope";
import type User from "$lib/models/user";
import type { AuthResult } from "$lib/models/user";
type FetchImpl = (info: RequestInfo, init?: RequestInit) => Promise<Response>;
export default class SL3APIClient {
private fetcher: FetchImpl
private root: string
private tokenPromise: Promise<string>
constructor(fetcher: FetchImpl, tokenPromise?: Promise<string>, root?: string | boolean) {
this.fetcher = fetcher;
if (typeof(root) === "string") {
this.root = root || "";
} else if (root === true) {
this.root = process.env.STUFFLOG3_API;
} else {
this.root = ""
}
this.tokenPromise = tokenPromise || null;
}
async findScope(id: number): Promise<Scope> {
return this.fetch<{scope: Scope}>("GET", `scopes/${id}`).then(r => r.scope);
}
async listScopes(): Promise<Scope[]> {
return this.fetch<{scopes: Scope[]}>("GET", "scopes").then(r => r.scopes);
}
async autchCheck(): Promise<User> {
return this.fetch<{user: User}>("GET", "auth").then(r => r.user);
}
async authLogin(username: string, password: string): Promise<AuthResult> {
return this.fetch<{auth: AuthResult}>("POST", "auth/login", {username, password}).then(r => r.auth);
}
async authRefresh(refreshToken: string): Promise<AuthResult> {
return this.fetch<{auth: AuthResult}>("POST", "auth/refresh", {refreshToken}).then(r => r.auth);
}
async fetch<T>(method: string, path: string, body: any = null): Promise<T> {
const headers: Record<string, string> = {};
let bodyData: string | undefined = void(0);
if (body != null) {
headers["Content-Type"] = "application/json";
bodyData = JSON.stringify(body);
}
if (this.tokenPromise != null) {
headers["Authorization"] = `Bearer ${await this.tokenPromise}`;
}
const res = await this.fetcher(`${this.root}/api/v1/${path}`, {
method, headers,
body: bodyData,
credentials: "same-origin",
});
if (!res.ok) {
if (!(res.headers.get("content-type")||"").includes("application/json")) {
throw await res.text();
}
throw await res.json();
}
return res.json();
}
}

38
frontend/src/lib/components/common/Card.svelte

@ -0,0 +1,38 @@
<div class="entry">
<slot></slot>
</div>
<style lang="scss">
@import "../../css/colors.sass";
div.entry {
display: block;
text-decoration: none;
color: $color-entry9;
background-color: $color-entry1-transparent;
border-bottom-right-radius: 0.75em;
margin: 0.5em 0;
padding: 0.25em 0.5ch;
transition: 250ms;
overflow-y: hidden;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
}
div.entry:hover {
background-color: $color-entry2-transparent;
color: $color-entry11;
}
:global(div.entry div.entry) {
margin: 0.25em 0;
}
:global(div.entry div.entry) {
border-bottom-right-radius: 0.5em;
}
:global(div.entry div.entry:hover) {
background-color: $color-entry3;
}
div.entry:last-of-type {
margin-bottom: 0.1em;
}
</style>

29
frontend/src/lib/components/common/CardHeader.svelte

@ -0,0 +1,29 @@
<script lang="ts">
export let subtitle: string = ""
</script>
<div class="name">
<span><slot></slot></span>
<span class="abbreviation">{subtitle}</span>
</div>
<style lang="scss">
@import "../../css/colors.sass";
div.name {
font-size: 1em;
font-weight: 600;
}
span.abbreviation {
font-size: 1em;
opacity: $opacity-entry4;
}
@media screen and (max-width: 1200px) {
span.abbreviation {
display: block;
font-size: 0.75em;
}
}
</style>

14
frontend/src/lib/components/common/LinkCard.svelte

@ -0,0 +1,14 @@
<script lang="ts">
import Card from "./Card.svelte";
export let href: string;
</script>
<a href={href}><Card><slot></slot></Card></a>
<style>
a {
text-decoration: none;
color: inherit;
}
</style>

11
frontend/src/lib/components/frontpage/ScopeLink.svelte

@ -0,0 +1,11 @@
<script lang="ts">
import type Scope from "$lib/models/scope";
import CardHeader from "../common/CardHeader.svelte";
import LinkCard from "../common/LinkCard.svelte";
export let scope: Scope;
</script>
<LinkCard href="/{scope.id}-{scope.abbreviation.toLocaleLowerCase()}/">
<CardHeader subtitle={scope.abbreviation}>{scope.name}</CardHeader>
</LinkCard>

64
frontend/src/lib/components/layout/Background.svelte

@ -0,0 +1,64 @@
<script lang="ts">
import {onMount} from "svelte";
export let initialOrientation: "portrait" | "landscape" = "portrait";
export let src = "/background.jpg";
export let opacity = 1.0;
let landscape = (initialOrientation === "landscape");
let image: HTMLImageElement;
onMount(() => {
if (image.complete && typeof(image.naturalHeight) !== 'undefined' && image.naturalHeight !== 0) {
const width = image.naturalWidth;
const height = image.naturalHeight;
landscape = (width > (height * 1.50));
} else {
image.onload = () => {
const width = image.naturalWidth;
const height = image.naturalHeight;
landscape = (width > (height * 1.50));
}
}
});
</script>
<img
bind:this={image}
class:landscape
src={src}
style={`opacity: ${opacity};`}
alt="Background"
/>
<style>
img {
/* Set rules to fill background */
width: 100%;
/* Set up proportionate scaling */
min-height: 100%;
height: auto;
/* Set up positioning */
position: fixed;
top: 0;
left: 0;
user-select: none;
-webkit-user-drag: none;
transition: 250ms;
z-index: -1;
}
img.landscape {
/* Set rules to fill background */
height: 100%;
/* Set up proportionate scaling */
min-width: 100%;
width: auto;
}
</style>

11
frontend/src/lib/components/layout/Column.svelte

@ -0,0 +1,11 @@
<div class="column">
<slot></slot>
</div>
<style lang="sass">
div.column
display: flex
flex-direction: column
flex-basis: 50
flex: 1
</style>

26
frontend/src/lib/components/layout/Columns.svelte

@ -0,0 +1,26 @@
<script lang="ts">
export let wide = false;
</script>
<div class="columns" class:wide>
<slot></slot>
</div>
<style lang="sass">
div.columns
display: flex
flex-direction: row
flex-basis: 100%
flex: 1
width: 80ch
max-width: 95%
margin: auto
@media screen and (max-width: 800px)
width: 100%
flex-direction: column
div.columns.wide
width: 120ch
</style>

46
frontend/src/lib/components/layout/Header.svelte

@ -0,0 +1,46 @@
<script lang="ts">
export let subtitle: string = "";
export let fullwidth: boolean = false;
export let wide: boolean = false;
</script>
<div class="header-wrapper" class:fullwidth class:wide>
<h1><slot></slot></h1>
<h2>{subtitle}</h2>
</div>
<style lang="scss">
@import "../../css/colors.sass";
div.header-wrapper {
&.fullwidth {
width: 80ch;
max-width: 95%;
margin: auto;
margin-top: 2em;
&.wide {
width: 120ch;
}
}
h1 {
margin: 0.333em 0;
margin-bottom: 0;
font-size: 3em;
font-weight: 100;
}
h2 {
font-size: 1em;
margin: 1em 0;
margin-top: 0;
font-style: italic;
font-weight: 100;
&:empty {
display: none;
}
}
}
</style>

22
frontend/src/lib/components/layout/Row.svelte

@ -0,0 +1,22 @@
<script lang="ts">
export let title = "";
</script>
<div class="row">
{#if title != ""}
<h2>{title}</h2>
{/if}
<slot></slot>
</div>
<style lang="sass">
div.row
margin: 0.5em 1ch
padding: 0.5em 1ch
> h2
font-size: 1.25em
margin: 0
margin-bottom: 0.25em
</style>

24
frontend/src/lib/css/colors.sass

@ -0,0 +1,24 @@
$color-entry0: hsl(240, 5%, 7%)
$color-entry0-transparent: hsla(240, 5%, 10%, 0.7)
$color-entry1: hsl(240, 5%, 14%)
$color-entry1-transparent: hsla(240, 5%, 17%, 0.8)
$color-entry2: hsl(240, 5%, 21%)
$color-entry2-transparent: hsla(240, 5%, 24%, 0.8)
$color-entry3: hsl(240, 5%, 28%)
$color-entry4: hsl(240, 5%, 35%)
$color-entry5: hsl(240, 5%, 42%)
$color-entry6: hsl(240, 5%, 49%)
$color-entry7: hsl(240, 5%, 56%)
$color-entry8: hsl(240, 5%, 63%)
$color-entry9: hsl(240, 5%, 70%) // Default
$color-entry10: hsl(240, 5%, 77%)
$color-entry11: hsl(240, 5%, 84%)
$color-entry12: hsl(240, 5%, 91%)
$color-entry13: hsl(240, 5%, 98%)
$opacity-entry4: 0.40
$opacity-entry5: 0.60
$opacity-entry6: 0.80
$color-green: hsl(120, 50%, 74%)
$color-green-dark: hsl(120, 50%, 35%)

16
frontend/src/lib/models/scope.ts

@ -0,0 +1,16 @@
import type Stat from "./stat"
export default interface Scope {
id: number
name: string
abbreviation: string
customLabels: Record<string, string>
members: Record<string, ScopeMember>
stats: Stat[]
}
export interface ScopeMember {
id: string
name: string
owner: boolean
}

12
frontend/src/lib/models/stat.ts

@ -0,0 +1,12 @@
export default interface Stat {
id: number
name: string
weight: number
description: string
allowedAmounts?: StatAllowedAmount[]
}
export interface StatAllowedAmount {
label: string
value: number
}

12
frontend/src/lib/models/user.ts

@ -0,0 +1,12 @@
export default interface User {
id: string
name: string
}
export interface AuthResult {
user?: User
token?: string
refreshToken?: string
session?: string
passwordChangeRequired?: boolean
}

20
frontend/src/routes/__layout.svelte

@ -0,0 +1,20 @@
<script>
import { onMount } from "svelte";
import { page } from "$app/stores";
import Background from "$lib/components/layout/Background.svelte"
import SL3APIClient from "$lib/clients/sl3";
onMount(() => {
setInterval(async() => {
await new SL3APIClient(fetch).autchCheck();
}, 30000)
})
let opacity = 0.175;
$: opacity = $page.url.pathname === "/" ? 0.3 : 0.2;
</script>
<Background initialOrientation="landscape" opacity={opacity} />
<slot></slot>

35
frontend/src/routes/api/[...any].ts

@ -0,0 +1,35 @@
import type { RequestHandler } from "@sveltejs/kit";
export const get: RequestHandler = async({ request, params, locals }) => {
const proxyUrl = `${process.env.STUFFLOG3_API}/api/${params.any}`;
const headers = {
...request.headers,
};
if (locals.idToken != null) {
headers["Authorization"] = `Bearer ${locals.idToken}`;
}
const res = await fetch(proxyUrl, {
method: request.method,
headers: headers,
body: request.body,
});
const result = {
status: res.status,
body: await res.text(),
headers: {},
}
res.headers.forEach((v, k) => {
result.headers[k] = v;
})
return result;
}
export const options = get;
export const post = get;
export const put = get;
export const del = get;

56
frontend/src/routes/index.svelte

@ -0,0 +1,56 @@
<script lang="ts" context="module">
import type { Load } from "@sveltejs/kit/types/internal";
import SL3APIClient from "$lib/clients/sl3";
export const load: Load = async({ fetch, session }) => {
const sl3 = new SL3APIClient(fetch);
try {
const scopes = await sl3.listScopes();
return {
props: {scopes}
};
} catch(err) {
if (err.statusCode === 403 || err.toString()?.includes("not authenticated")) {
return { status: 302, redirect: "/login" }
} else {
throw err;
}
}
}
</script>
<script lang="ts">
import Column from "$lib/components/layout/Column.svelte";
import Columns from "$lib/components/layout/Columns.svelte";
import Header from "$lib/components/layout/Header.svelte";
import Row from "$lib/components/layout/Row.svelte";
import type Scope from "$lib/models/scope";
import ScopeLink from "$lib/components/frontpage/ScopeLink.svelte";
export let scopes: Scope[] = [];
</script>
<Header fullwidth subtitle="Logging your stuff">Stufflog</Header>
<Columns>
<Column>
<Row title="Scopes">
{#each scopes as scope (scope.id)}
<ScopeLink scope={scope} />
{/each}
</Row>
<Row title="Schedule">
</Row>
<Row title="Today">
</Row>
</Column>
<Column>
<Row title="Sprints">
<p>Stuff</p>
</Row>
</Column>
</Columns>

42
frontend/src/routes/login.svelte

@ -0,0 +1,42 @@
<script lang="ts">
import { Amplify, Auth } from 'aws-amplify';
import { goto } from '$app/navigation';
Amplify.configure({
Auth: {
region: import.meta.env.VITE_STUFFLOG3_AWS_POOL_REGION,
userPoolId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_ID,
userPoolWebClientId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_CLIENT_ID,
},
ssr: true,
});
let username = '';
let password = '';
async function handleSignIn() {
try {
const res = await Auth.signIn(username, password);
} catch (error) {
console.log(error);
}
goto('/');
}
</script>
<form on:submit|preventDefault={handleSignIn}>
<input
type="text"
bind:value={username}
/>
<input
id="Password"
label="Password"
type="password"
bind:value={password}
/>
<button type="submit">Sign In</button>
</form>

BIN
frontend/static/background.jpg

After

Width: 1920  |  Height: 1080  |  Size: 495 KiB

BIN
frontend/static/favicon.png

After

Width: 128  |  Height: 128  |  Size: 1.5 KiB

15
frontend/svelte.config.js

@ -0,0 +1,15 @@
import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
adapter: adapter()
}
};
export default config;

36
frontend/tsconfig.json

@ -0,0 +1,36 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020", "DOM"],
"target": "es2020",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.
*/
"importsNotUsedAsValues": "error",
/**
TypeScript doesn't know about import usages in the template because it only sees the
script of a Svelte file. Therefore preserve all value imports. Requires TS 4.5 or higher.
*/
"preserveValueImports": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
}

3
go.mod

@ -5,9 +5,9 @@ go 1.18
require (
github.com/Masterminds/squirrel v1.5.2
github.com/aws/aws-sdk-go v1.44.27
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/gin-gonic/gin v1.7.7
github.com/go-sql-driver/mysql v1.6.0
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/lestrrat-go/jwx v1.2.25
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
)
@ -34,6 +34,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/snabb/isoweek v1.0.1 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect

2
go.sum

@ -63,6 +63,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/snabb/isoweek v1.0.1 h1:B4IsN2GU8lCNVkaUUgOzaVpPkKC2DdY9zcnxz5yc0qg=
github.com/snabb/isoweek v1.0.1/go.mod h1:CAijAxH7NMgjqGc9baHMDE4sTHMt4B/f6X/XLiEE1iA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

106
ports/cognitoauth/client.go

@ -53,6 +53,49 @@ func (c *Client) ListUsers(ctx context.Context) ([]entities.User, error) {
return users, nil
}
func (c *Client) RefreshUser(ctx context.Context, token, refreshToken string) (*entities.AuthResult, error) {
user := c.parseToken(token)
if user == nil {
return nil, models.PermissionDeniedError{}
}
mac := hmac.New(sha256.New, []byte(c.poolClientSecret))
mac.Write([]byte(user.Name + c.poolClientID))
secretHash := base64.StdEncoding.EncodeToString(mac.Sum(nil))
cognitoClient := cognitoidentityprovider.New(c.session)
res, err := cognitoClient.InitiateAuthWithContext(ctx, &cognitoidentityprovider.InitiateAuthInput{
AuthFlow: aws.String("REFRESH_TOKEN_AUTH"),
AuthParameters: map[string]*string{
"REFRESH_TOKEN": &refreshToken,
"SECRET_HASH": &secretHash,
},
ClientId: aws.String(c.poolClientID),
})
if err != nil {
return nil, err
}
if res.AuthenticationResult == nil || res.AuthenticationResult.IdToken == nil {
return nil, models.PermissionDeniedError{}
}
idToken := *res.AuthenticationResult.IdToken
if res.AuthenticationResult.RefreshToken != nil {
refreshToken = *res.AuthenticationResult.RefreshToken
} else {
refreshToken = ""
}
return &entities.AuthResult{
User: user,
Token: idToken,
RefreshToken: refreshToken,
}, nil
}
func (c *Client) LoginUser(ctx context.Context, username, password string) (*entities.AuthResult, error) {
mac := hmac.New(sha256.New, []byte(c.poolClientSecret))
mac.Write([]byte(username + c.poolClientID))
@ -91,9 +134,15 @@ func (c *Client) LoginUser(ctx context.Context, username, password string) (*ent
idToken := *res.AuthenticationResult.IdToken
refreshToken := ""
if res.AuthenticationResult.RefreshToken != nil {
refreshToken = *res.AuthenticationResult.RefreshToken
}
return &entities.AuthResult{
User: c.ValidateToken(nil, idToken),
Token: idToken,
User: c.ValidateToken(nil, idToken),
Token: idToken,
RefreshToken: refreshToken,
}, nil
}
@ -127,27 +176,7 @@ func (c *Client) SetupUser(ctx context.Context, session, username, newPassword s
return c.ValidateToken(ctx, idToken), nil
}
func (c *Client) ValidateToken(_ context.Context, token string) *entities.User {
_, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != jwa.RS256.String() { // jwa.RS256.String() works as well
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
key, ok := c.keySet.LookupKeyID(kid)
if !ok {
return nil, fmt.Errorf("key %v not found", kid)
}
var raw interface{}
err := key.Raw(&raw)
return raw, err
}, jwt.WithoutAudienceValidation())
if err != nil {
return nil
}
func (c *Client) parseToken(token string) *entities.User {
split := strings.SplitN(token, ".", 3)
if len(split) != 3 {
return nil
@ -168,13 +197,36 @@ func (c *Client) ValidateToken(_ context.Context, token string) *entities.User {
if sub, ok := m["custom:actual_userid"].(string); ok {
userID = sub
}
if actualUserID, ok := m["custom:override_sub"].(string); ok {
userID = actualUserID
}
userName, _ := m["cognito:username"].(string)
return &entities.User{
ID: userID,
ID: userID,
Name: userName,
}
}
func (c *Client) ValidateToken(_ context.Context, token string) *entities.User {
_, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != jwa.RS256.String() { // jwa.RS256.String() works as well
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
key, ok := c.keySet.LookupKeyID(kid)
if !ok {
return nil, fmt.Errorf("key %v not found", kid)
}
var raw interface{}
err := key.Raw(&raw)
return raw, err
}, jwt.WithoutAudienceValidation())
if err != nil {
return nil
}
return c.parseToken(token)
}
func New(regionId, clientID, clientSecret, poolId, poolClientId, poolClientSecret string) (*Client, error) {

32
ports/httpapi/auth.go

@ -12,8 +12,9 @@ import (
)
type loginInput struct {
Username string `json:"username"`
Password string `json:"password"`
Username string `json:"username"`
Password string `json:"password"`
RefreshToken string `json:"refreshToken"`
}
type setupInput struct {
@ -24,12 +25,12 @@ type setupInput struct {
func Auth(g *gin.RouterGroup, service *auth.Service) {
g.GET("", handler("user", func(c *gin.Context) (interface{}, error) {
user := service.GetUser(c.Request.Context())
if user == nil {
token := c.GetHeader("Authorization")
if len(token) < 8 {
return nil, models.PermissionDeniedError{}
}
return user, nil
return service.Provider.ValidateToken(c.Request.Context(), token[7:]), nil
}))
g.POST("/login", handler("auth", func(c *gin.Context) (interface{}, error) {
@ -45,6 +46,27 @@ func Auth(g *gin.RouterGroup, service *auth.Service) {
return service.Provider.LoginUser(c.Request.Context(), input.Username, input.Password)
}))
g.POST("/refresh", handler("auth", func(c *gin.Context) (interface{}, error) {
input := &loginInput{}
err := c.BindJSON(input)
if err != nil {
return nil, models.BadInputError{
Object: "LoginInput",
Problem: "Invalid JSON: " + err.Error(),
}
}
token := c.GetHeader("Authorization")
if len(token) < 8 || input.RefreshToken == "" {
return nil, models.BadInputError{
Object: "LoginInput",
Problem: "Missing token(s)",
}
}
return service.Provider.RefreshUser(c.Request.Context(), token[7:], input.RefreshToken)
}))
g.POST("/setup", handler("auth", func(c *gin.Context) (interface{}, error) {
input := &setupInput{}
err := c.BindJSON(input)

1
usecases/auth/provider.go

@ -8,6 +8,7 @@ import (
type Provider interface {
ListUsers(ctx context.Context) ([]entities.User, error)
LoginUser(ctx context.Context, username, password string) (*entities.AuthResult, error)
RefreshUser(ctx context.Context, token, refreshToken string) (*entities.AuthResult, error)
SetupUser(ctx context.Context, session, username, newPassword string) (*entities.User, error)
ValidateToken(ctx context.Context, token string) *entities.User
}

4
usecases/auth/service.go

@ -15,6 +15,10 @@ func (s *Service) Login(ctx context.Context, username, password string) (*entiti
return s.Provider.LoginUser(ctx, username, password)
}
func (s *Service) Refresh(ctx context.Context, token, refreshToken string) (*entities.AuthResult, error) {
return s.Provider.LoginUser(ctx, token, refreshToken)
}
func (s *Service) Setup(ctx context.Context, session, username, newPassword string) (*entities.User, error) {
return s.Provider.SetupUser(ctx, session, username, newPassword)
}

Loading…
Cancel
Save