Added login page
This commit is contained in:
parent
1b2fcf7ba8
commit
0da7f11239
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 ./
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
v16.13.2
|
|
@ -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"]
|
29
docker/frontend/components/navbar.tsx
Normal file
29
docker/frontend/components/navbar.tsx
Normal 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;
|
16
docker/frontend/layouts/main.tsx
Normal file
16
docker/frontend/layouts/main.tsx
Normal 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;
|
|
@ -1,6 +0,0 @@
|
|||
import { ApolloClient, InMemoryCache } from "@apollo/client";
|
||||
|
||||
export const client = new ApolloClient({
|
||||
uri: "/graphql",
|
||||
cache: new InMemoryCache(),
|
||||
});
|
2
docker/frontend/lib/cookieNames.ts
Normal file
2
docker/frontend/lib/cookieNames.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const _BASE = "mentorenwahl_";
|
||||
export const TOKEN = _BASE + "token";
|
|
@ -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",
|
||||
|
|
|
@ -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 <Component {...pageProps} />;
|
||||
const client = new ApolloClient({
|
||||
uri: "/graphql",
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps): JSX.Element {
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<MainLayout>
|
||||
<Component {...pageProps} />
|
||||
</MainLayout>
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
<h1>Willkommen zur Mentorenwahl!</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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 <p>Du bist eingeloggt!</p>;
|
||||
}
|
||||
|
||||
export default Home;
|
||||
|
|
|
@ -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<void> {
|
||||
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<void> => {
|
||||
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 (
|
||||
<div>
|
||||
|
@ -54,8 +55,9 @@ const Login: NextPage = () => {
|
|||
<br />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{error && <p style={{ color: "red" }}>{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Login;
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue