Compare commits

...

8 Commits

12 changed files with 115 additions and 19 deletions

View File

@ -1,7 +1,7 @@
{ {
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", "$id": "file:///etc/config/kratos/default.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person", "title": "default",
"type": "object", "type": "object",
"properties": { "properties": {
"traits": { "traits": {
@ -10,6 +10,7 @@
"email": { "email": {
"type": "string", "type": "string",
"format": "email", "format": "email",
"title": "E-Mail",
"minLength": 3, "minLength": 3,
"ory.sh/kratos": { "ory.sh/kratos": {
"credentials": { "credentials": {
@ -31,7 +32,7 @@
"minLength": 3 "minLength": 3
} }
}, },
"required": ["email"], "required": ["email", "username"],
"additionalProperties": false "additionalProperties": false
} }
} }

View File

@ -1,12 +1,28 @@
version: v0.10.1 version: v0.10.1
dsn: memory dsn: memory
dev: true
serve: serve:
public: public:
base_url: http://127.0.0.1:4433/ base_url: http://127.0.0.1:4433/
cors: cors:
enabled: true 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: admin:
base_url: http://kratos:4434/ base_url: http://kratos:4434/

View File

@ -1 +1,41 @@
![](assets/logo.png)
# twitch-clone # 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

1
client/.env.sample Normal file
View File

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

View File

@ -1,20 +1,50 @@
import { FC, useEffect, useState } from "react" import { FC, KeyboardEventHandler, useEffect, useRef, useState } from "react"
import { CHAT_URL } from "../../config" import { CHAT_URL, MAX_CHAT_MESSAGES } from "../../config"
import useSession from "../../hooks/useSession"
import { ChatMessage as Message } from "../../types" import { ChatMessage as Message } from "../../types"
import Input from "../common/Input" import Input from "../common/Input"
import ChatMessage from "../message/ChatMessage" import ChatMessage from "../message/ChatMessage"
const Chat: FC = () => { const Chat: FC = () => {
const { session } = useSession()
const [messages, setMessages] = useState<Message[]>([]) const [messages, setMessages] = useState<Message[]>([])
const wsRef = useRef<WebSocket | null>(null)
const messagesEndRef = useRef<HTMLDivElement | null>(null)
useEffect(() => { useEffect(() => {
const ws = new WebSocket(CHAT_URL) wsRef.current = new WebSocket(CHAT_URL)
ws.onmessage = (ev) => { wsRef.current.onmessage = (ev) => {
const newMsg = JSON.parse(ev.data) as Message const newMsg = JSON.parse(ev.data) as Message
setMessages((old) => [...old, newMsg]) 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 ( return (
<div className="bg-zinc-900 w-80 border-l border-l-zinc-700 flex flex-col"> <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"> <div className="flex flex-row justify-center items-center border-b border-b-zinc-700 p-2 h-12">
@ -24,9 +54,15 @@ const Chat: FC = () => {
{messages.map((message) => ( {messages.map((message) => (
<ChatMessage key={message.messageId.toString()} message={message} /> <ChatMessage key={message.messageId.toString()} message={message} />
))} ))}
<div ref={messagesEndRef} />
</div> </div>
<div className="m-2"> <div className="m-2">
<Input className="w-full p-2" placeholder="Send a message" /> <Input
disabled={!session}
className="w-full p-2"
placeholder="Send a message"
onKeyDown={handleChatInput}
/>
</div> </div>
</div> </div>
) )

View File

@ -45,6 +45,7 @@ const SignupForm: FC = () => {
password: data.password, password: data.password,
traits: { traits: {
email: data.email, email: data.email,
username: data.username,
}, },
}) })
} }

View File

@ -9,7 +9,7 @@ const ChatMessage: FC<ChatMessageProps> = ({
message: { fromUser, content }, message: { fromUser, content },
}) => { }) => {
return ( return (
<div className="mx-2 p-2 hover:bg-neutral-700 text-sm rounded-md"> <div className="mx-2 p-[0.4rem] hover:bg-neutral-700 text-xs rounded-md">
<div className="space-x-1 inline"> <div className="space-x-1 inline">
<span className="align-middle">{fromUser}: </span> <span className="align-middle">{fromUser}: </span>
</div> </div>

View File

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

View File

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

View File

@ -21,7 +21,7 @@ services:
# - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} # - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# - POSTGRES_DB=${POSTGRES_DB} # - POSTGRES_DB=${POSTGRES_DB}
chat-service-scylla: chat-service-scylla:
image: scylladb/scylla image: docker.io/scylladb/scylla
ports: ports:
- 7000:7000 - 7000:7000
- 7001:7001 - 7001:7001
@ -29,7 +29,7 @@ services:
- 9160:9160 - 9160:9160
- 10000:10000 - 10000:10000
kratos-migrate: kratos-migrate:
image: oryd/kratos:v0.10.1 image: docker.io/oryd/kratos:v0.10.1
environment: environment:
- DSN=postgres://${KRATOS_POSTGRES_USER}:${KRATOS_POSTGRES_PASSWORD}@kratos-postgres:5432/${KRATOS_POSTGRES_DB}?sslmode=disable&max_conns=20&max_idle_conns=4 - DSN=postgres://${KRATOS_POSTGRES_USER}:${KRATOS_POSTGRES_PASSWORD}@kratos-postgres:5432/${KRATOS_POSTGRES_DB}?sslmode=disable&max_conns=20&max_idle_conns=4
volumes: volumes:
@ -41,7 +41,7 @@ services:
kratos: kratos:
depends_on: depends_on:
- kratos-migrate - kratos-migrate
image: oryd/kratos:v0.10.1 image: docker.io/oryd/kratos:v0.10.1
ports: ports:
- '4433:4433' # public - '4433:4433' # public
- '4434:4434' # admin - '4434:4434' # admin
@ -57,12 +57,12 @@ services:
source: .docker/kratos source: .docker/kratos
target: /etc/config/kratos target: /etc/config/kratos
mailslurper: mailslurper:
image: oryd/mailslurper:latest-smtps image: docker.io/oryd/mailslurper:latest-smtps
ports: ports:
- '4436:4436' - '4436:4436'
- '4437:4437' - '4437:4437'
kratos-postgres: kratos-postgres:
image: postgres:9.6 image: docker.io/postgres:9.6
ports: ports:
- '5432:5432' - '5432:5432'
environment: environment: