Skip to content

Commit 3968b6b

Browse files
committed
Write out readme
1 parent e129cb5 commit 3968b6b

6 files changed

Lines changed: 100 additions & 8 deletions

File tree

client/src/index.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ function getSavedTheme() {
4343
// Branding: set to false to hide the "Powered by Codebox" message
4444
const SHOW_BRANDING = true
4545

46+
// Available languages for syntax highlighting
47+
const LANGUAGES = [
48+
{ id: 'markdown', label: 'Markdown' },
49+
{ id: 'plaintext', label: 'Plain Text' },
50+
{ id: 'javascript', label: 'JavaScript' },
51+
{ id: 'typescript', label: 'TypeScript' },
52+
{ id: 'python', label: 'Python' },
53+
{ id: 'html', label: 'HTML' },
54+
{ id: 'css', label: 'CSS' },
55+
{ id: 'json', label: 'JSON' },
56+
{ id: 'yaml', label: 'YAML' },
57+
{ id: 'sql', label: 'SQL' },
58+
{ id: 'shell', label: 'Shell' }
59+
]
60+
4661
function saveTheme(theme) {
4762
localStorage.setItem('codebox_theme', theme)
4863
}
@@ -533,6 +548,13 @@ function createBranding() {
533548
`
534549
}
535550

551+
function createLanguageSelector(currentLanguage = 'markdown') {
552+
const options = LANGUAGES.map(lang =>
553+
`<option value="${lang.id}" ${currentLanguage === lang.id ? 'selected' : ''}>${lang.label}</option>`
554+
).join('')
555+
return `<select id="language-select" class="language-select">${options}</select>`
556+
}
557+
536558
function startEditor(slug, token, canEdit, userName) {
537559
const app = document.getElementById('app')
538560

@@ -545,6 +567,7 @@ function startEditor(slug, token, canEdit, userName) {
545567
</div>
546568
<div class="controls">
547569
<span class="user-name">${userName}</span>
570+
${createLanguageSelector()}
548571
<span id="status" class="status hidden">connecting</span>
549572
${createThemeToggle()}
550573
<button id="connect-btn" class="hidden">Reconnect</button>
@@ -566,12 +589,14 @@ function startEditor(slug, token, canEdit, userName) {
566589
})
567590

568591
const ytext = ydoc.getText('monaco')
592+
const ySettings = ydoc.getMap('settings')
569593

570594
// Create Monaco editor
571595
const editorTheme = getEffectiveTheme() === 'dark' ? 'vs-dark' : 'vs'
596+
const initialLanguage = ySettings.get('language') || 'markdown'
572597
const editor = monaco.editor.create(document.getElementById('editor'), {
573598
value: '',
574-
language: 'markdown',
599+
language: initialLanguage,
575600
theme: editorTheme,
576601
automaticLayout: true,
577602
minimap: { enabled: false },
@@ -582,6 +607,39 @@ function startEditor(slug, token, canEdit, userName) {
582607
readOnly: !canEdit
583608
})
584609

610+
// Update language selector to match synced value
611+
const languageSelect = document.getElementById('language-select')
612+
613+
function updateLanguageFromYjs() {
614+
const language = ySettings.get('language') || 'markdown'
615+
if (languageSelect.value !== language) {
616+
languageSelect.value = language
617+
}
618+
const currentModel = editor.getModel()
619+
if (currentModel && monaco.editor.getModel(currentModel.uri)) {
620+
const currentLang = currentModel.getLanguageId()
621+
if (currentLang !== language) {
622+
monaco.editor.setModelLanguage(currentModel, language)
623+
}
624+
}
625+
}
626+
627+
// Listen for remote changes to language setting
628+
ySettings.observe(() => {
629+
updateLanguageFromYjs()
630+
})
631+
632+
// Update once synced
633+
provider.on('sync', () => {
634+
updateLanguageFromYjs()
635+
})
636+
637+
// Language selector handler - update Yjs when user changes
638+
languageSelect.addEventListener('change', (e) => {
639+
const newLanguage = e.target.value
640+
ySettings.set('language', newLanguage)
641+
})
642+
585643
// Bind Yjs to Monaco
586644
const monacoBinding = new MonacoBinding(
587645
ytext,

client/src/styles.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ header h1 {
104104
color: var(--text-muted);
105105
}
106106

107-
.theme-toggle {
107+
.theme-toggle,
108+
.language-select {
108109
background: var(--input-bg);
109110
border: 1px solid var(--input-border);
110111
color: var(--text-primary);
@@ -114,7 +115,8 @@ header h1 {
114115
cursor: pointer;
115116
}
116117

117-
.theme-toggle:focus {
118+
.theme-toggle:focus,
119+
.language-select:focus {
118120
outline: none;
119121
border-color: #007acc;
120122
}

client/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default {
3232
favicon: './src/favicon.svg'
3333
}),
3434
new MonacoWebpackPlugin({
35-
languages: ['javascript', 'typescript', 'json', 'html', 'css', 'markdown']
35+
languages: ['javascript', 'typescript', 'json', 'html', 'css', 'markdown', 'python', 'yaml', 'sql', 'shell', 'plaintext']
3636
})
3737
],
3838
devServer: {

readme.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
11
# Codebox
22

33
**Note:** this repo was built by and large with Claude, and builds off of the [Yjs Demos](https://github.com/yjs/yjs-demos) Monaco demo (public domain).
4+
5+
![Screenshot of a user editing a notepad with Codebox](./screenshots/editing.png)
6+
7+
## Features
8+
9+
* Real-time collaborative editing (Monaco + Yjs)
10+
* Live cursors with usernames
11+
* Syntax highlighting
12+
* Dark/light themes
13+
* Password protection options:
14+
- Unprotected (anyone can edit/view)
15+
- Edit protected (anyone can view but edit requires password)
16+
- Fully protected (both view/edit required password)
17+
- View only (no one can edit)
18+
* Admin dashboard
19+
* Pre-built Docker container
20+
21+
## Note on security
22+
23+
Don't use this for super secret stuff. Security caveats:
24+
25+
* Admin password is securely hashed with bcrypt
26+
* Notepad passwords are **stored in plain text** (so admin can see them) and use naive compare
27+
* When a user enters a notepad, they get a JWT token for 24 hours: if you change the protection level, their access is not automatically revoked

screenshots/editing.png

46.8 KB
Loading

server/auth.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import crypto from 'crypto'
34
import bcrypt from 'bcrypt'
45
import jwt from 'jsonwebtoken'
56

67
const SALT_ROUNDS = 12
78

8-
// JWT secret - in production, use environment variable
9-
const JWT_SECRET = process.env.JWT_SECRET || 'codebox-dev-secret-change-in-production'
10-
119
export function initAuth(dataDir) {
1210
const adminFile = path.join(dataDir, 'admin.json')
1311

1412
// Load or initialize admin data
15-
let adminData = { passwordHash: null }
13+
let adminData = { passwordHash: null, jwtSecret: null }
1614
if (fs.existsSync(adminFile)) {
1715
try {
1816
adminData = JSON.parse(fs.readFileSync(adminFile, 'utf-8'))
@@ -25,6 +23,16 @@ export function initAuth(dataDir) {
2523
fs.writeFileSync(adminFile, JSON.stringify(adminData, null, 2))
2624
}
2725

26+
// Generate JWT secret if not present
27+
if (!adminData.jwtSecret) {
28+
adminData.jwtSecret = crypto.randomBytes(64).toString('hex')
29+
saveAdminData()
30+
console.log('Generated new JWT secret')
31+
}
32+
33+
// Allow env override for JWT secret
34+
const JWT_SECRET = process.env.JWT_SECRET || adminData.jwtSecret
35+
2836
// Check if admin password is set
2937
function isAdminSetup() {
3038
return adminData.passwordHash !== null

0 commit comments

Comments
 (0)