initial commit

This commit is contained in:
john doe 2024-04-10 11:23:23 +02:00
commit 5ae12a2983
59 changed files with 7133 additions and 0 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
POSTGRES_USER=root
POSTGRES_PASSWORD=root
POSTGRES_DB=info

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
frontend/node_modules
go.sum
social-network
users.db
fiber
frontend/.next/
/out/

7
Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM nginx:latest
WORKDIR /etc/nginx/
COPY nginx.conf .
EXPOSE 80

50
README.md Normal file
View File

@ -0,0 +1,50 @@
# CopyBook
A messageboard type social website
## Backend
Fully coded in golang utilizing the following technologies:
- https://github.com/gofiber/fiber
- https://gorm.io/
The backend consists of multiple modules:
- db.go: database modules setup with gorm
- auth.go: authorization module with jwt tokens setup
- server.go: main buisness logic functionality
## Frontend
Typescript + nextjs + tailwindcss
the frontend uses nextjs for server side rendering.
there are multiple pages each responsible for its own functionality:
- index.tsx: mounted on `/` is the homepage for the messageboard.
- login.tsx: mounted on `/login` the login page
- logout.tsx: mounted on `/logout` for logging out and removing the jwt token.
- profile.tsx: mounted on `/profile` is for the user profile info.
- signup.tsx: mounted on `/signup` is for registering new users.
## Devops
Fully dockerized and ready for deployment on any environment
backend setup consists of 2 containers one that builds the golang binary for and another that runs the server
the database for the backend is running a postgresql server with persistent storage enabled
frontend setup has 3 containers:
`deps`: pulls the dependecies required for compiling the typescript files.
`builder`: compiles the typescript files.
`runner`: runs the nextjs server.
all of the previous services are running on `internalnet` virtual network and then afterwards its served with a nginx reverse proxy for traffic management and redundancy purposes
## TODO
- better db management
- more documentation
- user personalization
- more ui changes

13
backend/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM golang AS build
# RUN apk --no-cache add gcc g++ make git
WORKDIR /go/src/app
COPY . .
RUN go mod tidy
# RUN GOOS=linux go build -ldflags="-s -w" -o ./bin/web-app
RUN GOOS=linux go build -o ./bin/web-app
FROM ubuntu
WORKDIR /usr/bin
COPY --from=build /go/src/app/bin /go/bin
EXPOSE 3000
ENTRYPOINT /go/bin/web-app

3
backend/README.md Normal file
View File

@ -0,0 +1,3 @@
# Backend API for the social network
rest api utlizing golang and the gofiber framework

35
backend/auth.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"time"
"github.com/golang-jwt/jwt/v4"
)
var jwtKey = []byte("ShXYLRYfFOw+upPD")
type Claim struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
func Sign(username string) (string, error) {
expirationTime := jwt.NewNumericDate(time.Now().Add(5 * time.Hour))
claims := &Claim{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: expirationTime,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, err
}

75
backend/db.go Normal file
View File

@ -0,0 +1,75 @@
package main
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"golang.org/x/crypto/bcrypt"
)
type User struct {
ID uint `gorm:"primaryKey"`
Username string
Password string
Email string
Active bool
}
type APIUser struct {
ID uint
Username string
Email string
}
type Comment struct {
UID uint `gorm:"primaryKey"`
PostId uint
Commenter string
Text string
}
type APIComment struct {
PostId uint `json:"post_id"`
Text string `json:"content"`
}
type Post struct {
ID uint `gorm:"primaryKey"`
Content string
Username string
Comments string
}
func initDB() *gorm.DB {
dburl := "host=db user=root password=root dbname=info port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dburl), &gorm.Config{})
if err != nil {
panic("failed to open db")
}
db.AutoMigrate(&User{})
db.AutoMigrate(&Post{})
return db
}
func validateUser(u User) bool {
if len(u.Username) < 3 {
return false
}
if len(u.Password) < 3 {
return false
}
return true
}
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

