Compare commits

...

40 Commits

Author SHA1 Message Date
573568ee6d Update 'README.md' 2023-07-27 10:44:08 +00:00
9308ebba54 Updated README.md 2023-07-27 12:39:03 +02:00
793d464eda Fix: full image urls in docker-compose.yml 2023-07-27 12:38:51 +02:00
bfdc915631 Fixed CORS and trait issue Kratos 2022-10-21 01:27:02 +02:00
4d92bc3e86 Add max amount of messages in scroll & auto-scroll 2022-10-20 19:47:06 +02:00
b1a4470c68 Updated ChatMessage bigint to number 2022-10-20 19:46:43 +02:00
f15eb98192 Updated message padding and text size 2022-10-20 19:46:10 +02:00
f606706ba3 Disable chat input when user is not logged in 2022-10-20 18:48:13 +02:00
bff6cdd434 Added chat microservice, removed air 2022-10-20 18:45:58 +02:00
bdd1a655d4 Initial commit chat microservice 2022-10-19 17:51:51 +02:00
f30f9090b3 Added faker and removed some placeholder data 2022-10-19 15:52:44 +02:00
3fbe04896e Added Sentry to frontend 2022-10-19 15:24:56 +02:00
a627addad5 Added login flow 2022-10-16 21:02:41 +02:00
4afe2f906d Made onClose for login modal optionall 2022-10-16 19:29:11 +02:00
b87df50328 Save modal prop changes 2022-10-16 19:28:57 +02:00
582455933e Created seperate logo component 2022-10-16 19:18:47 +02:00
a408abdb97 Created seperate hook for signup flow 2022-10-16 19:18:34 +02:00
f4eadc34c7 Updated ory-prettier-styles 2022-10-16 19:06:11 +02:00
38f3caa524 Grouped components into folders 2022-10-16 16:26:24 +02:00
ea603804a2 WIP bunch of stuff 2022-10-16 15:58:23 +02:00
b4ff0c8f77 Migrated from React to NextJS 2022-10-14 13:58:57 +02:00
b2a16e5181 Added ory kratos to docker compose file 2022-10-14 12:15:07 +02:00
ebf1dd5adc Moved prettier config 2022-10-14 12:14:44 +02:00
bd171a10bf Moved types into seperate folder 2022-10-04 18:03:07 +02:00
76a20d7685 Moved routes from App.tsx and stores from index.tsx 2022-10-04 18:01:10 +02:00
d1c0ae0a15 Moved numFormatter to utils 2022-10-04 17:51:21 +02:00
5a7e37077a Moved from React proxy to config 2022-10-04 17:51:07 +02:00
92ee1cfd64 Added @ import path 2022-10-04 17:36:54 +02:00
837516f0e6 Updated prettier config 2022-10-04 17:31:54 +02:00
add9cdabcb Updated deprecated tailwind-scrollbar config 2022-10-04 17:29:35 +02:00
c768d5ccd5 Added eslint, removed react-query 2022-10-04 17:27:40 +02:00
3025245a79 Added login and signup page to frontend router 2022-09-30 17:40:17 +02:00
bab838ac35 Added and configured cypress 2022-09-30 17:28:13 +02:00
c3d610cbf7 login and register also stores a http-only cookie 2022-09-30 16:45:42 +02:00
21f3a3d000 Added CSRF middleware 2022-09-30 16:35:04 +02:00
11f84b9755 More frontend and I lost track 2022-09-29 23:58:41 +02:00
7255e22315 Added logo and frontend part of login and signup modals 2022-09-29 21:18:26 +02:00
2bbe65ce3e Default state of client view is logged out 2022-09-29 15:21:02 +02:00
2433d0c225 Added first draft of channel page 2022-09-29 15:05:46 +02:00
db1bee79ac Updated User model fields 2022-09-28 16:14:30 +02:00
99 changed files with 6141 additions and 658 deletions

View File

@ -1,47 +0,0 @@
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./application ./main.go"
# Binary file yields from `cmd`.
bin = "application"
# Customize binary.
full_bin = "application"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html", "mustache", "hbs", "pug"]
# Ignore these filename extensions or directories.
exclude_dir = ["tmp", "vendor", "node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms
[log]
# Show log time
time = false
[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true

View File

@ -1,47 +0,0 @@
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./application.exe ./main.go"
# Binary file yields from `cmd`.
bin = "application.exe"
# Customize binary.
full_bin = "application.exe"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html", "mustache", "hbs", "pug"]
# Ignore these filename extensions or directories.
exclude_dir = ["tmp", "vendor", "node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms
[log]
# Show log time
time = false
[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true

View File

@ -0,0 +1,39 @@
{
"$id": "file:///etc/config/kratos/default.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "default",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
},
"username": {
"title": "Username",
"type": "string",
"minLength": 3
}
},
"required": ["email", "username"],
"additionalProperties": false
}
}
}

94
.docker/kratos/kratos.yml Normal file
View File

@ -0,0 +1,94 @@
version: v0.10.1
dsn: memory
dev: true
serve:
public:
base_url: http://127.0.0.1:4433/
cors:
enabled: true
allowed_origins:
- http://127.0.0.1:3000
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
allowed_headers:
- Authorization
- Cookie
- Content-Type
exposed_headers:
- Content-Type
- Set-Cookie
admin:
base_url: http://kratos:4434/
selfservice:
default_browser_return_url: http://127.0.0.1:3000/
allowed_return_urls:
- http://127.0.0.1:3000/
methods:
password:
enabled: true
flows:
error:
ui_url: http://127.0.0.1:3000/error
settings:
ui_url: http://127.0.0.1:3000/settings
privileged_session_max_age: 15m
recovery:
enabled: false
ui_url: http://127.0.0.1:3000/recovery
verification:
enabled: false
ui_url: http://127.0.0.1:3000/verification
after:
default_browser_return_url: http://127.0.0.1:3000/
logout:
after:
default_browser_return_url: http://127.0.0.1:3000/login
login:
ui_url: http://127.0.0.1:3000/login
lifespan: 10m
registration:
lifespan: 10m
ui_url: http://127.0.0.1:3000/signup
log:
level: debug
format: text
leak_sensitive_values: true
secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
cipher:
- 32-LONG-SECRET-NOT-SECURE-AT-ALL
ciphers:
algorithm: xchacha20-poly1305
hashers:
algorithm: bcrypt
bcrypt:
cost: 8
identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/default.schema.json
courier:
smtp:
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true

13
.env.sample Normal file
View File

@ -0,0 +1,13 @@
# twitch-clone postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
# Ory Kratos postgres
KRATOS_POSTGRES_USER=kratos
KRATOS_POSTGRES_PASSWORD=secret
KRATOS_POSTGRES_DB=kratos
# Ory Kratos secrets
KRATOS_COOKIE_SECRET=dnIgMTQgb2t0IDIwMjIgMTI6Mzg6NTMg
KRATOS_CIPHER_SECRET=dnIgMTQgb2t0IDIwMjIgMTI6Mzg6NTMg

4
.gitignore vendored
View File

@ -21,3 +21,7 @@
go.work
tmp/
dist/
# Secrets
.env

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
.PHONY: chat-service
chat-service:
go build -o ./tmp/main ./cmd/chat-service/main.go

View File

