Added login page
This commit is contained in:
parent
1b2fcf7ba8
commit
0da7f11239
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 ./
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
v16.13.2
|
|
|
@ -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"]
|
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": {
|
"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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue