Added login page
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing

This commit is contained in:
Dominic Grimm 2022-02-02 15:38:36 +01:00
parent 1b2fcf7ba8
commit 0da7f11239
18 changed files with 166 additions and 78 deletions

View file

@ -51,10 +51,11 @@ type: docker
name: frontend name: frontend
steps: steps:
- name: prettier - name: prettier
image: elnebuloso/prettier image: node:alpine
commands: commands:
- cd docker/frontend/ - cd docker/frontend/
- prettier . -c - yarn global add prettier eslint
- yarn lint
- name: build - name: build
image: tmaier/docker-compose image: tmaier/docker-compose
volumes: volumes:

View file

@ -54,6 +54,9 @@ services:
- default - default
- db - db
- redis - redis
depends_on:
- db
- redis
environment: environment:
URL: ${URL} URL: ${URL}
BACKEND_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER} 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_NAME: ${BACKEND_SMTP_NAME}
BACKEND_SMTP_USERNAME: ${BACKEND_SMTP_USERNAME} BACKEND_SMTP_USERNAME: ${BACKEND_SMTP_USERNAME}
BACKEND_SMTP_PASSWORD: ${BACKEND_SMTP_PASSWORD} BACKEND_SMTP_PASSWORD: ${BACKEND_SMTP_PASSWORD}
depends_on:
- db
- redis
frontend: frontend:
build: build:
@ -80,6 +80,8 @@ services:
- default - default
depends_on: depends_on:
- backend - backend
environment:
NODE_ENV: production
networks: networks:
db: db:

View file

@ -1,10 +1,10 @@
FROM crystallang/crystal:latest-alpine as deps FROM crystallang/crystal:1.3-alpine as deps
WORKDIR /app WORKDIR /app
RUN apk add curl --no-cache RUN apk add curl --no-cache
COPY ./shard.yml ./shard.lock ./ COPY ./shard.yml ./shard.lock ./
RUN shards install --production RUN shards install --production
FROM crystallang/crystal:latest-alpine as builder FROM crystallang/crystal:1.3-alpine as builder
ARG BUILD_ENV ARG BUILD_ENV
WORKDIR /app WORKDIR /app
COPY --from=deps /app/shard.yml /app/shard.lock ./ COPY --from=deps /app/shard.yml /app/shard.lock ./

View file

@ -22,7 +22,7 @@ module Backend
JWT.encode({"data" => data.to_h, "exp" => expiration}, SAFE_ENV["BACKEND_JWT_SECRET"], JWT::Algorithm::HS256) JWT.encode({"data" => data.to_h, "exp" => expiration}, SAFE_ENV["BACKEND_JWT_SECRET"], JWT::Algorithm::HS256)
end 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) create_jwt({user_id: user_id}, expiration)
end end

View file

@ -1,12 +1,16 @@
require "CrystalEmail"
module Backend module Backend
module API module API
module Schema module Schema
@[GraphQL::Object] @[GraphQL::Object]
class Mutation < GraphQL::BaseMutation class Mutation < GraphQL::BaseMutation
@[GraphQL::Field] @[GraphQL::Field]
def login(input : LoginInput) : LoginPayload def login(email : String, password : String) : LoginPayload
user = Db::User.find_by(email: input.email) raise "Auth failed" if email.empty? || password.empty? || !CrystalEmail::Rfc5322::Public.validates?(email)
raise "Auth failed" unless user && Auth.verify_password?(input.password, user.password)
user = Db::User.find_by(email: email)
raise "Auth failed" unless user && Auth.verify_password?(password, user.password)
LoginPayload.new( LoginPayload.new(
user: User.new(user), user: User.new(user),

View file

@ -102,18 +102,18 @@ module Backend
end end
end end
@[GraphQL::InputObject] # @[GraphQL::InputObject]
class LoginInput < GraphQL::BaseInputObject # class LoginInput < GraphQL::BaseInputObject
getter email # getter email
getter password # getter password
@[GraphQL::Field] # @[GraphQL::Field]
def initialize( # def initialize(
@email : String, # @email : String,
@password : String # @password : String
) # )
end # end
end # end
@[GraphQL::Object] @[GraphQL::Object]
class LoginPayload < GraphQL::BaseObject class LoginPayload < GraphQL::BaseObject

View file

@ -2,7 +2,10 @@ module Backend
extend self extend self
def run : Nil 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) channel = Channel(Nil).new(SERVICES.size)

View file

@ -1 +0,0 @@
v16.13.2

View file

@ -1,5 +1,5 @@
# Install dependencies only when needed # 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. # 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 RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@ -7,27 +7,24 @@ COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM node:14-alpine AS builder FROM node:16-alpine AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
RUN yarn build RUN yarn build
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM node:14-alpine AS runner FROM node:16-alpine AS runner
WORKDIR /app WORKDIR /app
ARG BUILD_ENV
ENV NODE_ENV ${BUILD_ENV}
RUN addgroup -g 1001 -S nodejs RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001 RUN adduser -S nextjs -u 1001
# You only need to copy next.config.js if you are NOT using the default configuration # 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/next.config.js ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next 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 COPY --from=builder /app/package.json ./package.json
USER nextjs USER nextjs
@ -39,4 +36,7 @@ EXPOSE 3000
# Uncomment the following line in case you want to disable telemetry. # Uncomment the following line in case you want to disable telemetry.
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
ARG BUILD_ENV
ENV NODE_ENV ${BUILD_ENV}
CMD ["yarn", "start"] CMD ["yarn", "start"]

View file

@ -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 (
<nav>
<ul>
<li>
{isLoggedIn ? (
<button onClick={handleLogout as any}>Logout</button>
) : (
<Link href="/login" passHref>
<button>Login</button>
</Link>
)}
</li>
</ul>
</nav>
);
}
export default Navbar;

View file

@ -0,0 +1,16 @@
import Navbar from "../components/navbar";
interface MainLayoutProps {
children: React.ReactNode;
}
function MainLayout({ children }: MainLayoutProps): JSX.Element {
return (
<div>
<Navbar />
<main>{children}</main>
</div>
);
}
export default MainLayout;

View file

@ -1,6 +0,0 @@
import { ApolloClient, InMemoryCache } from "@apollo/client";
export const client = new ApolloClient({
uri: "/graphql",
cache: new InMemoryCache(),
});

View file

@ -0,0 +1,2 @@
export const _BASE = "mentorenwahl_";
export const TOKEN = _BASE + "token";

View file

@ -10,11 +10,13 @@
"dependencies": { "dependencies": {
"@apollo/client": "^3.5.8", "@apollo/client": "^3.5.8",
"graphql": "^16.3.0", "graphql": "^16.3.0",
"js-cookie": "^3.0.1",
"next": "12.0.9", "next": "12.0.9",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2" "react-dom": "17.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/js-cookie": "^3.0.1",
"@types/node": "17.0.13", "@types/node": "17.0.13",
"@types/react": "17.0.38", "@types/react": "17.0.38",
"eslint": "8.8.0", "eslint": "8.8.0",

View file

@ -1,9 +1,22 @@
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client";
import MainLayout from "../layouts/main";
import "../styles/globals.css"; import "../styles/globals.css";
function MyApp({ Component, pageProps }: AppProps) { const client = new ApolloClient({
return <Component {...pageProps} />; uri: "/graphql",
cache: new InMemoryCache(),
});
function MyApp({ Component, pageProps }: AppProps): JSX.Element {
return (
<ApolloProvider client={client}>
<MainLayout>
<Component {...pageProps} />
</MainLayout>
</ApolloProvider>
);
} }
export default MyApp; export default MyApp;

View file

@ -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 = () => { import * as cookieNames from "../lib/cookieNames";
return (
<div> function Home(): JSX.Element {
<h1>Willkommen zur Mentorenwahl!</h1> const token = Cookies.get(cookieNames.TOKEN);
</div> useEffect(() => {
); if (!token) {
}; Router.push("/login");
}
}, [token]);
if (!token) {
return <></>;
}
return <p>Du bist eingeloggt!</p>;
}
export default Home; export default Home;

View file

@ -1,39 +1,40 @@
import type { NextPage } from "next"; import type { FormEvent, FormEventHandler } from "react";
import type { FormEvent } from "react"; import { gql, useMutation } from "@apollo/client";
import { gql } 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 = () => { const LOGIN = gql`
async function loginUser(event: FormEvent): Promise<void> { 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<void> => {
event.preventDefault(); event.preventDefault();
const input = { const input = {
email: (event.target as HTMLFormElement).email.value, email: event.target.email.value as string,
password: (event.target as HTMLFormElement).password.value, password: event.target.password.value as string,
}; };
console.log(input); const data = (
await login({
// client variables: { email: input.email, password: input.password },
// .mutate({ })
// mutation: gql` ).data;
// mutation Login($input: LoginInput!) { if (data) {
// login(input: $input) { Cookies.set(cookieNames.TOKEN, data.login.token, { expires: 1 });
// user { Router.push("/");
// id }
// firstname };
// lastname
// email
// }
// bearer
// }
// }
// `,
// })
// .then((res) => {
// console.log(res);
// });
}
return ( return (
<div> <div>
@ -54,8 +55,9 @@ const Login: NextPage = () => {
<br /> <br />
<button type="submit">Login</button> <button type="submit">Login</button>
</form> </form>
{error && <p style={{ color: "red" }}>{error.message}</p>}
</div> </div>
); );
}; }
export default Login; export default Login;

View file

@ -162,6 +162,11 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323"
integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== 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": "@types/json5@^0.0.29":
version "0.0.29" version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" 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" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 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": "js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"