26
backend/go.mod Normal file
View File

@ -0,0 +1,26 @@
module social-network
go 1.13
require (
github.com/charmbracelet/bubbles v0.11.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gofiber/cli v0.0.9 // indirect
github.com/gofiber/fiber/v2 v2.34.0
github.com/gofiber/jwt/v3 v3.2.12 // indirect
github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.15.6 // indirect
github.com/mattn/go-sqlite3 v1.14.13 // indirect
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.12.0 // indirect
github.com/spf13/cobra v1.5.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/sync v0.7.0 // indirect
gorm.io/driver/postgres v1.5.7 // indirect
gorm.io/driver/sqlite v1.3.4 // indirect
gorm.io/gorm v1.25.9 // indirect
)

232
backend/server.go Normal file
View File

@ -0,0 +1,232 @@
package main
import (
"bytes"
"fmt"
"log"
"encoding/base64"
"encoding/gob"
"encoding/json"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
jwtware "github.com/gofiber/jwt/v3"
"github.com/golang-jwt/jwt/v4"
)
type Creds struct {
Username string `json:"username" xml:"username" form:"username"`
Pass string `json:"pass" xml:"pass" form:"pass"`
}
type Response struct {
Status string `json:"status"`
Text string `json:"text"`
}
type JsonComment struct {
Array []Comment
}
// go binary encoder
func ToGOB64(m JsonComment) string {
b := bytes.Buffer{}
e := gob.NewEncoder(&b)
err := e.Encode(m)
if err != nil {
fmt.Println(`failed gob Encode`, err)
}
return base64.StdEncoding.EncodeToString(b.Bytes())
}
// go binary decoder
func FromGOB64(str string) JsonComment {
m := JsonComment{}
by, err := base64.StdEncoding.DecodeString(str)
if err != nil {
fmt.Println(`failed base64 Decode`, err)
}
b := bytes.Buffer{}
b.Write(by)
d := gob.NewDecoder(&b)
err = d.Decode(&m)
if err != nil {
fmt.Println(`failed gob Decode`, err)
}
return m
}
func malformedToken(c *fiber.Ctx, w error) error {
res := Response{Status: "bad", Text: "Malformed token"}
return c.JSON(res)
}
func main() {
app := fiber.New()
db := initDB()
bad := Response{Status: "bad", Text: "Unauthorized"}
errResponse := Response{Status: "bad", Text: "Woops something went wrong ¯\\_(ツ)_/¯"}
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Origin, Content-Type, Accept, Authorization",
}))
app.Post("/login", func(c *fiber.Ctx) error {
p := new(Creds)
if err := c.BodyParser(p); err != nil {
return err
}
var res User
if err := db.First(&res, "Username = ?", p.Username).Error; err != nil {
return c.JSON(bad)
}
if !CheckPasswordHash(p.Pass, res.Password) {
return c.JSON(bad)
}
token, _ := Sign(p.Username)
response := Response{Status: "ok", Text: token}
return c.JSON(response)
})
app.Post("/signup", func(c *fiber.Ctx) error {
details := new(User)
if err := c.BodyParser(details); err != nil {
return err
}
if !validateUser(*details) {
return c.JSON(errResponse)
}
details.Active = true
var query User
if err := db.First(&query, "Username = ?", details.Username).Error; err != nil && query.Username != "" {
return c.JSON(errResponse)
}
hashedPass, err := HashPassword(details.Password)
if err != nil {
return c.JSON(errResponse)
}
details.Password = hashedPass
db.Create(details)
newUser := Response{Status: "ok", Text: "user created successfully"}
return c.JSON(newUser)
})
app.Use(jwtware.New(jwtware.Config{
SigningKey: []byte("ShXYLRYfFOw+upPD"),
ErrorHandler: malformedToken,
}))
app.Get("/posts", func(c *fiber.Ctx) error {
var query []Post
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8)
db.Find(&query)
res, err := json.Marshal(query)
if err != nil {
return c.JSON(errResponse)
}
return c.SendString(string(res))
})
app.Get("/posts/:uid", func(c *fiber.Ctx) error {
var post Post
db.Select("content").Find(&post, "ID = ?", c.Params("uid"))
res := Response{Status: "ok", Text: post.Content}
return c.JSON(res)
})
app.Get("/profile/:uid", func(c *fiber.Ctx) error {
var user APIUser
if err := db.Model(&User{}).Find(&user, "Username = ?", c.Params("uid")).Error; err != nil {
return c.JSON(errResponse)
}
return c.JSON(user)
})
app.Get("/profile", func(c *fiber.Ctx) error {
var user APIUser
token := c.Locals("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
name := claims["username"].(string)
if err := db.Model(&User{}).Find(&user, "Username = ?", name).Error; err != nil {
return c.JSON(errResponse)
}
return c.JSON(user)
})
app.Post("/create", func(c *fiber.Ctx) error {
info := new(Post)
if err := c.BodyParser(info); err != nil {
return c.JSON(errResponse)
}
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["username"].(string)
//figure out how to store user id's instead of usernames
/*
var query User
if err := db.First(&query, "Username = ?", name).Error; err != nil && query.Username != "" {
return c.JSON(errResponse)
}
info.Uuid = query.ID
*/
info.Username = name
db.Create(info)
newPost := Response{Status: "ok", Text: "post created successfully"}
return c.JSON(newPost)
})
app.Post("/comment", func(c *fiber.Ctx) error {
var comment Comment
apiComment := new(APIComment)
if err := c.BodyParser(apiComment); err != nil {
return c.JSON(errResponse)
}
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["username"].(string)
comment.PostId = apiComment.PostId
comment.Text = apiComment.Text
comment.Commenter = name
var post Post
db.Select("comments").Find(&post, "ID = ?", comment.PostId)
dbComments := FromGOB64(post.Comments)
comments := JsonComment{Array: append([]Comment{comment}, dbComments.Array...)}
store := ToGOB64(comments)
db.Model(&Post{}).Where("ID = ?", comment.PostId).Update("comments", store)
newComment := Response{Status: "ok", Text: "Comment created successfully"}
return c.JSON(newComment)
})
app.Get("/comments/:uid", func(c *fiber.Ctx) error {
var post Post
db.Select("comments").Find(&post, "ID = ?", c.Params("uid"))
comments := FromGOB64(post.Comments)
return c.JSON(comments)
})
log.Fatal(app.Listen(":3000"))
}

