Skip to content

Commit

Permalink
feat: Frontend auth (powerhouse-inc#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
valiafetisov authored Apr 13, 2023
1 parent 4696c87 commit 09d1940
Show file tree
Hide file tree
Showing 32 changed files with 15,173 additions and 9,667 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/build-and-deploy-staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ jobs:
- name: Build and Push switchboard-frontend
uses: docker/build-push-action@v3
with:
context: ./frontend
context: .
dockerfile: ./frontend/Dockerfile
platforms: linux/amd64
push: true
tags: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
env:
DATABASE_URL: file:./db.sqlite
AUTH_SIGNUP_ENABLED: 1

jobs:

api-test:
Expand Down
20 changes: 10 additions & 10 deletions api/src/generated/nexus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export interface NexusGenScalars {

export interface NexusGenObjects {
AuthPayload: { // root type
session?: NexusGenRootTypes['Session'] | null; // Session
token?: string | null; // String
session: NexusGenRootTypes['Session']; // Session!
token: string; // String!
}
CoreUnit: { // root type
code?: string | null; // String
Expand Down Expand Up @@ -94,9 +94,9 @@ export interface NexusGenObjects {
token: string; // String!
}
User: { // root type
id?: string | null; // String
password?: string | null; // String
username?: string | null; // String
id: string; // String!
password: string; // String!
username: string; // String!
}
}

Expand All @@ -112,8 +112,8 @@ export type NexusGenAllTypes = NexusGenRootTypes & NexusGenScalars

export interface NexusGenFieldTypes {
AuthPayload: { // field return type
session: NexusGenRootTypes['Session'] | null; // Session
token: string | null; // String
session: NexusGenRootTypes['Session']; // Session!
token: string; // String!
}
CoreUnit: { // field return type
code: string | null; // String
Expand Down Expand Up @@ -152,9 +152,9 @@ export interface NexusGenFieldTypes {
token: string; // String!
}
User: { // field return type
id: string | null; // String
password: string | null; // String
username: string | null; // String
id: string; // String!
password: string; // String!
username: string; // String!
}
}

Expand Down
10 changes: 5 additions & 5 deletions api/src/generated/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@


type AuthPayload {
session: Session
token: String
session: Session!
token: String!
}

type CoreUnit {
Expand Down Expand Up @@ -57,9 +57,9 @@ type SessionCreateOutput {
}

type User {
id: String
password: String
username: String
id: String!
password: String!
username: String!
}

input UserNamePass {
Expand Down
2 changes: 1 addition & 1 deletion api/src/helpers/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const jwtSchema = z.object({
exp: z.optional(z.number()),
});

export const format = (token: string) => `${token.slice(0, 3)}...${token.slice(-3)}`;
export const format = (token: string) => `${token.slice(0, 4)}...${token.slice(-4)}`;

/** Generate a JWT token
* - If expiryDurationSeconds is null, the token will never expire
Expand Down
5 changes: 4 additions & 1 deletion api/src/modules/Session/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PrismaClient, Prisma } from '@prisma/client';
import { inputObjectType, objectType } from 'nexus/dist';
import { PrismaClient, Prisma } from '@prisma/client';
import { randomUUID } from 'crypto';
import { GraphQLError } from 'graphql';
import ms from 'ms';
Expand Down Expand Up @@ -80,6 +80,9 @@ export function getSessionCrud(prisma: PrismaClient) {
where: {
createdBy: userId,
},
orderBy: {
createdAt: 'desc',
},
}),
revoke: async (sessionId: string, userId: string) => {
const session = await prisma.session.findUnique({
Expand Down
14 changes: 7 additions & 7 deletions api/src/modules/User/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
export const User = objectType({
name: 'User',
definition(t) {
t.string('id');
t.string('username');
t.string('password');
t.nonNull.string('id');
t.nonNull.string('username');
t.nonNull.string('password');
},
});

Expand All @@ -26,8 +26,8 @@ export const UserNamePass = inputObjectType({
export const AuthPayload = objectType({
name: 'AuthPayload',
definition(t) {
t.string('token');
t.field('session', { type: 'Session' });
t.nonNull.string('token');
t.nonNull.field('session', { type: 'Session' });
},
});

Expand All @@ -37,7 +37,7 @@ export function getUserCrud(prisma: PrismaClient) {
const { username, password } = userNamePass;
const user = await prisma.user.findUnique({
where: {
username,
username: username.toLocaleLowerCase(),
},
});
if (!user) {
Expand All @@ -59,7 +59,7 @@ export function getUserCrud(prisma: PrismaClient) {
try {
createdUser = await prisma.user.create({
data: {
username,
username: username.toLocaleLowerCase(),
password: hashedPassword,
},
});
Expand Down
2 changes: 1 addition & 1 deletion api/tests/helpers/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import builder from 'gql-query-builder';

export const USERNAME = 'usernameTest';
export const USERNAME = 'usernametest';
export const PASSWORD = 'passwordTest';

export const getSignUpMutation = (
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ services:
frontend:
restart: unless-stopped
build:
context: ./frontend
context: .
dockerfile: ./frontend/Dockerfile
expose:
- 3000

Expand Down
16 changes: 9 additions & 7 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@ ARG NODE_VERSION=node:16.14.2
FROM $NODE_VERSION AS dependency-base

# create destination directory
RUN mkdir -p /app
WORKDIR /app
RUN mkdir -p /app/frontend
WORKDIR /app/frontend

# copy the app, note .dockerignore
COPY package.json .
COPY package-lock.json .
COPY frontend/package.json .
COPY frontend/package-lock.json .
RUN npm ci

FROM dependency-base AS production-base

# build will also take care of building
# if necessary
COPY . .
COPY frontend /app/frontend
COPY api /app/api
WORKDIR /app/frontend
RUN npm run build

FROM $NODE_VERSION AS production

COPY --from=production-base /app/.output /app/.output
COPY --from=production-base /app/frontend/.output /app/frontend/.output

# Service hostname
ENV NUXT_HOST=0.0.0.0
Expand All @@ -34,4 +36,4 @@ ENV NUXT_APP_VERSION=${NUXT_APP_VERSION}
ENV NODE_ENV=production

# start the app
CMD [ "node", "/app/.output/server/index.mjs" ]
CMD [ "node", "/app/frontend/.output/server/index.mjs" ]
4 changes: 4 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The UI part of the system that intends to provide user-friendly wrapper over the

Please refer to the root readme to learn more about general development setup.

### Environment variables

- `API_ORIGIN` (optional, default `/api`): relative path or the url under which `api` service is running. For example, if the `api` service is running on port 4000, the value should be `http://localhost:4000`. However, if the `api` is sharing the origin with the `frontend` service via reverse-proxy, providing relative path is enough (e.g.: `/api`)

## Production

You can build application for production using `npm run build` and then locally preview production build via `npm run preview`.
Expand Down
16 changes: 11 additions & 5 deletions frontend/app.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<template>
<div class="h-screen flex flex-col">
<LayoutTheHeader class="flex-shrink-0 h-14" />
<div class="flex-grow">
<NuxtPage />
<NMessageProvider>
<div class="h-screen flex flex-col">
<LayoutTheHeader :user="user" :is-authorized="isAuthorized" class="flex-shrink-0 h-14 fixed top-0 z-10" />
<div class="flex-grow mt-14">
<NuxtPage :user="user" />
</div>
</div>
</div>
</NMessageProvider>
</template>

<script lang="ts" setup>
const { isAuthorized, user } = useAuth()
</script>

<style>
html, body {
@apply bg-neutral-50;
Expand Down
103 changes: 103 additions & 0 deletions frontend/components/auth/SessionForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<template>
<form class="flex items-end gap-5" @submit.prevent="create">
<label class="w-full"><span class="font-semibold">Name</span>
<n-input v-model:value="name" placeholder="Name" />
</label>
<label class="w-full"><span class="font-semibold">Duration</span>
<n-select v-model:value="expiryDurationSeconds" :options="options" clearable placeholder="Please select" />
</label>
<n-button
attr-type="submit"
type="primary"
class="!w-52"
:loading="isCreating"
:disabled="isCreationDisabed"
>
Create new token
</n-button>
</form>
<n-modal :show="!!createdToken">
<n-card
style="width: 570px"
title="Created token"
size="small"
role="dialog"
aria-modal="true"
>
<div class="bg-neutral-100 p-3 rounded border-2 border-neutral-200">
{{ createdToken }}
</div>
<div class="text-neutral-400 text-sm my-5">
Note: the token is not stored in our database, so it is only displayed
once to you. Please make sure you've copied it into a secure place before closing this window
</div>
<n-button
type="primary"
class="!w-full"
@click="createdToken = ''"
>
I have saved the token
</n-button>
</n-card>
</n-modal>
</template>

<script lang="ts" setup>
import { useMessage, NButton } from 'naive-ui'
const props = defineProps({
createSession: {
type: Function,
required: true
}
})
const options = [
{
label: '1 hour',
value: 60 * 60
},
{
label: '1 day',
value: 60 * 60 * 24
},
{
label: '1 week',
value: 60 * 60 * 24 * 7
},
{
label: '1 month',
value: 60 * 60 * 24 * 30
},
{
label: '1 year',
value: 60 * 60 * 24 * 365
},
{
label: 'Non expiring',
value: undefined
}
]
const message = useMessage()
const isCreating = ref(false)
const name = ref('')
const expiryDurationSeconds = ref(null)
const createdToken = ref('')
const isCreationDisabed = computed(() => !name.value || expiryDurationSeconds.value === null)
const create = async () => {
isCreating.value = true
try {
const token = await props.createSession(name.value, expiryDurationSeconds.value ?? null)
name.value = ''
expiryDurationSeconds.value = null
createdToken.value = token
} catch (error: any) {
console.error('Failed to create new token', error)
message.error(`Failed to create new token: ${error?.message}`)
} finally {
isCreating.value = false
}
}
</script>
Loading

0 comments on commit 09d1940

Please sign in to comment.