diff --git a/.drone.yml b/.drone.yml index ddbc078..7b5b258 100644 --- a/.drone.yml +++ b/.drone.yml @@ -51,10 +51,11 @@ type: docker name: frontend steps: - name: prettier - image: elnebuloso/prettier + image: node:alpine commands: - cd docker/frontend/ - - prettier . -c + - yarn global add prettier eslint + - yarn lint - name: build image: tmaier/docker-compose volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 256429b..8bc5be0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,6 +54,9 @@ services: - default - db - redis + depends_on: + - db + - redis environment: URL: ${URL} BACKEND_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER} @@ -65,9 +68,6 @@ services: BACKEND_SMTP_NAME: ${BACKEND_SMTP_NAME} BACKEND_SMTP_USERNAME: ${BACKEND_SMTP_USERNAME} BACKEND_SMTP_PASSWORD: ${BACKEND_SMTP_PASSWORD} - depends_on: - - db - - redis frontend: build: @@ -80,6 +80,8 @@ services: - default depends_on: - backend + environment: + NODE_ENV: production networks: db: diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 26ea8eb..83f53c6 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -1,10 +1,10 @@ -FROM crystallang/crystal:latest-alpine as deps +FROM crystallang/crystal:1.3-alpine as deps WORKDIR /app RUN apk add curl --no-cache COPY ./shard.yml ./shard.lock ./ RUN shards install --production -FROM crystallang/crystal:latest-alpine as builder +FROM crystallang/crystal:1.3-alpine as builder ARG BUILD_ENV WORKDIR /app COPY --from=deps /app/shard.yml /app/shard.lock ./ diff --git a/docker/backend/src/backend/api/auth.cr b/docker/backend/src/backend/api/auth.cr index 45f8266..7b35690 100644 --- a/docker/backend/src/backend/api/auth.cr +++ b/docker/backend/src/backend/api/auth.cr @@ -22,7 +22,7 @@ module Backend JWT.encode({"data" => data.to_h, "exp" => expiration}, SAFE_ENV["BACKEND_JWT_SECRET"], JWT::Algorithm::HS256) end - def create_user_jwt(user_id : Int, expiration : Int = (Time.utc + Time::Span.new(hours: 6)).to_unix) : String + def create_user_jwt(user_id : Int, expiration : Int = (Time.utc + Time::Span.new(days: 1)).to_unix) : String create_jwt({user_id: user_id}, expiration) end diff --git a/docker/backend/src/backend/api/schema/mutation.cr b/docker/backend/src/backend/api/schema/mutation.cr index dc35a86..cb0c70e 100644 --- a/docker/backend/src/backend/api/schema/mutation.cr +++ b/docker/backend/src/backend/api/schema/mutation.cr @@ -1,12 +1,16 @@ +require "CrystalEmail" + module Backend module API module Schema @[GraphQL::Object] class Mutation < GraphQL::BaseMutation @[GraphQL::Field] - def login(input : LoginInput) : LoginPayload - user = Db::User.find_by(email: input.email) - raise "Auth failed" unless user && Auth.verify_password?(input.password, user.password) + def login(email : String, password : String) : LoginPayload + raise "Auth failed" if email.empty? || password.empty? || !CrystalEmail::Rfc5322::Public.validates?(email) + + user = Db::User.find_by(email: email) + raise "Auth failed" unless user && Auth.verify_password?(password, user.password) LoginPayload.new( user: User.new(user), diff --git a/docker/backend/src/backend/api/schema/user.cr b/docker/backend/src/backend/api/schema/user.cr index eb631d6..51fe036 100644 --- a/docker/backend/src/backend/api/schema/user.cr +++ b/docker/backend/src/backend/api/schema/user.cr @@ -102,18 +102,18 @@ module Backend end end - @[GraphQL::InputObject] - class LoginInput < GraphQL::BaseInputObject - getter email - getter password + # @[GraphQL::InputObject] + # class LoginInput < GraphQL::BaseInputObject + # getter email + # getter password - @[GraphQL::Field] - def initialize( - @email : String, - @password : String - ) - end - end + # @[GraphQL::Field] + # def initialize( + # @email : String, + # @password : String + # ) + # end + # end @[GraphQL::Object] class LoginPayload < GraphQL::BaseObject diff --git a/docker/backend/src/backend/run.cr b/docker/backend/src/backend/run.cr index 3d77ffd..4865f43 100644 --- a/docker/backend/src/backend/run.cr +++ b/docker/backend/src/backend/run.cr @@ -2,7 +2,10 @@ module Backend extend self def run : Nil - Log.info { "Starting backend services..." } + {% if flag?(:development) %} + Log.warn { "Backend is running in development mode! Do not use this in production!" } + {% end %} + Log.info { "Starting services..." } channel = Channel(Nil).new(SERVICES.size) diff --git a/docker/frontend/.nvmrc b/docker/frontend/.nvmrc deleted file mode 100644 index 53a4221..0000000 --- a/docker/frontend/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v16.13.2 diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index facf062..a5b3442 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -1,5 +1,5 @@ # Install dependencies only when needed -FROM node:14-alpine AS deps +FROM node:16-alpine AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app @@ -7,27 +7,24 @@ COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile # Rebuild the source code only when needed -FROM node:14-alpine AS builder +FROM node:16-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN yarn build # Production image, copy all the files and run next -FROM node:14-alpine AS runner +FROM node:16-alpine AS runner WORKDIR /app -ARG BUILD_ENV -ENV NODE_ENV ${BUILD_ENV} - RUN addgroup -g 1001 -S nodejs RUN adduser -S nextjs -u 1001 # You only need to copy next.config.js if you are NOT using the default configuration # COPY --from=builder /app/next.config.js ./ +COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next -COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json USER nextjs @@ -39,4 +36,7 @@ EXPOSE 3000 # Uncomment the following line in case you want to disable telemetry. ENV NEXT_TELEMETRY_DISABLED 1 +ARG BUILD_ENV +ENV NODE_ENV ${BUILD_ENV} + CMD ["yarn", "start"] \ No newline at end of file diff --git a/docker/frontend/components/navbar.tsx b/docker/frontend/components/navbar.tsx new file mode 100644 index 0000000..e1a08b7 --- /dev/null +++ b/docker/frontend/components/navbar.tsx @@ -0,0 +1,29 @@ +import Cookies from "js-cookie"; +import Link from "next/link"; + +function Navbar(): JSX.Element { + const isLoggedIn = !!Cookies.get("mentorenwahl_bearer"); + + function handleLogout(event: MouseEvent): void { + event.preventDefault(); + Cookies.remove("mentorenwahl_bearer"); + } + + return ( + + ); +} + +export default Navbar; diff --git a/docker/frontend/layouts/main.tsx b/docker/frontend/layouts/main.tsx new file mode 100644 index 0000000..ebf8287 --- /dev/null +++ b/docker/frontend/layouts/main.tsx @@ -0,0 +1,16 @@ +import Navbar from "../components/navbar"; + +interface MainLayoutProps { + children: React.ReactNode; +} + +function MainLayout({ children }: MainLayoutProps): JSX.Element { + return ( +
+ +
{children}
+
+ ); +} + +export default MainLayout; diff --git a/docker/frontend/lib/client.ts b/docker/frontend/lib/client.ts deleted file mode 100644 index bf324f4..0000000 --- a/docker/frontend/lib/client.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ApolloClient, InMemoryCache } from "@apollo/client"; - -export const client = new ApolloClient({ - uri: "/graphql", - cache: new InMemoryCache(), -}); diff --git a/docker/frontend/lib/cookieNames.ts b/docker/frontend/lib/cookieNames.ts new file mode 100644 index 0000000..ebbdd54 --- /dev/null +++ b/docker/frontend/lib/cookieNames.ts @@ -0,0 +1,2 @@ +export const _BASE = "mentorenwahl_"; +export const TOKEN = _BASE + "token"; diff --git a/docker/frontend/package.json b/docker/frontend/package.json index e802a98..0b56484 100644 --- a/docker/frontend/package.json +++ b/docker/frontend/package.json @@ -10,11 +10,13 @@ "dependencies": { "@apollo/client": "^3.5.8", "graphql": "^16.3.0", + "js-cookie": "^3.0.1", "next": "12.0.9", "react": "17.0.2", "react-dom": "17.0.2" }, "devDependencies": { + "@types/js-cookie": "^3.0.1", "@types/node": "17.0.13", "@types/react": "17.0.38", "eslint": "8.8.0", diff --git a/docker/frontend/pages/_app.tsx b/docker/frontend/pages/_app.tsx index e9a8374..4916484 100644 --- a/docker/frontend/pages/_app.tsx +++ b/docker/frontend/pages/_app.tsx @@ -1,9 +1,22 @@ import type { AppProps } from "next/app"; +import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client"; +import MainLayout from "../layouts/main"; import "../styles/globals.css"; -function MyApp({ Component, pageProps }: AppProps) { - return ; +const client = new ApolloClient({ + uri: "/graphql", + cache: new InMemoryCache(), +}); + +function MyApp({ Component, pageProps }: AppProps): JSX.Element { + return ( + + + + + + ); } export default MyApp; diff --git a/docker/frontend/pages/index.tsx b/docker/frontend/pages/index.tsx index e92501f..b009802 100644 --- a/docker/frontend/pages/index.tsx +++ b/docker/frontend/pages/index.tsx @@ -1,11 +1,22 @@ -import type { NextPage } from "next"; +import Cookies from "js-cookie"; +import Router from "next/router"; +import { useEffect } from "react"; -const Home: NextPage = () => { - return ( -
-

Willkommen zur Mentorenwahl!

-
- ); -}; +import * as cookieNames from "../lib/cookieNames"; + +function Home(): JSX.Element { + const token = Cookies.get(cookieNames.TOKEN); + useEffect(() => { + if (!token) { + Router.push("/login"); + } + }, [token]); + + if (!token) { + return <>; + } + + return

Du bist eingeloggt!

; +} export default Home; diff --git a/docker/frontend/pages/login.tsx b/docker/frontend/pages/login.tsx index 398ea51..47fd454 100644 --- a/docker/frontend/pages/login.tsx +++ b/docker/frontend/pages/login.tsx @@ -1,39 +1,40 @@ -import type { NextPage } from "next"; -import type { FormEvent } from "react"; -import { gql } from "@apollo/client"; +import type { FormEvent, FormEventHandler } from "react"; +import { gql, useMutation } from "@apollo/client"; +import Cookies from "js-cookie"; +import Router from "next/router"; -import { client } from "../lib/client"; +import * as cookieNames from "../lib/cookieNames"; -const Login: NextPage = () => { - async function loginUser(event: FormEvent): Promise { +const LOGIN = gql` + mutation Login($email: String!, $password: String!) { + login(email: $email, password: $password) { + token + } + } +`; + +function Login(): JSX.Element { + const [login, { error }] = useMutation(LOGIN); + + const loginUser: FormEventHandler = async ( + event: FormEvent & { target: HTMLFormElement } + ): Promise => { event.preventDefault(); const input = { - email: (event.target as HTMLFormElement).email.value, - password: (event.target as HTMLFormElement).password.value, + email: event.target.email.value as string, + password: event.target.password.value as string, }; - console.log(input); - - // client - // .mutate({ - // mutation: gql` - // mutation Login($input: LoginInput!) { - // login(input: $input) { - // user { - // id - // firstname - // lastname - // email - // } - // bearer - // } - // } - // `, - // }) - // .then((res) => { - // console.log(res); - // }); - } + const data = ( + await login({ + variables: { email: input.email, password: input.password }, + }) + ).data; + if (data) { + Cookies.set(cookieNames.TOKEN, data.login.token, { expires: 1 }); + Router.push("/"); + } + }; return (
@@ -54,8 +55,9 @@ const Login: NextPage = () => {
+ {error &&

{error.message}

}
); -}; +} export default Login; diff --git a/docker/frontend/yarn.lock b/docker/frontend/yarn.lock index 29b38b0..c832be9 100644 --- a/docker/frontend/yarn.lock +++ b/docker/frontend/yarn.lock @@ -162,6 +162,11 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== +"@types/js-cookie@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.1.tgz#04aa743e2e0a85a22ee9aa61f6591a8bc19b5d68" + integrity sha512-7wg/8gfHltklehP+oyJnZrz9XBuX5ZPP4zB6UsI84utdlkRYLnOm2HfpLXazTwZA+fpGn0ir8tGNgVnMEleBGQ== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1090,6 +1095,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +js-cookie@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" + integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== + "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"