@ -1 +1,41 @@
![](assets/logo.png)
# twitch-clone
A Twitch clone to experiment with various technologies.
![](assets/screenshot.png)
## Technologies
- ScyllaDB for storing messages, compatible with Cassandra but much faster. Usage is inspired by Discord's usage of the technology.
- Ory Kratos for auth, a very comprehensive open-source Auth0 competitor.
- Sentry for client-side monitoring, to help find bugs or other issues early.
- Next.JS for the frontend, React with SSR important for SEO and to speed up the initial load.
- Tailwind for styling, eliminating the need to manage stylesheets.
## Dependencies
- [podman](https://github.com/containers/podman)
- [pnpm](https://github.com/pnpm/pnpm)
- [node](https://github.com/nodejs/node)
- [go](https://github.com/golang/go)
## Usage
Start the necessary infrastructure:
```sh
podman-compose up
```
Start the frontend:
```sh
cd client
pnpm i
pnpm run dev
```
Start the chat-service:
```
go run ./cmd/chat-service/main.go
```
## Disclaimer
I am in no way affiliated with Amazon or Twitch.

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

43
chat-service/app/app.go Normal file
View File

@ -0,0 +1,43 @@
package app
import (
"twitch-clone/chat-service/logic"
"twitch-clone/chat-service/models"
"twitch-clone/chat-service/repository"
"twitch-clone/chat-service/repository/scylla"
service "twitch-clone/chat-service/services"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func Run() {
err := godotenv.Load()
if err != nil {
panic(err)
}
e := echo.New()
var c repository.ChatRepository = scylla.NewChatRepository()
var s service.ChatService = logic.NewChatService(c)
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("*", func(c echo.Context) error {
return s.Subscribe(c.Response().Writer, c.Request())
})
e.POST("*", func(c echo.Context) error {
return s.Publish(c.Request().URL.Path, &models.ChatMessage{
FromUserID: 0,
FromUser: "niku",
ToUserID: 0,
ToUser: "niku",
Content: "Welcome",
})
})
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -0,0 +1,70 @@
package logic
import (
"encoding/json"
"net/http"
"time"
"twitch-clone/chat-service/models"
"twitch-clone/chat-service/repository"
"twitch-clone/chat-service/serializer"
"github.com/bwmarrin/snowflake"
"github.com/olahol/melody"
)
type chatService struct {
ChatRepo repository.ChatRepository
Melody melody.Melody
MsgSerializer serializer.JsonMessageSerializer
Snowflake snowflake.Node
}
func NewChatService(chatRepo repository.ChatRepository) *chatService {
flakeGen, err := snowflake.NewNode(0)
if err != nil {
panic(err)
}
c := &chatService{
ChatRepo: chatRepo,
Melody: *melody.New(),
MsgSerializer: serializer.JsonMessageSerializer{},
Snowflake: *flakeGen,
}
c.Melody.HandleMessage(func(s *melody.Session, b []byte) {
msg, err := c.MsgSerializer.Decode(b)
if err != nil {
bytes, _ := json.Marshal(err.Error())
s.Write(bytes)
return
}
c.Publish(s.Request.URL.Path, msg)
})
return c
}
func (c *chatService) Subscribe(w http.ResponseWriter, r *http.Request) error {
return c.Melody.HandleRequest(w, r)
}
func (c *chatService) Publish(namespace string, msg *models.ChatMessage) error {
msg.MessageID = c.Snowflake.Generate().Int64()
msg.CreatedAt = time.Now().Unix()
rawMsg, err := c.MsgSerializer.Encode(msg)
if err != nil {
return err
}
err = c.Melody.BroadcastFilter(rawMsg, func(q *melody.Session) bool {
return q.Request.URL.Path == namespace
})
if err != nil {
return err
}
err = c.ChatRepo.Store(msg)
return err
}

View File

@ -0,0 +1,11 @@
package models
type ChatMessage struct {
MessageID int64 `json:"messageId,omitempty"`
FromUserID int64 `validate:"required" json:"fromUserID"`
FromUser string `validate:"required" json:"fromUser"`
ToUserID int64 `validate:"required" json:"toUserID"`
ToUser string `validate:"required" json:"toUser"`
Content string `validate:"required" json:"content"`
CreatedAt int64 `json:"createdAt,omitempty"`
}

View File

@ -0,0 +1,9 @@
package repository
import (
"twitch-clone/chat-service/models"
)
type ChatRepository interface {
Store(*models.ChatMessage) error
}

View File

@ -0,0 +1,48 @@
package scylla
import (
"os"
"twitch-clone/chat-service/models"
"github.com/gocql/gocql"
"github.com/scylladb/gocqlx/v2"
"github.com/scylladb/gocqlx/v2/table"
)
var messageMetadata = table.Metadata{
Name: "chat_service.messages",
Columns: []string{"message_id", "from_user_id", "from_user", "to_user_id", "to_user", "content", "created_at"},
}
var messageTable = table.New(messageMetadata)
type ChatRepository struct {
cluster gocql.ClusterConfig
}
func (r *ChatRepository) Store(msg *models.ChatMessage) error {
session, err := gocqlx.WrapSession(r.cluster.CreateSession())
if err != nil {
return err
}
defer session.Close()
q := session.Query(messageTable.Insert()).BindStruct(msg)
if err := q.ExecRelease(); err != nil {
return err
}
return nil
}
func NewChatRepository() *ChatRepository {
cluster := gocql.NewCluster(os.Getenv("CHAT_SCYLLA_HOSTS"))
session, _ := gocqlx.WrapSession(cluster.CreateSession())
Seed(session)
session.Close()
return &ChatRepository{
cluster: *cluster,
}
}

View File

@ -0,0 +1,30 @@
package scylla
import "github.com/scylladb/gocqlx/v2"
func Seed(session gocqlx.Session) error {
err := session.ExecStmt(`
CREATE KEYSPACE IF NOT EXISTS chat_service
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
if err != nil {
return err
}
err = session.ExecStmt(`
CREATE TABLE IF NOT EXISTS chat_service.messages (
message_id bigint,
from_user_id bigint,
from_user text,
to_user_id bigint,
to_user text,
content text,
created_at timestamp,
PRIMARY KEY (to_user_id, message_id)
)`)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,31 @@
package serializer
import (
"encoding/json"
"twitch-clone/chat-service/models"
"twitch-clone/chat-service/utils"
)
type JsonMessageSerializer struct{}
func (s *JsonMessageSerializer) Decode(input []byte) (*models.ChatMessage, error) {
msg := &models.ChatMessage{}
if err := json.Unmarshal(input, msg); err != nil {
return nil, err
}
if err := utils.Validate.Struct(msg); err != nil {
return nil, err
}
return msg, nil
}
func (s *JsonMessageSerializer) Encode(input *models.ChatMessage) ([]byte, error) {
msg, err := json.Marshal(input)
if err != nil {
return nil, err
}
return msg, nil
}

View File

@ -0,0 +1,8 @@
package serializer
import "twitch-clone/chat-service/models"
type MessageSerializer interface {
Decode(input []byte) (*models.ChatMessage, error)
Encode(input *models.ChatMessage) ([]byte, error)
}

View File

@ -0,0 +1,11 @@
package service
import (
"net/http"
"twitch-clone/chat-service/models"
)
type ChatService interface {
Subscribe(http.ResponseWriter, *http.Request) error
Publish(string, *models.ChatMessage) error
}

View File

@ -1,4 +1,4 @@
package models
package utils
import "github.com/go-playground/validator/v10"

1
client/.env.sample Normal file
View File

@ -0,0 +1 @@
ORY_SDK_URL=http://localhost:4433

3
client/.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

43
client/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# 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
cypress/screenshots
cypress/videos
# Sentry
.sentryclirc

54
client/.package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build --outDir ../dist",
"preview": "vite preview",
"lint": "eslint --fix --ext .js,.ts,.tsx ./src --ignore-path .gitignore",
"prettier": "prettier --ignore-path .gitignore --write \"**/*.+(js|json|ts|tsx)\"",
"cypress": "cypress"
},
"dependencies": {
"@headlessui/react": "^1.7.2",
"@heroicons/react": "^2.0.11",
"@hookform/resolvers": "^2.9.8",
"@ory/client": "0.2.0-alpha.48",
"axios": "^0.27.2",
"clsx": "^1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.36.1",
"react-router-dom": "^6.4.1",
"tailwind-scrollbar": "^2.0.1",
"zod": "^3.19.1"
},
"devDependencies": {
"@testing-library/cypress": "^8.0.3",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"@vitejs/plugin-react": "^2.1.0",
"autoprefixer": "^10.4.12",
"cypress": "^10.9.0",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.7.2",
"postcss": "^8.4.16",
"prettier": "^2.7.1",
"tailwindcss": "^3.1.8",
"typescript": "^4.6.4",
"vite": "^3.1.0"
}
}

6
client/.prettierrc.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
...require('ory-prettier-styles'),
importOrder: ['^\\.\\./(?!.*\\.[a-z]+$)(.*)$', '^\\./(?!.*\\.[a-z]+$)(.*)$'],
importOrderSeparation: true,
experimentalBabelParserPluginsList: ['jsx', 'typescript']
}

View File

@ -0,0 +1,71 @@
import { FC, KeyboardEventHandler, useEffect, useRef, useState } from "react"
import { CHAT_URL, MAX_CHAT_MESSAGES } from "../../config"
import useSession from "../../hooks/useSession"
import { ChatMessage as Message } from "../../types"
import Input from "../common/Input"
import ChatMessage from "../message/ChatMessage"
const Chat: FC = () => {
const { session } = useSession()
const [messages, setMessages] = useState<Message[]>([])
const wsRef = useRef<WebSocket | null>(null)
const messagesEndRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
wsRef.current = new WebSocket(CHAT_URL)
wsRef.current.onmessage = (ev) => {
const newMsg = JSON.parse(ev.data) as Message
if (typeof newMsg === "string") return
setMessages((old) => {
if (old.length >= MAX_CHAT_MESSAGES) old.shift()
return [...old, newMsg]
})
}
return () => {
if (wsRef.current) wsRef.current.close()
}
}, [])
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages])
const handleChatInput: KeyboardEventHandler<HTMLInputElement> = (event) => {
if (event.key === "Enter" && wsRef.current) {
const msg = JSON.stringify({
fromUser: "niku",
fromUserID: 10,
toUser: "niku",
toUserID: 10,
content: event.currentTarget.value,
})
wsRef.current.send(msg)
event.currentTarget.value = ""
}
}
return (
<div className="bg-zinc-900 w-80 border-l border-l-zinc-700 flex flex-col">
<div className="flex flex-row justify-center items-center border-b border-b-zinc-700 p-2 h-12">
<p className="uppercase font-semibold text-sm">Stream Chat</p>
</div>
<div className="flex-1 overflow-scrollbar">
{messages.map((message) => (
<ChatMessage key={message.messageId.toString()} message={message} />
))}
<div ref={messagesEndRef} />
</div>
<div className="m-2">
<Input
disabled={!session}
className="w-full p-2"
placeholder="Send a message"
onKeyDown={handleChatInput}
/>
</div>
</div>
)
}
export default Chat

View File

@ -0,0 +1,25 @@
import clsx from 'clsx';
import { FC } from 'react';
type ButtonVariants = 'filled' | 'subtle';
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
variant?: ButtonVariants;
}
const getStyling = (variant?: ButtonVariants) => {
switch (variant) {
case 'filled':
return 'bg-neutral-700';
case 'subtle':
return 'hover:bg-neutral-500';
default:
return 'bg-neutral-700';
}
};
const Button: FC<ButtonProps> = ({ className, variant, ...rest }) => {
return <button className={clsx('rounded-md', getStyling(variant), className)} {...rest} />;
};
export default Button;

View File

@ -0,0 +1,17 @@
import clsx from 'clsx';
import { FC } from 'react';
import Link from 'next/link';
export interface InlineLinkProps extends React.ComponentPropsWithoutRef<'span'> {
to: string;
}
const InlineLink: FC<InlineLinkProps> = ({ to, className, ...rest }) => {
return (
<Link href={to} passHref={true}>
<span className={clsx('text-violet-400 cursor-pointer text-sm', className)} {...rest} />
</Link>
);
};
export default InlineLink;

View File

@ -0,0 +1,22 @@
/* eslint-disable react/display-name */
import clsx from "clsx"
import { forwardRef } from "react"
type InputProps = React.ComponentPropsWithoutRef<"input">
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, ...rest }, ref) => {
return (
<input
className={clsx(
"bg-zinc-700 rounded-md box-border focus:outline outline-violet-400 text-sm",
className,
)}
{...rest}
ref={ref}
/>
)
},
)
export default Input

View File

@ -0,0 +1,11 @@
import { FC } from "react"
interface LogoProps {
className?: string
}
const Logo: FC<LogoProps> = ({ className }) => {
return <img src="./assets/images/logo.png" className={className} alt="logo" />
}
export default Logo

View File

@ -0,0 +1,25 @@
/* eslint-disable react/display-name */
import { forwardRef, ReactNode } from "react"
import Input from "../Input"
interface FormFieldProps extends React.ComponentPropsWithoutRef<"input"> {
label?: string
bottomElement?: ReactNode
}
const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
({ label, bottomElement, hidden, ...inputProps }, ref) => {
return (
<div className="space-y-1">
<label htmlFor={inputProps.id} className="font-semibold text-sm">
{label}
</label>
<Input {...inputProps} ref={ref} />
{bottomElement}
</div>
)
},
)
export default FormField

View File

@ -0,0 +1,19 @@
import clsx from "clsx"
import { FC } from "react"
type ButtonProps = React.ComponentPropsWithoutRef<"input">
const SubmitButton: FC<ButtonProps> = ({ className, ...rest }) => {
return (
<input
type="submit"
className={clsx(
"cursor-pointer rounded-md bg-violet-500 font-semibold py-2 text-sm",
className,
)}
{...rest}
/>
)
}
export default SubmitButton

View File

@ -0,0 +1,13 @@
import NavBar from "../nav/NavBar"
import { NextPage } from "next"
const BrowseLayout: NextPage = ({ children }) => {
return (
<div className="font-inter flex flex-col h-screen text-gray-100">
<NavBar />
<main className="flex-1 flex flex-row overflow-hidden">{children}</main>
</div>
)
}
export default BrowseLayout

View File

@ -0,0 +1,72 @@
import { FC } from "react"
import { useForm, SubmitHandler } from "react-hook-form"
import FormField from "../common/form/FormField"
import InlineLink from "../common/InlineLink"
import SubmitButton from "../common/form/SubmitButton"
import * as validation from "../../config/validation"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import useLogInFlow from "../../hooks/useLogInFlow"
import Input from "../common/Input"
const LogInFormSchema = z.object({
csrfToken: z.string(),
password: validation.password,
email: z.string().email({ message: "Not a valid email address" }).trim(),
})
type LogInFormValues = z.infer<typeof LogInFormSchema>
const formFields = [
{ id: "email", label: "Email", type: "email" },
{ id: "password", label: "Password", type: "password" },
]
const LoginForm: FC = () => {
const logInFlow = useLogInFlow()
const { register, handleSubmit, formState } = useForm<LogInFormValues>({
resolver: zodResolver(LogInFormSchema),
})
const onSubmit: SubmitHandler<LogInFormValues> = (data) =>
logInFlow.submitData({
csrf_token: data.csrfToken,
method: "password",
identifier: data.email,
password: data.password,
})
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{logInFlow.flow && (
<Input
hidden={true}
value={logInFlow.flow.ui.nodes[0].attributes.value}
{...register("csrfToken")}
/>
)}
{formFields.map((field) => (
<FormField
key={field.id}
id={field.id}
type={field.type}
label={field.label}
{...register(field.id as any)}
className="py-2 px-2 outline-2 w-full"
bottomElement={
<p className="text-xs">
{formState.errors[(field.id as any) || ""]?.message}
</p>
}
/>
))}
<SubmitButton className="w-full" value="Log In" />
<div className="text-center">
<InlineLink to="#">Trouble logging in?</InlineLink>
</div>
</form>
)
}
export default LoginForm

View File

@ -0,0 +1,57 @@
import { Dialog, Tab } from "@headlessui/react"
import { FC } from "react"
import Logo from "../common/Logo"
import LoginForm from "./LoginForm"
import LoginModalTab from "./LoginModalTab"
import SignupForm from "./SignupForm"
export interface LoginModelProps {
isOpen: boolean
onClose?: () => any
defaultPage?: number
}
const LoginModal: FC<LoginModelProps> = ({
defaultPage,
isOpen,
onClose = (b: boolean) => {},
}) => {
return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<div className="bg-black/80 fixed inset-0 flex items-center justify-center">
<Dialog.Panel className="bg-zinc-900 text-gray-100 w-[420px] rounded-md py-12 px-6">
<div className="flex flex-row items-center justify-center">
<Dialog.Title className="text-xl">
<Logo className="inline w-12 h-12" /> Log in to twitch-clone
</Dialog.Title>
</div>
<Tab.Group defaultIndex={defaultPage}>
<Tab.List className="space-x-4 border-b border-b-neutral-100/40 mt-4">
<Tab>
{({ selected }) => (
<LoginModalTab selected={selected}>Log In</LoginModalTab>
)}
</Tab>
<Tab>
{({ selected }) => (
<LoginModalTab selected={selected}>Sign Up</LoginModalTab>
)}
</Tab>
</Tab.List>
<Tab.Panels className="mt-4">
<Tab.Panel>
<LoginForm />
</Tab.Panel>
<Tab.Panel>
<SignupForm />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Dialog.Panel>
</div>
</Dialog>
)
}
export default LoginModal

View File

@ -0,0 +1,20 @@
import clsx from 'clsx';
import { FC } from 'react';
interface LoginModalTabProps extends React.ComponentPropsWithoutRef<'p'> {
selected: boolean;
}
const LoginModalTab: FC<LoginModalTabProps> = ({ selected, ...rest }) => {
return (
<p
className={clsx(
'font-semibold p-1',
selected && 'text-violet-400 border-b-2 border-b-violet-400'
)}
{...rest}
/>
);
};
export default LoginModalTab;

View File

@ -0,0 +1,97 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { FC } from "react"
import { SubmitHandler, useForm } from "react-hook-form"
import { z } from "zod"
import FormField from "../common/form/FormField"
import InlineLink from "../common/InlineLink"
import Input from "../common/Input"
import SubmitButton from "../common/form/SubmitButton"
import useSignUpFlow from "../../hooks/useSignUpFlow"
import * as validation from "../../config/validation"
const SignupFormSchema = z
.object({
csrfToken: z.string(),
username: validation.username,
password: validation.password,
passwordRepeat: z.string().trim(),
email: z.string().email({ message: "Not a valid email address" }).trim(),
})
.refine((data) => data.password === data.passwordRepeat, {
message: "Passwords do not match.",
path: ["passwordRepeat"],
})
type SignupFormValues = z.infer<typeof SignupFormSchema>
const formFields = [
{ id: "username", label: "Username", type: "text" },
{ id: "password", label: "Password", type: "password" },
{ id: "passwordRepeat", label: "Confirm Password", type: "password" },
{ id: "email", label: "Email", type: "email" },
]
const SignupForm: FC = () => {
const signUpFlow = useSignUpFlow()
const { register, handleSubmit, formState } = useForm<SignupFormValues>({
resolver: zodResolver(SignupFormSchema),
})
const onSubmit: SubmitHandler<SignupFormValues> = async (data) => {
await signUpFlow.submitData({
csrf_token: data.csrfToken,
method: "password",
password: data.password,
traits: {
email: data.email,
username: data.username,
},
})
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<p className="text-sm">
Creating an account allows you to participate in chat, follow your
favorite channels, and broadcast from your own channel.
</p>
{signUpFlow.flow && (
<Input
hidden={true}
value={signUpFlow.flow.ui.nodes[0].attributes.value}
{...register("csrfToken")}
/>
)}
{formFields.map((field) => (
<FormField
key={field.id}
id={field.id}
type={field.type}
label={field.label}
{...register(field.id as any)}
className="py-2 px-2 outline-2 w-full"
bottomElement={
<p className="text-xs">
{formState.errors[(field.id as any) || ""]?.message}
</p>
}
/>
))}
<p className="text-sm text-center">
By clicking Sign Up, you are agreeing to twitch-clone&apos;s{" "}
<InlineLink to="https://tosdr.org/en/service/200">
Terms of Service
</InlineLink>
.
</p>
<SubmitButton
disabled={!signUpFlow.flow}
className="w-full"
value="Sign Up"
/>
</form>
)
}
export default SignupForm

View File

@ -0,0 +1,21 @@
import { FC } from "react"
import { ChatMessage } from "../../types"
export interface ChatMessageProps {
message: ChatMessage
}
const ChatMessage: FC<ChatMessageProps> = ({
message: { fromUser, content },
}) => {
return (
<div className="mx-2 p-[0.4rem] hover:bg-neutral-700 text-xs rounded-md">
<div className="space-x-1 inline">
<span className="align-middle">{fromUser}: </span>
</div>
<span className="break-all align-middle">{content}</span>
</div>
)
}
export default ChatMessage

View File

@ -0,0 +1,93 @@
import {
ArrowRightIcon,
ArrowRightOnRectangleIcon,
UserIcon,
} from "@heroicons/react/24/outline"
import { FC, useState } from "react"
import { useLogout } from "../../hooks/useLogout"
import useSession from "../../hooks/useSession"
import Button from "../common/Button"
import Logo from "../common/Logo"
import LoginModal, { LoginModelProps } from "../login/LoginModal"
const NavBar: FC = () => {
const logout = useLogout()
const session = useSession((state) => state.session)
const [modalProps, setModalProps] = useState<LoginModelProps>({
isOpen: false,
defaultPage: 0,
})
const showLoginTab = () =>
setModalProps({
defaultPage: 0,
isOpen: true,
})
const showSignupTab = () =>
setModalProps({
defaultPage: 1,
isOpen: true,
})
console.log({ session })
return (
<nav className="bg-zinc-800 w-screen font-semibold border-b border-b-black">
<div className="flex flex-row justify-between items-center h-12 mx-2">
<div>
<ul className="flex flex-row space-x-8 items-center">
<li>
<Logo className="w-8 h-8" />
</li>
</ul>
</div>
<div>
<ul className="justify-end flex flex-row space-x-3 items-center">
{session ? (
<>
<li>
<UserIcon className="h-5 w-5 inline-block" />
</li>
<li>
<Button
variant="subtle"
className="p-[0.4rem]"
onClick={logout}
>
<ArrowRightOnRectangleIcon className="h-5 w-5 inline-block" />
</Button>
</li>
</>
) : (
<>
<li>
<Button
className="text-sm px-3 py-2 bg-neutral-700"
onClick={showLoginTab}
>
Log In
</Button>
</li>
<li>
<Button
className="text-sm px-3 py-2 bg-violet-500"
onClick={showSignupTab}
>
Sign Up
</Button>
</li>
</>
)}
</ul>
</div>
</div>
<LoginModal
{...modalProps}
onClose={() => setModalProps((old) => ({ ...old, isOpen: false }))}
/>
</nav>
)
}
export default NavBar

3
client/config/index.ts Normal file
View File

@ -0,0 +1,3 @@
export const KRATOS_URL = "http://127.0.0.1:4433"
export const CHAT_URL = "ws://localhost:1323"
export const MAX_CHAT_MESSAGES = 50

View File

@ -0,0 +1,18 @@
import { z } from "zod"
export const PASSWORD_REGEX =
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$/
export const username = z
.string()
.trim()
.min(3, { message: "Username must be at least 3 character long." })
.max(16, { message: "Username can't be longer than 16 characters.." })
export const password = z
.string()
.trim()
.regex(
PASSWORD_REGEX,
"Password must be 8-64 long and must contain a number, uppercase, lowercase and special character.",
)

3
client/cypress.json Normal file
View File

@ -0,0 +1,3 @@
{
"baseUrl": "http://localhost:3000"
}

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,64 @@
const randomString = () => (Math.random() + 1).toString(36).substring(7)
const randomPassword = () => randomString() + randomString()
const randomEmail = () => randomString() + '@' + randomString() + '.com'
const login = (email, password) => {
cy.visit('/api/.ory/ui/login')
cy.get('[name="identifier"]').type(email)
cy.get('[name="password"]').type(password)
cy.get('[name="method"]').click()
loggedIn(email)
}
const loggedIn = (email) => {
cy.location('pathname').should('eq', '/')
cy.get('[data-testid="session-content"]').should('contain.text', email)
cy.get('[data-testid="logout"]').should('have.attr', 'aria-disabled', 'false')
}
context('Basic UI interactions', () => {
const email = randomEmail()
const password = randomPassword()
beforeEach(() => {
cy.clearCookies({ domain: null })
})
it('can load the start page', () => {
cy.visit('/')
cy.get('a[href="/api/.ory/self-service/login/browser"]').should('exist')
cy.get('a[href="/api/.ory/self-service/registration/browser"]').should(
'exist'
)
})
it('redirects to login when accessing settings without session', () => {
cy.visit('/api/.ory/ui/settings')
cy.location('pathname').should('contain', 'api/.ory/ui/login')
cy.get('[name="method"]').should('exist')
})
it('can submit registration', () => {
cy.visit('/api/.ory/ui/registration')
cy.get('[name="traits.email"]').type(email)
cy.get('[name="password"]').type(password)
cy.get('[name="method"]').click()
loggedIn(email)
})
it('can load the login page', () => {
login(email, password)
})
it('goes to registration and clicks on log in and redirect works', () => {
cy.visit('/api/.ory/ui/registration')
cy.get('[data-testid="cta-link"]').click()
login(email, password)
})
it('can log out', () => {
login(email, password)
cy.get('a[data-testid="logout"]').click()
cy.get('[data-testid="logout"]').should('not.exist')
})
})

View File

@ -0,0 +1,22 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

View File

@ -0,0 +1,44 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>;
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>;
// dismiss(
// subject: string,
// options?: Partial<TypeOptions>
// ): Chainable<Element>;
// visit(
// originalFn: CommandOriginalFn,
// url: string,
// options: Partial<VisitOptions>
// ): Chainable<Element>;
// }
// }
// }

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@ -0,0 +1,39 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
import { mount } from 'cypress/react18';
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}
Cypress.Commands.add('mount', mount);
// Example use:
// cy.mount(<MyComponent />)

View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,19 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"isolatedModules": false
}
}

View File

@ -0,0 +1,53 @@
import {
SelfServiceRegistrationFlow,
SubmitSelfServiceLoginFlowBody,
} from "@ory/client"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import ory from "../services/ory"
import useSession from "./useSession"
const useLogInFlow = () => {
const router = useRouter()
const [flow, setFlow] = useState<SelfServiceRegistrationFlow>()
const { flow: flowId } = router.query
const updateSession = useSession((state) => state.update)
useEffect(() => {
const func = async () => {
if (!router.isReady || flow) {
return
}
let serviceFlow
if (flowId) {
serviceFlow = await ory.getSelfServiceLoginFlow(String(flowId))
} else {
serviceFlow = await ory.initializeSelfServiceLoginFlowForBrowsers()
}
setFlow(serviceFlow.data)
}
func()
}, [flowId, router, router.isReady, flow])
const submitData = async (data: SubmitSelfServiceLoginFlowBody) => {
await router.push(`/login?flow=${flow?.id}`, undefined, {
shallow: true,
})
ory
.submitSelfServiceLoginFlow(String(flow?.id), undefined, data)
.then(async ({ data }) => {
updateSession(data.session)
await router.push(flow?.return_to || "/")
})
.catch((err) => {
console.log({ err })
})
}
return { flow, submitData }
}
export default useLogInFlow

38
client/hooks/useLogout.ts Normal file
View File

@ -0,0 +1,38 @@
import { AxiosError } from "axios"
import { useRouter } from "next/router"
import { useState, useEffect, DependencyList } from "react"
import ory from "../services/ory"
import useSession from "./useSession"
export function useLogout(deps?: DependencyList) {
const session = useSession()
const [logoutToken, setLogoutToken] = useState<string>("")
const router = useRouter()
useEffect(() => {
ory
.createSelfServiceLogoutFlowUrlForBrowsers()
.then(({ data }) => {
setLogoutToken(data.logout_token)
})
.catch((err: AxiosError) => {
switch (err.response?.status) {
case 401:
return
}
return Promise.reject(err)
})
}, deps)
return () => {
if (logoutToken) {
session.drop()
ory
.submitSelfServiceLogoutFlow(logoutToken)
.then(() => router.push("/"))
.then(() => router.reload())
}
}
}

View File

@ -0,0 +1,31 @@
import create from "zustand"
import { Session } from "@ory/client"
import ory from "../services/ory"
import { AxiosError } from "axios"
export interface SessionState {
session?: Session
load: () => void
update: (data: Session) => void
drop: () => void
}
const useSession = create<SessionState>((set) => ({
session: undefined,
load: () => {
ory
.toSession()
.then(({ data }) => {
set({ session: data })
})
.catch((err: AxiosError) => {})
},
update: (session) => set({ session }),
drop: () => {
set({ session: undefined })
},
}))
export default useSession

View File

@ -0,0 +1,55 @@
import {
SelfServiceRegistrationFlow,
Session,
SubmitSelfServiceRegistrationFlowBody,
} from "@ory/client"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import ory from "../services/ory"
import useSession from "./useSession"
export const useSignUpFlow = () => {
const router = useRouter()
const [flow, setFlow] = useState<SelfServiceRegistrationFlow>()
const { flow: flowId } = router.query
const updateSession = useSession((state) => state.update)
useEffect(() => {
const func = async () => {
if (!router.isReady || flow) {
return
}
let serviceFlow
if (flowId) {
serviceFlow = await ory.getSelfServiceRegistrationFlow(String(flowId))
} else {
serviceFlow =
await ory.initializeSelfServiceRegistrationFlowForBrowsers()
}
setFlow(serviceFlow.data)
}
func()
}, [flowId, router, router.isReady, flow])
const submitData = async (data: SubmitSelfServiceRegistrationFlowBody) => {
await router.push(`/signup?flow=${flow?.id}`, undefined, {
shallow: true,
})
ory
.submitSelfServiceRegistrationFlow(String(flow?.id), data)
.then(async ({ data }) => {
updateSession(data.session as Session)
await router.push(flow?.return_to || "/")
})
.catch((err) => {
console.log({ err })
})
}
return { flow, submitData }
}
export default useSignUpFlow

5
client/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.

36
client/next.config.js Normal file
View File

@ -0,0 +1,36 @@
// This file sets a custom webpack configuration to use your Next.js app
// with Sentry.
// https://nextjs.org/docs/api-reference/next.config.js/introduction
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
const { withSentryConfig } = require("@sentry/nextjs")
const moduleExports = {
// Your existing module.exports
reactStrictMode: true,
sentry: {
// Use `hidden-source-map` rather than `source-map` as the Webpack `devtool`
// for client-side builds. (This will be the default starting in
// `@sentry/nextjs` version 8.0.0.) See
// https://webpack.js.org/configuration/devtool/ and
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map
// for more information.
hideSourceMaps: true,
},
}
const sentryWebpackPluginOptions = {
// Additional config options for the Sentry Webpack plugin. Keep in mind that
// the following options are set automatically, and overriding them is not
// recommended:
// release, url, org, project, authToken, configFile, stripPrefix,
// urlPrefix, include, ignore
silent: true, // Suppresses all logs
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options.
}
// Make sure adding Sentry options is the last code to run before exporting, to
// ensure that your source maps include changes from all other Webpack plugins
module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions)

51
client/package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "kratos-nextjs-react-example",
"version": "0.1.0",
"private": true,
"config": {
"prettierTarget": "{cypress,pages,styles,public,}{/**/,}*.{tsx,ts,json,md,js,css}"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write ${npm_package_config_prettierTarget}",
"format:check": "prettier --check ${npm_package_config_prettierTarget}",
"test": "cypress run",
"test:dev": "cypress open"
},
"dependencies": {
"@headlessui/react": "^1.7.3",
"@heroicons/react": "^2.0.12",
"@hookform/resolvers": "^2.9.8",
"@ory/client": "0.0.1-alpha.169",
"@ory/integrations": "0.2.5",
"@sentry/nextjs": "^7.16.0",
"axios": "^1.1.2",
"clsx": "^1.2.1",
"next": "12.1.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-hook-form": "^7.37.0",
"tailwind-scrollbar": "2.1.0-preview.0",
"zod": "^3.19.1",
"zustand": "^4.1.2"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@sentry/cli": "^2.7.0",
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
"@types/node": "16.11.6",
"@types/react": "17.0.33",
"autoprefixer": "^10.4.12",
"cypress": "^9.6.0",
"eslint": "7.32.0",
"eslint-config-next": "12.0.1",
"ory-prettier-styles": "^1.3.0",
"postcss": "^8.4.18",
"prettier": "^2.3.2",
"tailwindcss": "^3.1.8",
"typescript": "4.4.4"
}
}

View File

@ -0,0 +1,28 @@
import ChatMessage from "../../components/message/ChatMessage"
import Input from "../../components/common/Input"
import { NextPage } from "next"
import BrowseLayout from "../../components/layout/BrowseLayout"
import { createRandomMessage } from "../../placeholder/chatMessages"
import Chat from "../../components/chat"
const ChannelPage: NextPage = () => {
return (
<BrowseLayout>
<div className="flex-1 flex flex-row">
<div className="bg-neutral-900 flex-1 flex flex-col">
<div className="w-full bg-red-200 flex-1" />
<div className="flex flex-row p-2 items-center justify-between">
<div className="flex flex-row items-center space-x-3">
<span className="w-8 h-8 bg-yellow-300 rounded-full" />
<span className="font-bold">niku</span>
</div>
<div>1:14:32</div>
</div>
</div>
<Chat />
</div>
</BrowseLayout>
)
}
export default ChannelPage

15
client/pages/_app.tsx Normal file
View File

@ -0,0 +1,15 @@
import "../styles/globals.css"
import type { AppProps } from "next/app"
import useSession from "../hooks/useSession"
import { useEffect } from "react"
function MyApp({ Component, pageProps }: AppProps) {
const loadSession = useSession((state) => state.load)
useEffect(() => {
loadSession()
}, [loadSession])
return <Component {...pageProps} />
}
export default MyApp

39
client/pages/_error.js Normal file
View File

@ -0,0 +1,39 @@
/**
* NOTE: This requires `@sentry/nextjs` version 7.3.0 or higher.
*
* NOTE: If using this with `next` version 12.2.0 or lower, uncomment the
* penultimate line in `CustomErrorComponent`.
*
* This page is loaded by Nextjs:
* - on the server, when data-fetching methods throw or reject
* - on the client, when `getInitialProps` throws or rejects
* - on the client, when a React lifecycle method throws or rejects, and it's
* caught by the built-in Nextjs error boundary
*
* See:
* - https://nextjs.org/docs/basic-features/data-fetching/overview
* - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props
* - https://reactjs.org/docs/error-boundaries.html
*/
import * as Sentry from "@sentry/nextjs"
import NextErrorComponent from "next/error"
const CustomErrorComponent = (props) => {
// If you're using a Nextjs version prior to 12.2.1, uncomment this to
// compensate for https://github.com/vercel/next.js/issues/8592
// Sentry.captureUnderscoreErrorException(props);
return <NextErrorComponent statusCode={props.statusCode} />
}
CustomErrorComponent.getInitialProps = async (contextData) => {
// In case this is running in a serverless function, await this in order to give Sentry
// time to send the error before the lambda exits
await Sentry.captureUnderscoreErrorException(contextData)
// This will contain the status code of the response
return NextErrorComponent.getInitialProps(contextData)
}
export default CustomErrorComponent

View File

@ -0,0 +1,13 @@
// @ory/integrations offers a package for integrating with NextJS.
import { config, createApiHandler } from "@ory/integrations/next-edge"
// We need to export the config.
export { config }
// And create the Ory Cloud API "bridge".
export default createApiHandler({
fallbackToPlayground: true,
// Because vercel.app is a public suffix and setting cookies for
// vercel.app is not possible.
dontUseTldForCookieDomain: true,
})

9
client/pages/index.tsx Normal file
View File

@ -0,0 +1,9 @@
import { NextPage } from "next"
import useSession from "../hooks/useSession"
const IndexPage: NextPage = () => {
const session = useSession()
return <div>{JSON.stringify(session.session || {})}</div>
}
export default IndexPage

13
client/pages/login.tsx Normal file
View File

@ -0,0 +1,13 @@
import { NextPage } from "next"
import LoginModal from "../components/login/LoginModal"
const LoginPage: NextPage = () => {
return (
<div className="bg-neutral-900 w-screen h-screen">
<LoginModal isOpen={true} defaultPage={0} />
</div>
)
}
export default LoginPage

12
client/pages/signup.tsx Normal file
View File

@ -0,0 +1,12 @@
import { NextPage } from "next"
import LoginModal from "../components/login/LoginModal"
const SignupPage: NextPage = () => {
return (
<div className="bg-neutral-900 w-screen h-screen">
<LoginModal isOpen={true} defaultPage={1} />
</div>
)
}
export default SignupPage

4079
client/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
client/postcss.config.js Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

4
client/public/vercel.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
client/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,17 @@
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN || 'https://69c12baf84284b558f1aaea95321b6e5@o4504010426941440.ingest.sentry.io/4504010431397888',
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

4
client/sentry.properties Normal file
View File

@ -0,0 +1,4 @@
defaults.url=https://sentry.io/
defaults.org=niku-94
defaults.project=twitch-clone-frontend
cli.executable=./node_modules/.bin/sentry-cli

View File

@ -0,0 +1,17 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN || 'https://69c12baf84284b558f1aaea95321b6e5@o4504010426941440.ingest.sentry.io/4504010431397888',
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

View File

@ -0,0 +1,4 @@
import { Configuration, V0alpha2Api } from "@ory/client"
import { edgeConfig } from "@ory/integrations/next"
export default new V0alpha2Api(new Configuration(edgeConfig))

View File

@ -0,0 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Inter&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
.overflow-scrollbar {
@apply scrollbar-thin scrollbar-thumb-neutral-600 scrollbar-thumb-rounded-full overflow-y-auto;
}

15
client/tailwind.config.js Normal file
View File

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
fontFamily: {
inter: ["Inter"],
},
},
},
plugins: [require("tailwind-scrollbar")({ nocompatible: true })],
}

20
client/tsconfig.json Normal file
View 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"]
}

9
client/types/index.ts Normal file
View File

@ -0,0 +1,9 @@
export interface ChatMessage {
messageId: number
fromUserID: number
fromUser: string
toUserID: number
toUser: string
content: string
createdAt: number
}

1
client/utils/format.ts Normal file
View File

@ -0,0 +1 @@
export const numFormatter = Intl.NumberFormat('en', { notation: 'compact' });

9
cmd/chat-service/main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"twitch-clone/chat-service/app"
)
func main() {
app.Run()
}

View File

@ -1,9 +0,0 @@
{
"db": {
"user":"test",
"password":"pass",
"server":"test.mongodb.net",
"cluster":"test"
},
"env": "dev"
}

12
dist/index.html vendored
View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Hello there!</h1>
</body>
</html>

View File

@ -1,20 +1,71 @@
version: '3.3'
services:
app:
build: "."
depends_on:
- postgres
# app:
# build: '.'
# depends_on:
# - app-postgres
# ports:
# - 5000:5000
# rtmp:
# image: alfg/nginx-rtmp
# ports:
# - 1935:1935
# - 8080:80
# app-postgres:
# image: postgres:9.6
# ports:
# - '5433:5432'
# environment:
# - POSTGRES_USER=${POSTGRES_USER}
# - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# - POSTGRES_DB=${POSTGRES_DB}
chat-service-scylla:
image: docker.io/scylladb/scylla
ports:
- 5000:5000
rtmp:
image: alfg/nginx-rtmp
ports:
- 1935:1935
- 8080:80
postgres:
image: postgres:latest
- 7000:7000
- 7001:7001
- 9042:9042
- 9160:9160
- 10000:10000
kratos-migrate:
image: docker.io/oryd/kratos:v0.10.1
environment:
- POSTGRES_PASSWORD=postgres
- DSN=postgres://${KRATOS_POSTGRES_USER}:${KRATOS_POSTGRES_PASSWORD}@kratos-postgres:5432/${KRATOS_POSTGRES_DB}?sslmode=disable&max_conns=20&max_idle_conns=4
volumes:
- type: bind
source: .docker/kratos
target: /etc/config/kratos
command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes
restart: on-failure
kratos:
depends_on:
- kratos-migrate
image: docker.io/oryd/kratos:v0.10.1
ports:
- 5432:5432
- '4433:4433' # public
- '4434:4434' # admin
restart: unless-stopped
environment:
- DSN=postgres://${KRATOS_POSTGRES_USER}:${KRATOS_POSTGRES_PASSWORD}@kratos-postgres:5432/${KRATOS_POSTGRES_DB}?sslmode=disable&max_conns=20&max_idle_conns=4
- LOG_LEVEL=trace
- SECRETS_COOKIE_0=${KRATOS_COOKIE_SECRET}
- SECRETS_CIPHER_0=${KRATOS_CIPHER_SECRET}
command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier
volumes:
- type: bind
source: .docker/kratos
target: /etc/config/kratos
mailslurper:
image: docker.io/oryd/mailslurper:latest-smtps
ports:
- '4436:4436'
- '4437:4437'
kratos-postgres:
image: docker.io/postgres:9.6
ports:
- '5432:5432'
environment:
- POSTGRES_USER=${KRATOS_POSTGRES_USER}
- POSTGRES_PASSWORD=${KRATOS_POSTGRES_PASSWORD}
- POSTGRES_DB=${KRATOS_POSTGRES_DB}

44
go.mod
View File

@ -3,36 +3,34 @@ module twitch-clone
go 1.19
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/bwmarrin/snowflake v0.3.0
github.com/go-playground/validator/v10 v10.11.1
github.com/gofiber/fiber/v2 v2.37.1
github.com/golang-jwt/jwt v3.2.2+incompatible
gorm.io/driver/postgres v1.3.10
gorm.io/gorm v1.23.10
github.com/gocql/gocql v1.2.1
github.com/joho/godotenv v1.4.0
github.com/labstack/echo/v4 v4.9.1
github.com/olahol/melody v1.1.1
github.com/scylladb/gocqlx/v2 v2.7.0
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/scylladb/go-reflectx v1.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.40.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
)

237
go.sum
View File

@ -1,23 +1,13 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
@ -26,217 +16,88 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofiber/fiber/v2 v2.37.1 h1:QK2032gjv0ulegpv/qlTEBoXQD3eFFzCHXcNN12UZCs=
github.com/gofiber/fiber/v2 v2.37.1/go.mod h1:j3UslgQeJQP3mNhBxHnLLE8TPqA1Fd/lrl4gD25rRUY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gocql/gocql v1.2.1 h1:G/STxUzD6pGvRHzG0Fi7S04SXejMKBbRZb7pwre1edU=
github.com/gocql/gocql v1.2.1/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y=
github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/olahol/melody v1.1.1 h1:amgBhR7pDY0rA0JHWprgLF0LnVztognAwEQgf/WYLVM=
github.com/olahol/melody v1.1.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ=
github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc=
github.com/scylladb/gocqlx/v2 v2.7.0 h1:/w1VeJHCEAsg9eTculTvIS9eIe/VmEu0clhlH1CF7lc=
github.com/scylladb/gocqlx/v2 v2.7.0/go.mod h1:jKhM0/LkEAhEOSwd10TCMQdlC5x8aEzK7cXjQcPyMJ0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc=
github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.3.10 h1:Fsd+pQpFMGlGxxVMUPJhNo8gG8B1lKtk8QQ4/VZZAJw=
gorm.io/driver/postgres v1.3.10/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.10 h1:4Ne9ZbzID9GUxRkllxN4WjJKpsHx8YbKvekVdgyWh24=
gorm.io/gorm v1.23.10/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

View File

@ -1,9 +0,0 @@
package main
import (
"twitch-clone/pkg/app"
)
func main() {
app.Init()
}

View File

@ -1,59 +0,0 @@
package app
import (
"log"
"twitch-clone/pkg/database"
"twitch-clone/pkg/handler"
"twitch-clone/pkg/middleware"
"twitch-clone/pkg/models"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
)
/*Init : set the port,cors,api and then serve the api*/
func Init() {
app := fiber.New(fiber.Config{
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return ctx.Status(code).JSON(models.BaseError{Message: err.Error()})
},
})
database.ConnectDb()
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowMethods: "GET, POST, HEAD, PUT,DELETE, PATCH, OPTIONS",
AllowCredentials: true,
}))
app.Use(logger.New())
api := app.Group("/api")
v1 := api.Group("/v1")
auth := v1.Group("/auth")
auth.Post("login", handler.Login)
auth.Post("register", handler.Register)
test := v1.Group("/test", middleware.CheckToken)
test.Get("/", func(c *fiber.Ctx) error {
return c.SendString("This is a protected route!")
})
// Serve SPA
app.Static("/", "./dist")
app.Get("/*", func(ctx *fiber.Ctx) error {
return ctx.SendFile("./dist/index.html")
})
err := app.Listen(":5000")
if err != nil {
log.Fatal(err.Error())
}
}

View File

@ -1,54 +0,0 @@
package database
import (
"log"
"twitch-clone/pkg/models"
"github.com/bwmarrin/snowflake"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type Dbinstance struct {
Db *gorm.DB
Snowflake *snowflake.Node
}
var DB Dbinstance
// connectDb
func ConnectDb() {
node, err := snowflake.NewNode(1)
if err != nil {
log.Fatal("Failed to setup snowflake generator. \n", err)
}
dsn := "host=postgres user=postgres password=postgres dbname=postgres port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
log.Fatal("Failed to connect to database. \n", err)
}
log.Println("connected")
db.Logger = logger.Default.LogMode(logger.Info)
db.AutoMigrate(&models.User{})
DB = Dbinstance{
Db: db,
Snowflake: node,
}
}
func Db() *gorm.DB {
return DB.Db
}
func GetID() int64 {
return DB.Snowflake.Generate().Int64()
}

View File

@ -1,46 +0,0 @@
package handler
import (
"errors"
"twitch-clone/pkg/database"
"twitch-clone/pkg/jwt"
"twitch-clone/pkg/models"
"github.com/gofiber/fiber/v2"
)
type LoginRequest struct {
Username string `json:"username" validate:"required,min=4,max=32"`
Password string `json:"password" validate:"required,min=8,max=128"`
}
type LoginResponse struct {
Token string `json:"access_token"`
}
func Login(ctx *fiber.Ctx) error {
db := database.Db()
body := LoginRequest{}
if err := ctx.BodyParser(&body); err != nil {
return err
}
if err := models.Validate.Struct(body); err != nil {
return err
}
user := new(models.User)
result := db.Where(&models.User{Username: body.Username, Password: body.Password}).Select("id").First(user)
if result.Error != nil {
return errors.New("invalid combination of username and password")
}
token, err := jwt.GenerateJWT(models.Claim{ID: user.ID})
if err != nil {
return err
}
return ctx.JSON(LoginResponse{Token: token})
}

View File

@ -1,45 +0,0 @@
package handler
import (
"twitch-clone/pkg/database"
"twitch-clone/pkg/jwt"
"twitch-clone/pkg/models"
"github.com/gofiber/fiber/v2"
)
type RegisterRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=4,max=32"`
Password string `json:"password" validate:"required,min=8,max=128"`
}
type RegisterResponse struct {
Token string `json:"access_token"`
}
func Register(ctx *fiber.Ctx) error {
db := database.Db()
body := RegisterRequest{}
if err := ctx.BodyParser(&body); err != nil {
return err
}
if err := models.Validate.Struct(body); err != nil {
return err
}
user := models.User{ID: database.GetID(), Username: body.Username, Password: body.Password, Email: body.Email}
result := db.Create(&user)
if result.Error != nil {
return result.Error
}
token, err := jwt.GenerateJWT(models.Claim{ID: user.ID})
if err != nil {
return err
}
return ctx.JSON(LoginResponse{Token: token})
}

View File

@ -1,51 +0,0 @@
package jwt
import (
"errors"
"strings"
"time"
"twitch-clone/pkg/models"
jwt "github.com/golang-jwt/jwt"
)
func GenerateJWT(user models.Claim) (string, error) {
payload := jwt.MapClaims{
"id": user.ID,
"exp": time.Now().Add(time.Hour * 1500).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
tokenStr, err := token.SignedString([]byte(SecretKey))
if err != nil {
return tokenStr, err
}
return tokenStr, nil
}
func ProcessJWT(token string) (*models.Claim, bool, int64, error) {
claims := &models.Claim{}
splitToken := strings.Split(token, "Bearer")
if len(splitToken) != 2 {
return claims, false, 0, errors.New("invalid JWT format")
}
token = strings.TrimSpace(splitToken[1])
tkn, err := jwt.ParseWithClaims(token, claims, func(tk *jwt.Token) (interface{}, error) {
return []byte(SecretKey), nil
})
if err != nil {
return claims, false, 0, err
}
if !tkn.Valid {
return claims, false, 0, errors.New("invalid JWT token")
}
// TODO: validate whether user exists
return claims, true, claims.ID, nil
}

View File

@ -1,3 +0,0 @@
package jwt
const SecretKey = "secret"

View File

@ -1,18 +0,0 @@
package middleware
import (
"twitch-clone/pkg/jwt"
"github.com/gofiber/fiber/v2"
)
/*CheckToken : Check the validate of the jwt*/
func CheckToken(c *fiber.Ctx) error {
_, _, _, err := jwt.ProcessJWT(c.Get("Authorization"))
if err != nil {
return err
}
c.Next()
return nil
}

View File

@ -1,11 +0,0 @@
package models
import (
jwt "github.com/dgrijalva/jwt-go"
)
type Claim struct {
ID int64 `json:"id,omitempty"`
jwt.StandardClaims
}

View File

@ -1,5 +0,0 @@
package models
type BaseError struct {
Message string `json:"msg"`
}

View File

@ -1,16 +0,0 @@
package models
import (
"time"
)
type User struct {
ID int64 `json:"id,omitempty" gorm:"primaryKey"`
Username string `json:"name,omitempty" gorm:"unique"`
Email string `json:"email"`
Password string `json:"password,omitempty"`
Avatar string `json:"avatar,omitempty"`
CreatedAt time.Time
UpdatedAt time.Time
}