JWT key issue fix
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m24s

This commit is contained in:
Elijah 2026-05-24 11:26:55 -07:00
parent e1fdbd9388
commit 07aad3b8d4
5 changed files with 103 additions and 40 deletions

View file

@ -18,6 +18,7 @@ type Config struct {
BackupDir string BackupDir string
MaxLoginAttempts int MaxLoginAttempts int
LockoutSeconds int LockoutSeconds int
OnlyOfficeJWT string
} }
func Load() *Config { func Load() *Config {
@ -29,6 +30,7 @@ func Load() *Config {
JWTExpirySecs: getEnvInt("JWT_EXPIRY_SECS", 900), // 15 minutes JWTExpirySecs: getEnvInt("JWT_EXPIRY_SECS", 900), // 15 minutes
MaxLoginAttempts: getEnvInt("MAX_LOGIN_ATTEMPTS", 5), MaxLoginAttempts: getEnvInt("MAX_LOGIN_ATTEMPTS", 5),
LockoutSeconds: getEnvInt("LOCKOUT_SECONDS", 300), // 5 minutes LockoutSeconds: getEnvInt("LOCKOUT_SECONDS", 300), // 5 minutes
OnlyOfficeJWT: getEnv("ONLYOFFICE_JWT_SECRET", ""),
} }
cfg.DBPath = cfg.DataDir + "/drive.db" cfg.DBPath = cfg.DataDir + "/drive.db"

View file

@ -10,6 +10,7 @@ import (
"git.elijahkuntz.com/Elijah/drive/config" "git.elijahkuntz.com/Elijah/drive/config"
"git.elijahkuntz.com/Elijah/drive/middleware" "git.elijahkuntz.com/Elijah/drive/middleware"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
) )
type OnlyOfficeHandler struct { type OnlyOfficeHandler struct {
@ -127,3 +128,28 @@ func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error {
"name": filepath.Base(finalPath), "name": filepath.Base(finalPath),
}) })
} }
// SignConfig takes the OnlyOffice configuration payload and signs it with the JWT secret
func (h *OnlyOfficeHandler) SignConfig(c *fiber.Ctx) error {
if h.Config.OnlyOfficeJWT == "" {
return c.Status(500).JSON(fiber.Map{"error": "JWT secret is not configured on the server"})
}
var payload map[string]interface{}
if err := c.BodyParser(&payload); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid json payload"})
}
// Create a new JWT token using the HS256 algorithm
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload))
// Sign the token with our secret
signedToken, err := token.SignedString([]byte(h.Config.OnlyOfficeJWT))
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "failed to sign token"})
}
return c.JSON(fiber.Map{
"token": signedToken,
})
}

View file