58
docker-compose.yml Normal file
View File

@ -0,0 +1,58 @@
services:
nginx:
build: .
container_name: nginx
restart: "always"
privileged: true
links:
- "go-api:api"
- "frontend:frontend"
ports:
- 80:80
networks:
- internalnet
go-api:
build: ./backend
restart: "always"
container_name: api
environment:
- DB_USER=${POSTGRES_USER}
- DB_PASSWORD=${POSTGRES_PASSWORD}
- DB_NAME=${POSTGRES_DB}
- DB_HOST=db
- DB_PORT=5432
ports:
- 3000:3000
depends_on:
- db
networks:
- internalnet
frontend:
build: ./frontend
restart: "always"
container_name: frontend
ports:
- 3001:3000
networks:
- internalnet
db:
image: postgres:latest
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- internalnet
networks:
internalnet:
driver: bridge
volumes:
postgres_data:

7
frontend/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

1
frontend/.env Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_URL=127.0.0.1:3000

60
frontend/Dockerfile Normal file
View File

@ -0,0 +1,60 @@
# 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
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 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 during the build.
ENV NEXT_TELEMETRY_DISABLED 1
#RUN yarn build
# If using npm comment out above and use below instead
RUN npm run build
# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 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/public ./public
COPY --from=builder /app/package.json ./package.json
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]

3
frontend/README.md Normal file
View File

@ -0,0 +1,3 @@
# Frontend for the social network
the frontend uses typscript + nextjs

View File

