Frontend #19
41
.drone.yml
41
.drone.yml
|
@ -8,10 +8,6 @@ steps:
|
|||
commands:
|
||||
- cd scripts/
|
||||
- find -name "*.sh" | xargs shellcheck
|
||||
- name: env
|
||||
image: alpine
|
||||
commands:
|
||||
- . .example.env
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
|
@ -26,17 +22,15 @@ steps:
|
|||
- name: pgsanity
|
||||
image: boechat107/pgsanity
|
||||
commands:
|
||||
- cd docker/backend/db/
|
||||
- find -name "*.sql" | xargs pgsanity
|
||||
- name: backend
|
||||
image: docker:dind
|
||||
- pgsanity docker/backend/db/**/*.sql
|
||||
- name: build
|
||||
image: tmaier/docker-compose
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- cp .example.env .env
|
||||
- cd docker/
|
||||
- docker build --build-arg BUILD_ENV=development backend
|
||||
- docker-compose build --build-arg BUILD_ENV=development backend
|
||||
depends_on:
|
||||
- ameba
|
||||
- pgsanity
|
||||
|
@ -46,3 +40,30 @@ volumes:
|
|||
path: /var/run/docker.sock
|
||||
depends_on:
|
||||
- default
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: frontend
|
||||
steps:
|
||||
- name: prettier
|
||||
image: elnebuloso/prettier
|
||||
commands:
|
||||
- cd docker/frontend/
|
||||
- prettier --ignore-path .gitignore --check --plugin-search-dir=. .
|
||||
- name: build
|
||||
image: tmaier/docker-compose
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- cp .example.env .env
|
||||
- docker-compose build --build-arg BUILD_ENV=development frontend
|
||||
depends_on:
|
||||
- prettier
|
||||
volumes:
|
||||
- name: dockersock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
depends_on:
|
||||
- default
|
||||
|
|
|
@ -3,18 +3,16 @@ events {
|
|||
|
||||
http {
|
||||
server {
|
||||
# location / {
|
||||
# # proxy_set_header Host $host;
|
||||
# # proxy_set_header X-Real-IP $remote_addr;
|
||||
# # proxy_pass http://frontend:3000;
|
||||
# }
|
||||
location / {
|
||||
proxy_pass http://frontend:3000/;
|
||||
}
|
||||
|
||||
location /graphql {
|
||||
proxy_pass http://backend;
|
||||
proxy_pass http://backend/;
|
||||
}
|
||||
|
||||
location /adminer {
|
||||
proxy_pass http://adminer:8080;
|
||||
proxy_pass http://adminer:8080/;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ services:
|
|||
depends_on:
|
||||
- adminer
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
db:
|
||||
image: postgres:alpine
|
||||
|
@ -53,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}
|
||||
|
@ -64,9 +68,20 @@ services:
|
|||
BACKEND_SMTP_NAME: ${BACKEND_SMTP_NAME}
|
||||
BACKEND_SMTP_USERNAME: ${BACKEND_SMTP_USERNAME}
|
||||
BACKEND_SMTP_PASSWORD: ${BACKEND_SMTP_PASSWORD}
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./docker/frontend
|
||||
args:
|
||||
BUILD_ENV: production
|
||||
container_name: frontend
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
- backend
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
|
||||
networks:
|
||||
db:
|
||||
|
|
|
@ -4,3 +4,8 @@
|
|||
/.shards/
|
||||
*.dwarf
|
||||
.ameba.yml
|
||||
Dockerfile
|
||||
README.md
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.gitignore
|
||||
|
|
|
@ -1,27 +1,24 @@
|
|||
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 ./
|
||||
COPY --from=deps /app/lib ./lib
|
||||
COPY ./src ./src
|
||||
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
||||
time shards build --static --verbose -s -p -t; \
|
||||
time shards build -Ddevelopment --static --verbose -s -p -t; \
|
||||
else \
|
||||
time shards build --static --release --no-debug --verbose -s -p -t; \
|
||||
fi
|
||||
|
||||
FROM ubuntu:latest as user
|
||||
RUN useradd -u 10001 backend
|
||||
|
||||
FROM alpine as runner
|
||||
WORKDIR /app
|
||||
COPY --from=user /etc/passwd /etc/passwd
|
||||
RUN adduser -S backend -u 1001
|
||||
COPY --from=builder /app/bin ./bin
|
||||
COPY ./db ./db
|
||||
USER backend
|
||||
|
|
|
@ -80,26 +80,26 @@ shards:
|
|||
git: https://github.com/amberframework/quartz-mailer.git
|
||||
version: 0.8.0
|
||||
|
||||
radix:
|
||||
git: https://github.com/luislavena/radix.git
|
||||
version: 0.4.1
|
||||
|
||||
redis:
|
||||
git: https://github.com/stefanwille/crystal-redis.git
|
||||
version: 2.8.3
|
||||
|
||||
router:
|
||||
git: https://github.com/tbrand/router.cr.git
|
||||
version: 0.2.8
|
||||
|
||||
secrets-env:
|
||||
git: https://github.com/spider-gazelle/secrets-env.git
|
||||
version: 1.3.1
|
||||
|
||||
seg:
|
||||
git: https://github.com/soveran/seg.git
|
||||
version: 0.1.0+git.commit.7f1cee94fb7ed7a2ba15f1388cbaede72a85eef9
|
||||
|
||||
senf:
|
||||
git: https://git.dergrimm.net/dergrimm/senf.git
|
||||
version: 0.1.0
|
||||
|
||||
toro:
|
||||
git: https://github.com/soveran/toro.git
|
||||
version: 0.4.3
|
||||
|
||||
version_from_shard:
|
||||
git: https://github.com/hugopl/version_from_shard.git
|
||||
version: 1.2.5
|
||||
|
|
|
@ -29,8 +29,6 @@ dependencies:
|
|||
CrystalEmail:
|
||||
git: https://git.sceptique.eu/Sceptique/CrystalEmail.git
|
||||
branch: master
|
||||
toro:
|
||||
github: soveran/toro
|
||||
commander:
|
||||
github: mrrooijen/commander
|
||||
fancyline:
|
||||
|
@ -51,3 +49,5 @@ dependencies:
|
|||
github: jeromegn/kilt
|
||||
email:
|
||||
github: arcage/crystal-email
|
||||
router:
|
||||
github: tbrand/router.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
|
||||
|
||||
|
|
7
docker/backend/src/backend/api/log.cr
Normal file
7
docker/backend/src/backend/api/log.cr
Normal file
|
@ -0,0 +1,7 @@
|
|||
require "log"
|
||||
|
||||
module Backend
|
||||
module API
|
||||
Log = ::Log.for(self)
|
||||
end
|
||||
end
|
|
@ -5,10 +5,7 @@ module Backend
|
|||
extend self
|
||||
|
||||
def run : Nil
|
||||
Server.run(80, [HTTP::LogHandler.new, HTTP::ErrorHandler.new]) do |server|
|
||||
server.bind_tcp("0.0.0.0", 80, true)
|
||||
server.listen
|
||||
end
|
||||
WebServer.new.run
|
||||
end
|
||||
end
|
||||
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
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
require "toro"
|
||||
require "json"
|
||||
|
||||
module Backend
|
||||
module API
|
||||
class Server < Toro::Router
|
||||
private struct GraphQLData
|
||||
include JSON::Serializable
|
||||
|
||||
property query : String
|
||||
property variables : Hash(String, JSON::Any)?
|
||||
property operation_name : String?
|
||||
end
|
||||
|
||||
def routes
|
||||
on "graphql" do
|
||||
post do
|
||||
content_type "application/json"
|
||||
|
||||
data = GraphQLData.from_json(context.request.body.not_nil!.gets.not_nil!)
|
||||
|
||||
write Schema::SCHEMA.execute(
|
||||
data.query,
|
||||
data.variables,
|
||||
data.operation_name,
|
||||
Context.new(context.request)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
9
docker/backend/src/backend/api/service.cr
Normal file
9
docker/backend/src/backend/api/service.cr
Normal file
|
@ -0,0 +1,9 @@
|
|||
module Backend
|
||||
module API
|
||||
SERVICE = ->do
|
||||
Log.info { "Starting API service..." }
|
||||
run
|
||||
Log.info { "API service stopped." }
|
||||
end
|
||||
end
|
||||
end
|
52
docker/backend/src/backend/api/webserver.cr
Normal file
52
docker/backend/src/backend/api/webserver.cr
Normal file
|
@ -0,0 +1,52 @@
|
|||
require "router"
|
||||
require "http/server"
|
||||
require "json"
|
||||
|
||||
module Backend
|
||||
module API
|
||||
class WebServer
|
||||
include Router
|
||||
|
||||
def draw_routes : Nil
|
||||
post "/" do |context|
|
||||
context.response.content_type = "application/json"
|
||||
|
||||
data = GraphQLQueryData.from_json(context.request.body.not_nil!.gets.not_nil!)
|
||||
context.response.puts(
|
||||
Schema::SCHEMA.execute(
|
||||
data.query,
|
||||
data.variables,
|
||||
data.operation_name,
|
||||
Context.new(context.request)
|
||||
)
|
||||
)
|
||||
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
def run : Nil
|
||||
draw_routes
|
||||
|
||||
server = HTTP::Server.new(
|
||||
[
|
||||
HTTP::LogHandler.new,
|
||||
HTTP::ErrorHandler.new,
|
||||
HTTP::CompressHandler.new,
|
||||
route_handler,
|
||||
]
|
||||
)
|
||||
server.bind_tcp("0.0.0.0", 80, true)
|
||||
server.listen
|
||||
end
|
||||
|
||||
private struct GraphQLQueryData
|
||||
include JSON::Serializable
|
||||
|
||||
property query : String
|
||||
property variables : Hash(String, JSON::Any)?
|
||||
property operation_name : String?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
5
docker/backend/src/backend/log.cr
Normal file
5
docker/backend/src/backend/log.cr
Normal file
|
@ -0,0 +1,5 @@
|
|||
require "log"
|
||||
|
||||
module Backend
|
||||
Log = ::Log.for(self)
|
||||
end
|
|
@ -2,27 +2,24 @@ module Backend
|
|||
extend self
|
||||
|
||||
def run : Nil
|
||||
puts "Running backend..."
|
||||
puts "-" * 10
|
||||
{% 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
|
||||
channel = Channel(Nil).new(SERVICES.size)
|
||||
|
||||
spawn same_thread: true do
|
||||
puts "Starting API..."
|
||||
API.run
|
||||
|
||||
channel.send(nil)
|
||||
SERVICES.each do |service|
|
||||
spawn do
|
||||
service.call
|
||||
channel.send(nil)
|
||||
end
|
||||
end
|
||||
|
||||
spawn same_thread: true do
|
||||
puts "Starting worker..."
|
||||
Worker.run
|
||||
|
||||
channel.send(nil)
|
||||
end
|
||||
|
||||
2.times do
|
||||
SERVICES.size.times do
|
||||
channel.receive
|
||||
end
|
||||
Fiber.yield
|
||||
Log.info { "Backend services started." }
|
||||
end
|
||||
end
|
||||
|
|
6
docker/backend/src/backend/services.cr
Normal file
6
docker/backend/src/backend/services.cr
Normal file
|
@ -0,0 +1,6 @@
|
|||
module Backend
|
||||
SERVICES = [
|
||||
API::SERVICE,
|
||||
Worker::SERVICE,
|
||||
]
|
||||
end
|
7
docker/backend/src/backend/worker/log.cr
Normal file
7
docker/backend/src/backend/worker/log.cr
Normal file
|
@ -0,0 +1,7 @@
|
|||
require "log"
|
||||
|
||||
module Backend
|
||||
module Worker
|
||||
Log = ::Log.for(self)
|
||||
end
|
||||
end
|
9
docker/backend/src/backend/worker/service.cr
Normal file
9
docker/backend/src/backend/worker/service.cr
Normal file
|
@ -0,0 +1,9 @@
|
|||
module Backend
|
||||
module Worker
|
||||
SERVICE = ->do
|
||||
Log.info { "Starting worker service..." }
|
||||
run
|
||||
Log.info { "Worker service stopped." }
|
||||
end
|
||||
end
|
||||
end
|
45
docker/frontend/.dockerignore
Normal file
45
docker/frontend/.dockerignore
Normal file
|
@ -0,0 +1,45 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
Dockerfile
|
||||
README.md
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.gitignore
|
||||
.prettierrc
|
||||
.eslintrc.js
|
37
docker/frontend/.gitignore
vendored
Normal file
37
docker/frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
1
docker/frontend/.prettierrc
Normal file
1
docker/frontend/.prettierrc
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
42
docker/frontend/Dockerfile
Normal file
42
docker/frontend/Dockerfile
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Install dependencies only when needed
|
||||
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
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
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:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
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/package.json ./package.json
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/next.config.js ./next.config.js
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# 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"]
|
15
docker/frontend/components/dashboard.tsx
Normal file
15
docker/frontend/components/dashboard.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { User } from "../lib/graphql/user";
|
||||
|
||||
interface DashboardProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
function Dashboard({ user }: DashboardProps): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hallo {user.firstname}!</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
5
docker/frontend/components/loading.tsx
Normal file
5
docker/frontend/components/loading.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
function Loading(): JSX.Element {
|
||||
return <p>Laden...</p>;
|
||||
}
|
||||
|
||||
export default Loading;
|
33
docker/frontend/components/navbar.tsx
Normal file
33
docker/frontend/components/navbar.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import Cookies from "js-cookie";
|
||||
import Link from "next/link";
|
||||
import Router from "next/router";
|
||||
|
||||
import * as cookieNames from "../lib/cookieNames";
|
||||
|
||||
function Navbar(): JSX.Element {
|
||||
const isLoggedIn = !!Cookies.get(cookieNames.TOKEN);
|
||||
|
||||
function handleLogout(event: Event): void {
|
||||
event.preventDefault();
|
||||
Cookies.remove(cookieNames.TOKEN);
|
||||
Router.reload();
|
||||
}
|
||||
|
||||
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;
|
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";
|
14
docker/frontend/lib/graphql/user.ts
Normal file
14
docker/frontend/lib/graphql/user.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export enum Role {
|
||||
ADMIN = "admin",
|
||||
TEACHER = "teacher",
|
||||
STUDENT = "student",
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
role?: Role;
|
||||
}
|
5
docker/frontend/next-env.d.ts
vendored
Normal file
5
docker/frontend/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
6
docker/frontend/next.config.js
Normal file
6
docker/frontend/next.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
25
docker/frontend/package.json
Normal file
25
docker/frontend/package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"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",
|
||||
"eslint-config-next": "12.0.9",
|
||||
"typescript": "4.5.5"
|
||||
}
|
||||
}
|
27
docker/frontend/pages/_app.tsx
Normal file
27
docker/frontend/pages/_app.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import type { AppProps } from "next/app";
|
||||
import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
import MainLayout from "../layouts/main";
|
||||
import "../styles/globals.css";
|
||||
import * as cookieNames from "../lib/cookieNames";
|
||||
|
||||
const token = Cookies.get(cookieNames.TOKEN);
|
||||
|
||||
const client = new ApolloClient({
|
||||
uri: "/graphql",
|
||||
cache: new InMemoryCache(),
|
||||
headers: token ? { authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps): JSX.Element {
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<MainLayout>
|
||||
<Component {...pageProps} />
|
||||
</MainLayout>
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
58
docker/frontend/pages/index.tsx
Normal file
58
docker/frontend/pages/index.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import Cookies from "js-cookie";
|
||||
import Router from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { gql, useLazyQuery } from "@apollo/client";
|
||||
|
||||
import * as cookieNames from "../lib/cookieNames";
|
||||
import Dashboard from "../components/dashboard";
|
||||
import Loading from "../components/loading";
|
||||
|
||||
const ME_QUERY = gql`
|
||||
query Me {
|
||||
me {
|
||||
id
|
||||
firstname
|
||||
email
|
||||
role
|
||||
teacher {
|
||||
id
|
||||
}
|
||||
student {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const token = Cookies.get(cookieNames.TOKEN);
|
||||
|
||||
function Home(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
Router.push("/login");
|
||||
}
|
||||
});
|
||||
|
||||
const [loadMe, { called, loading, data }] = useLazyQuery(ME_QUERY, {
|
||||
variables: { token },
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (called && loading) {
|
||||
return <Loading />;
|
||||
} else if (!called) {
|
||||
loadMe();
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dashboard user={data.me} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
64
docker/frontend/pages/login.tsx
Normal file
64
docker/frontend/pages/login.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import type { FormEvent, FormEventHandler } from "react";
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
import Cookies from "js-cookie";
|
||||
import Router from "next/router";
|
||||
|
||||
import * as cookieNames from "../lib/cookieNames";
|
||||
|
||||
const LOGIN_MUTATION = gql`
|
||||
mutation Login($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function Login(): JSX.Element {
|
||||
const [login, { error }] = useMutation(LOGIN_MUTATION);
|
||||
|
||||
const loginUser: FormEventHandler = async (
|
||||
event: FormEvent & { target: HTMLFormElement }
|
||||
): Promise<void> => {
|
||||
event.preventDefault();
|
||||
|
||||
const input = {
|
||||
email: event.target.email.value as string,
|
||||
password: event.target.password.value as string,
|
||||
};
|
||||
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>
|
||||
<h1>Login:</h1>
|
||||
<form onSubmit={loginUser}>
|
||||
<label htmlFor="email">Email:</label>
|
||||
<br />
|
||||
<input type="email" id="email" autoComplete="email" required />
|
||||
<br />
|
||||
<label htmlFor="password">Password:</label>
|
||||
<br />
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{error && <p style={{ color: "red" }}>{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
0
docker/frontend/public/.keep
Normal file
0
docker/frontend/public/.keep
Normal file
0
docker/frontend/styles/globals.css
Normal file
0
docker/frontend/styles/globals.css
Normal file
20
docker/frontend/tsconfig.json
Normal file
20
docker/frontend/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
1777
docker/frontend/yarn.lock
Normal file
1777
docker/frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue