JWT key issue fix
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m24s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m24s
This commit is contained in:
parent
e1fdbd9388
commit
07aad3b8d4
5 changed files with 103 additions and 40 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,22 +22,20 @@ 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(() => {
|
||||||
return (
|
if (!downloadToken) return;
|
||||||
<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>
|
|
||||||
{loadError && <div className="mt-4 text-red-500 font-medium">Error: {loadError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getApiBase = () => {
|
const getApiBase = () => {
|
||||||
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
|
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
@ -47,15 +45,17 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit
|
||||||
|
|
||||||
const API_BASE = getApiBase();
|
const API_BASE = getApiBase();
|
||||||
|
|
||||||
const docServerUrl = 'https://office.elijahkuntz.com/';
|
// 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
|
// URL that OnlyOffice will use to download the file
|
||||||
const fileUrl = `${API_BASE}/api/files/download/?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
|
const fileUrl = `${INTERNAL_BACKEND_URL}/api/files/download/?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
|
||||||
|
|
||||||
// Callback URL that OnlyOffice will post to when saving
|
// Callback URL that OnlyOffice will post to when saving
|
||||||
const callbackUrl = `${API_BASE}/api/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
|
const callbackUrl = `${INTERNAL_BACKEND_URL}/api/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
|
||||||
|
|
||||||
const config = {
|
const baseConfig: any = {
|
||||||
document: {
|
document: {
|
||||||
fileType: file.name.split('.').pop() || 'docx',
|
fileType: file.name.split('.').pop() || 'docx',
|
||||||
key: `${file.path.replace(/[^a-zA-Z0-9\-_=]/g, '_')}_${new Date(file.mod_time).getTime()}`.substring(0, 128),
|
key: `${file.path.replace(/[^a-zA-Z0-9\-_=]/g, '_')}_${new Date(file.mod_time).getTime()}`.substring(0, 128),
|
||||||
|
|
@ -75,6 +75,32 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<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>
|
||||||
|
{loadError && <div className="mt-4 text-red-500 font-medium">Error: {loadError}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docServerUrl = 'https://office.elijahkuntz.com/';
|
||||||
|
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in a new issue