@ -0,0 +1,22 @@
import Link from 'next/link'
export default function CommentBox({
content,
user,
id
}: {
content: string
user: string
id: number
}) {
return (
<div className="card w-96 card-side text-white hover:bg-base-300">
<div className="card-body">
<Link href={`/profile/${user}`}>
<h2 className="hover:text-primary card-title">{user}</h2>
</Link>
<p className="text-xl">{content}</p>
</div>
</div>
)
}

View File

@ -0,0 +1,27 @@
import Link from 'next/link'
export default function PostBox({
content,
user,
id
}: {
content: string
user: string
id: number
}) {
return (
<Link href={`/post/${id}`}>
<div className="card w-96 card-side text-white hover:bg-base-300">
<div className="card-body">
<Link href={`/profile/${user}`}>
<h2 className="hover:text-primary card-title">{user}</h2>
</Link>
<p className="text-xl">{content}</p>
<div className="card-actions justify-end">
{/*<button className="btn btn-primary">Like</button>*/ }
</div>
</div>
</div>
</Link>
)
}

View File

@ -0,0 +1,32 @@
import Link from 'next/link'
export default function Dashboard() {
return (
<div>
<ul className="menu bg-base-300 rounded-box fixed min-h-full py-6 space-y-9">
<li>
<Link href="/">
<a>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
</a>
</Link>
</li>
<li>
<Link href="/profile">
<a>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
</a>
</Link>
</li>
<li>
<a>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>
</a>
</li>
</ul>
<header className='grid place-items-center pt-5 pb-20'>
<h1>Copybook</h1>
</header>
</div>
);
}

5
frontend/next-env.d.ts vendored Normal file
View 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.

15
frontend/next.config.js Normal file
View File

@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
nx: {
svgr: false,
},
// fix for running the docker container
output: 'standalone',
experimental: {
outputStandalone: true,
}
}
module.exports = nextConfig

5914
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
frontend/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "social-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"daisyui": "^2.19.0",
"next": "12.1.6",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-router-dom": "^6.3.0"
},
"devDependencies": {
"@types/node": "^17.0.40",
"@types/react": "^18.0.11",
"autoprefixer": "^10.4.7",
"eslint": "8.17.0",
"eslint-config-next": "12.1.6",
"postcss": "^8.4.14",
"tailwindcss": "^3.0.24"
}
}

6
frontend/pages/_app.tsx Normal file
View File

@ -0,0 +1,6 @@
import '../styles/globals.css'
import { AppProps } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}

60
frontend/pages/index.tsx Normal file
View File