@ -188,8 +188,9 @@ func main() {
protected.Post("/files/upload", fsHandler.Upload) protected.Post("/files/upload", fsHandler.Upload)
protected.Post("/files/check-conflicts", fsHandler.CheckConflicts) protected.Post("/files/check-conflicts", fsHandler.CheckConflicts)
// OnlyOffice blanks // OnlyOffice
protected.Post("/onlyoffice/create", onlyOfficeHandler.CreateBlank) protected.Post("/onlyoffice/create", onlyOfficeHandler.CreateBlank)
protected.Post("/onlyoffice/sign", onlyOfficeHandler.SignConfig)
protected.Get("/files/folders-tree", fsHandler.GetFolderTree) protected.Get("/files/folders-tree", fsHandler.GetFolderTree)

View file

@ -22,15 +22,75 @@ interface OnlyOfficeEditorProps {
export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEditorProps) { export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEditorProps) {
const [downloadToken, setDownloadToken] = useState<string | null>(null); const [downloadToken, setDownloadToken] = useState<string | null>(null);
const [loadError, setLoadError] = useState<string | null>(null); const [loadError, setLoadError] = useState<string | null>(null);
const [config, setConfig] = useState<any>(null);
useEffect(() => { useEffect(() => {
// We need a short-lived download token to pass to the Document Server // We need a short-lived download token to pass to the Document Server
api.createDownloadToken() api.createDownloadToken()
.then((token: string) => setDownloadToken(token)) .then((token: string) => setDownloadToken(token))
.catch(console.error); .catch(err => {
setLoadError("Failed to get download token");
console.error(err);
});
}, [file.path]); }, [file.path]);
if (!downloadToken) { useEffect(() => {
if (!downloadToken) return;
const getApiBase = () => {
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
if (typeof window !== 'undefined') return window.location.origin;
return 'http://localhost:8080';
};
const API_BASE = getApiBase();
// Use the internal backend URL for OnlyOffice server-to-server communication
// to avoid Hairpin NAT/DNS issues from inside the Docker network.
const INTERNAL_BACKEND_URL = 'http://192.168.50.81:5827';
// URL that OnlyOffice will use to download the file
const fileUrl = `${INTERNAL_BACKEND_URL}/api/files/download/?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
// Callback URL that OnlyOffice will post to when saving
const callbackUrl = `${INTERNAL_BACKEND_URL}/api/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
const baseConfig: any = {
document: {
fileType: file.name.split('.').pop() || 'docx',
key: `${file.path.replace(/[^a-zA-Z0-9\-_=]/g, '_')}_${new Date(file.mod_time).getTime()}`.substring(0, 128),
title: file.name,
url: fileUrl,
},
documentType: type,
editorConfig: {
callbackUrl: callbackUrl,
mode: 'edit',
customization: {
forcesave: true,
goback: {
url: '',
}
}
},
};
// Sign the config with our backend
api.signOnlyOfficeConfig(baseConfig)
.then((res: any) => {
if (res.token) {
baseConfig.token = res.token;
}
setConfig(baseConfig);
})
.catch((err) => {
console.warn("Failed to sign config (JWT secret might not be set). Falling back to unsigned config.", err);
setConfig(baseConfig);
});
}, [downloadToken, file, type]);
if (!config) {
return ( return (
<div className="flex-1 flex flex-col items-center justify-center" style={{ backgroundColor: 'var(--color-bg-primary)' }}> <div className="flex-1 flex flex-col items-center justify-center" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
<div className="text-sm" style={{ color: 'var(--color-text-tertiary)' }}>Loading Editor...</div> <div className="text-sm" style={{ color: 'var(--color-text-tertiary)' }}>Loading Editor...</div>
@ -39,41 +99,7 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit
); );
} }
const getApiBase = () => {
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
if (typeof window !== 'undefined') return window.location.origin;
return 'http://localhost:8080';
};
const API_BASE = getApiBase();
const docServerUrl = 'https://office.elijahkuntz.com/'; const docServerUrl = 'https://office.elijahkuntz.com/';
// URL that OnlyOffice will use to download the file
const fileUrl = `${API_BASE}/api/files/download/?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
// Callback URL that OnlyOffice will post to when saving
const callbackUrl = `${API_BASE}/api/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
const config = {
document: {
fileType: file.name.split('.').pop() || 'docx',
key: `${file.path.replace(/[^a-zA-Z0-9\-_=]/g, '_')}_${new Date(file.mod_time).getTime()}`.substring(0, 128),
title: file.name,
url: fileUrl,
},
documentType: type,
editorConfig: {
callbackUrl: callbackUrl,
mode: 'edit',
customization: {
forcesave: true,
goback: {
url: '',
}
}
},
};
const onDocumentReady = function (event: any) { const onDocumentReady = function (event: any) {
console.log("Document is loaded"); console.log("Document is loaded");
@ -109,7 +135,7 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit
<DocumentEditor <DocumentEditor
id="docxEditor" id="docxEditor"
documentServerUrl={docServerUrl} documentServerUrl={docServerUrl}
config={config as any} config={config}
events_onDocumentReady={onDocumentReady} events_onDocumentReady={onDocumentReady}
onLoadComponentError={onLoadComponentError} onLoadComponentError={onLoadComponentError}
/> />

View file

@ -471,9 +471,17 @@ class ApiClient {
// --- OnlyOffice --- // --- OnlyOffice ---
async createOnlyOfficeFile(type: 'word' | 'slide') { async createOnlyOfficeFile(type: 'word' | 'slide') {
const res = await this.request('/api/onlyoffice/create', { const res = await this.request('/onlyoffice/create', {
method: 'POST', method: 'POST',
body: { type }, body: JSON.stringify({ type })
});
return res.json();
}
async signOnlyOfficeConfig(config: any) {
const res = await this.request('/onlyoffice/sign', {
method: 'POST',
body: JSON.stringify(config)
}); });
return res.json(); return res.json();
} }