From 09d1940ce00a64ba5671e9455d72dcc6e72fa229 Mon Sep 17 00:00:00 2001 From: valia fetisov Date: Thu, 13 Apr 2023 12:24:00 +0200 Subject: [PATCH] feat: Frontend auth (#38) --- .../workflows/build-and-deploy-staging.yaml | 3 +- .github/workflows/ci.yaml | 1 + api/src/generated/nexus.ts | 20 +- api/src/generated/schema.graphql | 10 +- api/src/helpers/token.ts | 2 +- api/src/modules/Session/model.ts | 5 +- api/src/modules/User/model.ts | 14 +- api/tests/helpers/const.ts | 2 +- docker-compose.yaml | 3 +- frontend/Dockerfile | 16 +- frontend/README.md | 4 + frontend/app.vue | 16 +- frontend/components/auth/SessionForm.vue | 103 + frontend/components/auth/SessionTable.vue | 106 + frontend/components/auth/SignInForm.vue | 96 + frontend/components/auth/UserMenu.vue | 24 + frontend/components/auth/WrapperContainer.vue | 13 + frontend/components/layout/TheHeader.vue | 48 +- frontend/composables/useAuth.ts | 96 + frontend/containers/SessionContainer.vue | 54 + frontend/graphql/createSession.gql | 9 + frontend/graphql/me.gql | 6 + frontend/graphql/revokeSession.gql | 6 + frontend/graphql/sessions.gql | 12 + frontend/graphql/signIn.gql | 5 + frontend/graphql/signUp.gql | 5 + frontend/nuxt.config.ts | 21 +- frontend/package-lock.json | 24093 ++++++++++------ frontend/package.json | 9 +- frontend/pages/graphql-playground.vue | 7 +- frontend/pages/index.vue | 2 +- frontend/pages/user/index.vue | 29 +- 32 files changed, 15173 insertions(+), 9667 deletions(-) create mode 100644 frontend/components/auth/SessionForm.vue create mode 100644 frontend/components/auth/SessionTable.vue create mode 100644 frontend/components/auth/SignInForm.vue create mode 100644 frontend/components/auth/UserMenu.vue create mode 100644 frontend/components/auth/WrapperContainer.vue create mode 100644 frontend/composables/useAuth.ts create mode 100644 frontend/containers/SessionContainer.vue create mode 100644 frontend/graphql/createSession.gql create mode 100644 frontend/graphql/me.gql create mode 100644 frontend/graphql/revokeSession.gql create mode 100644 frontend/graphql/sessions.gql create mode 100644 frontend/graphql/signIn.gql create mode 100644 frontend/graphql/signUp.gql diff --git a/.github/workflows/build-and-deploy-staging.yaml b/.github/workflows/build-and-deploy-staging.yaml index 9c56c3e9..1a38c044 100644 --- a/.github/workflows/build-and-deploy-staging.yaml +++ b/.github/workflows/build-and-deploy-staging.yaml @@ -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: | diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 91f33924..ef7490c9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,6 +9,7 @@ on: env: DATABASE_URL: file:./db.sqlite AUTH_SIGNUP_ENABLED: 1 + jobs: api-test: diff --git a/api/src/generated/nexus.ts b/api/src/generated/nexus.ts index fd906ce9..f023c6d3 100644 --- a/api/src/generated/nexus.ts +++ b/api/src/generated/nexus.ts @@ -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 @@ -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! } } @@ -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 @@ -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! } } diff --git a/api/src/generated/schema.graphql b/api/src/generated/schema.graphql index f629098a..5646be89 100644 --- a/api/src/generated/schema.graphql +++ b/api/src/generated/schema.graphql @@ -3,8 +3,8 @@ type AuthPayload { - session: Session - token: String + session: Session! + token: String! } type CoreUnit { @@ -57,9 +57,9 @@ type SessionCreateOutput { } type User { - id: String - password: String - username: String + id: String! + password: String! + username: String! } input UserNamePass { diff --git a/api/src/helpers/token.ts b/api/src/helpers/token.ts index bd46990f..dcc8961b 100644 --- a/api/src/helpers/token.ts +++ b/api/src/helpers/token.ts @@ -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 diff --git a/api/src/modules/Session/model.ts b/api/src/modules/Session/model.ts index 5bd01d85..ff8f362d 100644 --- a/api/src/modules/Session/model.ts +++ b/api/src/modules/Session/model.ts @@ -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'; @@ -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({ diff --git a/api/src/modules/User/model.ts b/api/src/modules/User/model.ts index fa0947b9..eb79079e 100644 --- a/api/src/modules/User/model.ts +++ b/api/src/modules/User/model.ts @@ -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'); }, }); @@ -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' }); }, }); @@ -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) { @@ -59,7 +59,7 @@ export function getUserCrud(prisma: PrismaClient) { try { createdUser = await prisma.user.create({ data: { - username, + username: username.toLocaleLowerCase(), password: hashedPassword, }, }); diff --git a/api/tests/helpers/const.ts b/api/tests/helpers/const.ts index 5bfddfaf..6e8b1fa0 100644 --- a/api/tests/helpers/const.ts +++ b/api/tests/helpers/const.ts @@ -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 = ( diff --git a/docker-compose.yaml b/docker-compose.yaml index 2212356c..dca4960c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -35,7 +35,8 @@ services: frontend: restart: unless-stopped build: - context: ./frontend + context: . + dockerfile: ./frontend/Dockerfile expose: - 3000 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index cd6835ce..bf7c8720 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -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 @@ -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" ] diff --git a/frontend/README.md b/frontend/README.md index ca54ff6c..9070d834 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -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`. diff --git a/frontend/app.vue b/frontend/app.vue index fa29d274..e82d43a3 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -1,12 +1,18 @@ + +