Compare commits
8 Commits
bff6cdd434
...
main
Author | SHA1 | Date | |
---|---|---|---|
573568ee6d | |||
9308ebba54 | |||
793d464eda | |||
bfdc915631 | |||
4d92bc3e86 | |||
b1a4470c68 | |||
f15eb98192 | |||
f606706ba3 |
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/
|
||||||
|
|
||||||
|
40
README.md
40
README.md
@ -1 +1,41 @@
|
|||||||
|

|
||||||
|
|
||||||
# twitch-clone
|
# twitch-clone
|
||||||
|
|
||||||
|
A Twitch clone to experiment with various technologies.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
BIN
assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
1
client/.env.sample
Normal file
1
client/.env.sample
Normal file
@ -0,0 +1 @@
|
|||||||
|
ORY_SDK_URL=http://localhost:4433
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -45,6 +45,7 @@ const SignupForm: FC = () => {
|
|||||||
password: data.password,
|
password: data.password,
|
||||||
traits: {
|
traits: {
|
||||||
email: data.email,
|
email: data.email,
|
||||||
|
username: data.username,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
Reference in New Issue
Block a user