@ -0,0 +1,60 @@
import PostBox from '../components/post';
import Dashboard from '../components/ui';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
let url = process.env.NEXT_PUBLIC_URL;
export default function Home(props) {
const router = useRouter();
let [posts, setPosts] = useState([]);
useEffect(() => {
let token = localStorage.getItem("Token");
let ps = async () => {
setPosts(await fetch(`http://${url}/posts`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
}
ps().catch(console.error);
setInterval(refreshPosts, 20000)
}, []);
if (posts["status"] == "bad") {
localStorage.removeItem("Token");
router.push("/login");
}
let content = [];
if (posts.length != 0 && posts["status"] == undefined) {
content = Object.assign([], posts.map(post => {
return [post.Content, post.ID, post.Username]
}));
content.reverse()
}
let refreshPosts = async() => {
let token = localStorage.getItem("Token");
let ps = async () => {
setPosts(await fetch(`http://${url}/posts`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
}
ps().catch(console.error);
}
let newPost = async () => {
let token = localStorage.getItem("Token");
let y = document.getElementById("postContent") as HTMLInputElement;
await fetch(`http://${url}/create`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, mode: "cors", body: JSON.stringify({ Title: "UI", Content: y.value }) });
y.value = "";
await refreshPosts();
}
return (
<div>
<Dashboard />
<main className='flex flex-col place-items-center space-y-3'>
<textarea id="postContent" className="textarea textarea-secondary text-xl" placeholder="What's on your mind ?"></textarea>
<button className="btn btn-outline btn-secondary w-20" onClick={newPost}>Post</button>
{content.length != 0 ? content.map(post => {
return <PostBox id={post[1]} content={post[0]} key={post[1]} user={post[2]}/>
}) : ""}
</main>
</div>
)
}

50
frontend/pages/login.tsx Normal file
View File

@ -0,0 +1,50 @@
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import Link from 'next/link'
let url = process.env.NEXT_PUBLIC_URL
export default function Login() {
const router = useRouter();
useEffect(() => {
let stuff = localStorage.getItem("Token");
if (stuff != null) {
router.push("/")
}
}, []);
let grab_token = async (event) => {
event.preventDefault();
let username = (document.getElementById("Username") as HTMLInputElement).value;
let password = (document.getElementById("Password") as HTMLInputElement).value;
let token = await fetch(`http://${url}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, mode: "cors", body: JSON.stringify({ "username": username, "pass": password }) }).then(r => r.json());
if (token["status"] == "ok") {
await localStorage.setItem("Token", token["text"]);
router.push('/')
}
else {
alert("bad username/password");
}
}
return (
<div>
<header className='grid place-items-center pt-5 pb-20 text-white text-xl'>
<h1>Copybook</h1>
</header>
<form className="grid place-items-center space-y-5" action='#' onSubmit={grab_token}>
<h1 className="text-xl pb-5">Login</h1>
<input id="Username" type="text" placeholder="Username" className="input input-bordered input-accent w-full max-w-xs text-xl" />
<input id="Password" type="password" placeholder="Password" className="input input-bordered input-primary w-full max-w-xs text-xl" />
<button className="btn btn-outline btn-accent w-40">Login</button>
<Link href="/signup">
<a className="link link-accent">Signup ?</a>
</Link>
</form>
</div>
)
}

15
frontend/pages/logout.tsx Normal file
View File

@ -0,0 +1,15 @@
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
export default function Logout() {
const router = useRouter();
useEffect(() => {
localStorage.removeItem("Token");
router.push("/login");
})
return (
<div>
<h1>Logging you out ...</h1>
</div>
);
}

View File

@ -0,0 +1,56 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Dashboard from '../../components/ui';
import CommentBox from '../../components/comment';
export default function Profile() {
const router = useRouter();
let pid = router.query.pid as string;
let [post, setPost] = useState({text: ""});
let [comments, setComments] = useState([]);
useEffect(() => {
if (!router.isReady) return;
let token = localStorage.getItem("Token");
let ps = async () => {
pid = router.query.pid as string;
setPost(await fetch(`http://127.0.0.1:3000/posts/${pid}`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
setComments(await fetch(`http://127.0.0.1:3000/comments/${pid}`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
}
ps().catch(console.error);
}, [router.isReady]);
let refreshComments = () => {
let token = localStorage.getItem("Token");
let ps = async () => {
setComments(await fetch(`http://127.0.0.1:3000/comments/${pid}`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
}
ps().catch(console.error);
}
let newComment = async () => {
let token = localStorage.getItem("Token");
let y = document.getElementById("commentContent") as HTMLInputElement;
await fetch("http://127.0.0.1:3000/comment", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, mode: "cors", body: JSON.stringify({ "post_id": parseInt(pid), "content": y.value }) });
y.value = "";
refreshComments();
}
return (
<div>
<Dashboard />
<main className='flex flex-col place-items-center space-y-3'>
<h1 className='text-2xl text-white'>{post.text}</h1>
<textarea id="commentContent" className="textarea textarea-secondary text-xl" placeholder="New comment..."></textarea>
<button className="btn btn-outline btn-secondary w-20" onClick={newComment}>Comment</button>
<div className="divider"></div>
<div className='pt-10'>
{comments["Array"] != undefined ? comments["Array"].map(comment => {
return (
<CommentBox content={comment["Text"]} user={comment["Commenter"]} id={comment["PostId"]} />
);
}) : ""}
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,27 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Dashboard from '../components/ui';
let url = process.env.NEXT_PUBLIC_URL
export default function Profile() {
const router = useRouter();
let [info, setInfo] = useState({});
useEffect(() => {
let token = localStorage.getItem("Token");
let ps = async () => {
setInfo(await fetch(`http://${url}/profile`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
}
ps().catch(console.error);
}, [router.isReady]);
return (
<div>
<Dashboard />
<main className='flex flex-col place-items-center space-y-3'>
<h1 className='text-2xl text-white'>{info["Username"]}</h1>
<p>About me :</p>
<p className='text-xl'>woops... </p>
</main>
</div>
)
}

View File

@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Dashboard from '../../components/ui';
export default function Profile() {
const router = useRouter();
let [info, setInfo] = useState({});
useEffect(() => {
if (!router.isReady) return;
let token = localStorage.getItem("Token");
let ps = async () => {
const { pid } = router.query
setInfo(await fetch(`http://127.0.0.1:3000/profile/${pid}`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
}
ps().catch(console.error);
}, [router.isReady]);
return (
<div>
<Dashboard />
<main className='flex flex-col place-items-center space-y-3'>
<h1 className='text-2xl text-white'>{info["Username"]}</h1>
<p>About me :</p>
<p className='text-xl'>woops... </p>
</main>
</div>
)
}

38
frontend/pages/signup.tsx Normal file
View File

@ -0,0 +1,38 @@
import { useRouter } from 'next/router';
let url = process.env.NEXT_PUBLIC_URL
export default function Signup() {
const router = useRouter();
let signup = async (event) => {
event.preventDefault();
let username = (document.getElementById("Username") as HTMLInputElement).value;
let email = (document.getElementById("Email") as HTMLInputElement).value;
let password = (document.getElementById("Password") as HTMLInputElement).value;
let token = await fetch(`http://${url}/signup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, mode: "cors", body: JSON.stringify({ "Username": username, "Email": email, "Password": password }) }).then(r => r.json());
if (token["status"] == "ok") {
router.push('/login')
}
else {
alert("Username already exists");
}
}
return (
<div>
<header className='grid place-items-center pt-5 pb-20 text-white text-xl'>
<h1>Copybook</h1>
</header>
<form className="grid place-items-center space-y-5" action="#" onSubmit={signup}>
<h1 className="text-xl pb-5">Signup</h1>
<input id="Email" type="text" placeholder="Email" className="input input-bordered input-accent w-full max-w-xs text-xl" />
<input id="Username" type="text" placeholder="Username" className="input input-bordered input-accent w-full max-w-xs text-xl" />
<input id="Password" type="password" placeholder="Password" className="input input-bordered input-primary w-full max-w-xs text-xl" />
<button className="btn btn-outline btn-accent w-40">Signup</button>
</form>
</div>
)
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
frontend/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,116 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View File

@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

View File

@ -0,0 +1,27 @@
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
daisyui: {
themes: [
{
mytheme: {
"primary": "#e26394",
"secondary": "#76e8f7",
"accent": "#ed44a6",
"neutral": "#16191D",
"base-100": "#2F354C",
"info": "#95AAE4",
"success": "#12735F",
"warning": "#F3DA58",
"error": "#EB333F",
},
},
],
},
plugins: [require("daisyui")],
}

30
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"resolveJsonModule": true,
"moduleResolution": "node",
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}

22
nginx.conf Normal file
View File

@ -0,0 +1,22 @@
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name social.org;
location / {
proxy_pass http://frontend:3000;
}
}
server {
listen 80;
server_name api.social.org;
location / {
proxy_pass http://api:3000;
}
}
}

3
remove-app.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
sudo docker rmi social-network-nginx social-network-frontend

BIN
screenshots/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
screenshots/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
screenshots/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
screenshots/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

3
start-app.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
sudo docker compose up -d

3
stop-app.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
sudo docker compose down