feat: add support for custom favicons (#4770)
* feat: add support for custom favicons * Commit updated API documentation * chore(i18n): add localization of text * fix(js): regenerate package lock file * chore(test): add favicon test * fix: max size not respected * Commit updated API documentation * fix: move favicon.ico to the static dir * fix: fix tests * fix: remove hard-coded content-type of icon * chore: update extracted translations * feat(admin): add support for resetting to default icon * chore: use a higher res default favicon * Commit updated API documentation --------- Co-authored-by: Owncast <owncast@owncast.online>
@@ -1704,6 +1704,60 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
$ref: '#/components/responses/204'
|
$ref: '#/components/responses/204'
|
||||||
|
/admin/config/favicon:
|
||||||
|
post:
|
||||||
|
summary: Upload custom favicon
|
||||||
|
operationId: SetFavicon
|
||||||
|
tags: ['Internal', 'Admin']
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
favicon:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
description: Favicon file (PNG or ICO, max 200KB)
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Favicon updated
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BaseAPIResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401BasicAuth'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/Default'
|
||||||
|
delete:
|
||||||
|
summary: Reset favicon to default
|
||||||
|
operationId: ResetFavicon
|
||||||
|
tags: ['Internal', 'Admin']
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Favicon reset to default
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BaseAPIResponse'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401BasicAuth'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/Default'
|
||||||
|
options:
|
||||||
|
operationId: SetFaviconOptions
|
||||||
|
x-internal: true
|
||||||
|
tags: ['Objects', 'Internal', 'Admin']
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
$ref: '#/components/responses/204'
|
||||||
/admin/config/tags:
|
/admin/config/tags:
|
||||||
post:
|
post:
|
||||||
summary: Update server tags
|
summary: Update server tags
|
||||||
|
|||||||
@@ -60,4 +60,5 @@ const (
|
|||||||
streamKeysKey = "stream_keys"
|
streamKeysKey = "stream_keys"
|
||||||
disableSearchIndexingKey = "disable_search_indexing"
|
disableSearchIndexingKey = "disable_search_indexing"
|
||||||
videoServingEndpointKey = "video_serving_endpoint"
|
videoServingEndpointKey = "video_serving_endpoint"
|
||||||
|
faviconPathKey = "favicon_path"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -129,4 +129,6 @@ type ConfigRepository interface {
|
|||||||
GetPrivateKey() string
|
GetPrivateKey() string
|
||||||
SetPublicKey(key string) error
|
SetPublicKey(key string) error
|
||||||
SetPrivateKey(key string) error
|
SetPrivateKey(key string) error
|
||||||
|
GetFaviconPath() string
|
||||||
|
SetFaviconPath(favicon string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,22 @@ func (r *SqlConfigRepository) GetLogoUniquenessString() string {
|
|||||||
return uniqueness
|
return uniqueness
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFaviconPath will return the filename for the favicon in the data directory.
|
||||||
|
func (r *SqlConfigRepository) GetFaviconPath() string {
|
||||||
|
favicon, err := r.datastore.GetString(faviconPathKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Traceln(faviconPathKey, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return favicon
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFaviconPath will set the filename for the favicon in the data directory.
|
||||||
|
func (r *SqlConfigRepository) SetFaviconPath(favicon string) error {
|
||||||
|
return r.datastore.SetString(faviconPathKey, favicon)
|
||||||
|
}
|
||||||
|
|
||||||
// GetServerSummary will return the server summary text.
|
// GetServerSummary will return the server summary text.
|
||||||
func (r *SqlConfigRepository) GetServerSummary() string {
|
func (r *SqlConfigRepository) GetServerSummary() string {
|
||||||
summary, err := r.datastore.GetString(serverSummaryKey)
|
summary, err := r.datastore.GetString(serverSummaryKey)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@@ -67,6 +67,14 @@ func GetLogo() []byte {
|
|||||||
return getFileSystemStaticFileOrDefault("img/logo.png", logo)
|
return getFileSystemStaticFileOrDefault("img/logo.png", logo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:embed favicon.png
|
||||||
|
var favicon []byte
|
||||||
|
|
||||||
|
// GetFavicon will return the favicon data.
|
||||||
|
func GetFavicon() []byte {
|
||||||
|
return getFileSystemStaticFileOrDefault("favicon.png", favicon)
|
||||||
|
}
|
||||||
|
|
||||||
func getFileSystemStaticFileOrDefault(path string, defaultData []byte) []byte {
|
func getFileSystemStaticFileOrDefault(path string, defaultData []byte) []byte {
|
||||||
fullPath := filepath.Join("static", path)
|
fullPath := filepath.Join("static", path)
|
||||||
data, err := os.ReadFile(fullPath) //nolint: gosec
|
data, err := os.ReadFile(fullPath) //nolint: gosec
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
|
||||||
|
Before Width: | Height: | Size: 661 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,184 @@
|
|||||||
|
var request = require('supertest');
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
request = request('http://127.0.0.1:8080');
|
||||||
|
|
||||||
|
const defaultAdminPassword = 'abc123';
|
||||||
|
|
||||||
|
// Create test favicon files
|
||||||
|
const testFaviconDir = path.join(__dirname, 'testdata');
|
||||||
|
|
||||||
|
// Ensure testdata directory exists
|
||||||
|
if (!fs.existsSync(testFaviconDir)) {
|
||||||
|
fs.mkdirSync(testFaviconDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a minimal valid PNG file (1x1 pixel)
|
||||||
|
const minimalPNG = Buffer.from([
|
||||||
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
|
||||||
|
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
|
||||||
|
0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44,
|
||||||
|
0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff, 0x3f, 0x00, 0x05, 0xfe, 0x02,
|
||||||
|
0xfe, 0xdc, 0xcc, 0x59, 0xe7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
|
||||||
|
0xae, 0x42, 0x60, 0x82,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a minimal valid ICO file (1x1 pixel)
|
||||||
|
const minimalICO = Buffer.from([
|
||||||
|
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x18,
|
||||||
|
0x00, 0x30, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a minimal JPEG file (not supported)
|
||||||
|
const minimalJPEG = Buffer.from([
|
||||||
|
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01,
|
||||||
|
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08,
|
||||||
|
0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0a,
|
||||||
|
0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d,
|
||||||
|
0x1a, 0x1f, 0x1e, 0x1d, 0x1a, 0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22,
|
||||||
|
0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34, 0x34, 0x34,
|
||||||
|
0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xc0,
|
||||||
|
0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4,
|
||||||
|
0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
|
||||||
|
0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01,
|
||||||
|
0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d,
|
||||||
|
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13,
|
||||||
|
0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42,
|
||||||
|
0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a,
|
||||||
|
0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35,
|
||||||
|
0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
|
||||||
|
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67,
|
||||||
|
0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84,
|
||||||
|
0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
|
||||||
|
0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,
|
||||||
|
0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
|
||||||
|
0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1,
|
||||||
|
0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4,
|
||||||
|
0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00,
|
||||||
|
0x00, 0x3f, 0x00, 0xfb, 0xd5, 0xdb, 0x20, 0xa8, 0xf1, 0x7c, 0xa5, 0x2f, 0xff,
|
||||||
|
0xd9,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create an oversized file (>200KB)
|
||||||
|
const oversizedFile = Buffer.alloc(250 * 1024, 0x00); // 250KB of zeros
|
||||||
|
|
||||||
|
const testPNGPath = path.join(testFaviconDir, 'test-favicon.png');
|
||||||
|
const testICOPath = path.join(testFaviconDir, 'test-favicon.ico');
|
||||||
|
const testJPEGPath = path.join(testFaviconDir, 'test-favicon.jpg');
|
||||||
|
const testOversizedPath = path.join(testFaviconDir, 'test-oversized.png');
|
||||||
|
|
||||||
|
// Write test files before tests run
|
||||||
|
beforeAll(() => {
|
||||||
|
fs.writeFileSync(testPNGPath, minimalPNG);
|
||||||
|
fs.writeFileSync(testICOPath, minimalICO);
|
||||||
|
fs.writeFileSync(testJPEGPath, minimalJPEG);
|
||||||
|
fs.writeFileSync(testOversizedPath, oversizedFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up test files after tests complete
|
||||||
|
afterAll(() => {
|
||||||
|
if (fs.existsSync(testPNGPath)) fs.unlinkSync(testPNGPath);
|
||||||
|
if (fs.existsSync(testICOPath)) fs.unlinkSync(testICOPath);
|
||||||
|
if (fs.existsSync(testJPEGPath)) fs.unlinkSync(testJPEGPath);
|
||||||
|
if (fs.existsSync(testOversizedPath)) fs.unlinkSync(testOversizedPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('upload PNG favicon successfully', async () => {
|
||||||
|
const res = await request
|
||||||
|
.post('/api/admin/config/favicon')
|
||||||
|
.auth('admin', defaultAdminPassword)
|
||||||
|
.attach('favicon', testPNGPath)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(res.body.success).toBe(true);
|
||||||
|
expect(res.body.message).toBe('favicon updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verify favicon.ico endpoint returns the uploaded favicon', async () => {
|
||||||
|
const res = await request.get('/favicon.ico').expect(200);
|
||||||
|
|
||||||
|
expect(res.headers['content-type']).toMatch(/image\/(png|x-icon)/);
|
||||||
|
expect(res.body).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('upload ICO favicon successfully', async () => {
|
||||||
|
const res = await request
|
||||||
|
.post('/api/admin/config/favicon')
|
||||||
|
.auth('admin', defaultAdminPassword)
|
||||||
|
.attach('favicon', testICOPath)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(res.body.success).toBe(true);
|
||||||
|
expect(res.body.message).toBe('favicon updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reject unsupported file type (JPEG)', async () => {
|
||||||
|
const res = await request
|
||||||
|
.post('/api/admin/config/favicon')
|
||||||
|
.auth('admin', defaultAdminPassword)
|
||||||
|
.attach('favicon', testJPEGPath)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
expect(res.body.message).toBe('favicon must be PNG or ICO format');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reject oversized file (>200KB)', async () => {
|
||||||
|
const res = await request
|
||||||
|
.post('/api/admin/config/favicon')
|
||||||
|
.auth('admin', defaultAdminPassword)
|
||||||
|
.attach('favicon', testOversizedPath)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
expect(res.body.message).toBe('file too large, max 200KB');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reject request without file', async () => {
|
||||||
|
const res = await request
|
||||||
|
.post('/api/admin/config/favicon')
|
||||||
|
.auth('admin', defaultAdminPassword)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
expect(res.body.message).toBe('no file provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reject unauthenticated request', async () => {
|
||||||
|
await request.post('/api/admin/config/favicon').expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reset favicon to default successfully', async () => {
|
||||||
|
// First upload a favicon to ensure we have one to reset
|
||||||
|
await request
|
||||||
|
.post('/api/admin/config/favicon')
|
||||||
|
.auth('admin', defaultAdminPassword)
|
||||||
|
.attach('favicon', testPNGPath)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
// Now reset to default
|
||||||
|
const res = await request
|
||||||
|
.delete('/api/admin/config/favicon')
|
||||||
|
.auth('admin', defaultAdminPassword)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(res.body.success).toBe(true);
|
||||||
|
expect(res.body.message).toBe('favicon reset to default');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verify favicon.ico returns default after reset', async () => {
|
||||||
|
const res = await request.get('/favicon.ico').expect(200);
|
||||||
|
|
||||||
|
expect(res.headers['content-type']).toMatch(/image\/(png|x-icon)/);
|
||||||
|
expect(res.body).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reject unauthenticated reset request', async () => {
|
||||||
|
await request.delete('/api/admin/config/favicon').expect(401);
|
||||||
|
});
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import { Button, Upload, Popconfirm } from 'antd';
|
||||||
|
import { RcFile } from 'antd/lib/upload/interface';
|
||||||
|
import React, { useState, useRef, FC } from 'react';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useTranslation } from 'next-export-i18n';
|
||||||
|
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||||
|
import { RESET_TIMEOUT } from '../../utils/config-constants';
|
||||||
|
import {
|
||||||
|
createInputStatus,
|
||||||
|
StatusState,
|
||||||
|
STATUS_ERROR,
|
||||||
|
STATUS_PROCESSING,
|
||||||
|
STATUS_SUCCESS,
|
||||||
|
} from '../../utils/input-statuses';
|
||||||
|
import { NEXT_PUBLIC_API_HOST } from '../../utils/apis';
|
||||||
|
import { Localization } from '../../types/localization';
|
||||||
|
import { Translation } from '../ui/Translation/Translation';
|
||||||
|
|
||||||
|
import { ACCEPTED_FAVICON_TYPES, MAX_FAVICON_FILESIZE, readableBytes } from '../../utils/images';
|
||||||
|
|
||||||
|
const LoadingOutlined = dynamic(() => import('@ant-design/icons/LoadingOutlined'), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const UploadOutlined = dynamic(() => import('@ant-design/icons/UploadOutlined'), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const UndoOutlined = dynamic(() => import('@ant-design/icons/UndoOutlined'), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ADMIN_USERNAME = process.env.NEXT_PUBLIC_ADMIN_USERNAME;
|
||||||
|
const ADMIN_STREAMKEY = process.env.NEXT_PUBLIC_ADMIN_STREAMKEY;
|
||||||
|
|
||||||
|
export const EditFavicon: FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [faviconCachebuster, setFaviconCacheBuster] = useState(0);
|
||||||
|
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||||
|
const pendingFile = useRef<RcFile | null>(null);
|
||||||
|
let resetTimer = null;
|
||||||
|
|
||||||
|
const resetStates = () => {
|
||||||
|
setSubmitStatus(null);
|
||||||
|
clearTimeout(resetTimer);
|
||||||
|
resetTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// validate file type and size
|
||||||
|
const beforeUpload = (file: RcFile) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
if (file.size > MAX_FAVICON_FILESIZE) {
|
||||||
|
const msg = t(Localization.Admin.StatusMessages.fileSizeTooBig, {
|
||||||
|
size: readableBytes(file.size),
|
||||||
|
});
|
||||||
|
setSubmitStatus(
|
||||||
|
createInputStatus(
|
||||||
|
STATUS_ERROR,
|
||||||
|
t(Localization.Admin.StatusMessages.thereWasAnError, { message: msg }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||||
|
setLoading(false);
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
return rej();
|
||||||
|
}
|
||||||
|
if (!ACCEPTED_FAVICON_TYPES.includes(file.type)) {
|
||||||
|
const msg = t(Localization.Admin.StatusMessages.fileTypeNotSupported, { type: file.type });
|
||||||
|
setSubmitStatus(
|
||||||
|
createInputStatus(
|
||||||
|
STATUS_ERROR,
|
||||||
|
t(Localization.Admin.StatusMessages.thereWasAnError, { message: msg }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||||
|
setLoading(false);
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
return rej();
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingFile.current = file;
|
||||||
|
setTimeout(() => res(), 100);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFaviconUpdate = async () => {
|
||||||
|
if (!pendingFile.current) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('favicon', pendingFile.current);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encoded = btoa(`${ADMIN_USERNAME}:${ADMIN_STREAMKEY}`);
|
||||||
|
const response = await fetch(`${NEXT_PUBLIC_API_HOST}api/admin/config/favicon`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encoded}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setSubmitStatus(createInputStatus(STATUS_SUCCESS));
|
||||||
|
setFaviconCacheBuster(Math.floor(Math.random() * 100));
|
||||||
|
} else {
|
||||||
|
setSubmitStatus(
|
||||||
|
createInputStatus(
|
||||||
|
STATUS_ERROR,
|
||||||
|
t(Localization.Admin.StatusMessages.thereWasAnError, { message: result.message }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitStatus(
|
||||||
|
createInputStatus(
|
||||||
|
STATUS_ERROR,
|
||||||
|
t(Localization.Admin.StatusMessages.thereWasAnError, { message: error.message }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingFile.current = null;
|
||||||
|
setLoading(false);
|
||||||
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetFavicon = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encoded = btoa(`${ADMIN_USERNAME}:${ADMIN_STREAMKEY}`);
|
||||||
|
const response = await fetch(`${NEXT_PUBLIC_API_HOST}api/admin/config/favicon`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encoded}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setSubmitStatus(createInputStatus(STATUS_SUCCESS));
|
||||||
|
setFaviconCacheBuster(Math.floor(Math.random() * 100));
|
||||||
|
} else {
|
||||||
|
setSubmitStatus(
|
||||||
|
createInputStatus(
|
||||||
|
STATUS_ERROR,
|
||||||
|
t(Localization.Admin.StatusMessages.thereWasAnError, { message: result.message }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitStatus(
|
||||||
|
createInputStatus(
|
||||||
|
STATUS_ERROR,
|
||||||
|
t(Localization.Admin.StatusMessages.thereWasAnError, { message: error.message }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||||
|
};
|
||||||
|
|
||||||
|
const faviconDisplayUrl = `${NEXT_PUBLIC_API_HOST}favicon.ico?random=${faviconCachebuster}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formfield-container logo-upload-container">
|
||||||
|
<div className="label-side">
|
||||||
|
<span className="formfield-label">
|
||||||
|
<Translation
|
||||||
|
translationKey={Localization.Admin.EditFavicon.label}
|
||||||
|
defaultText="Favicon"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-side">
|
||||||
|
<div className="input-group">
|
||||||
|
<img
|
||||||
|
src={faviconDisplayUrl}
|
||||||
|
alt="favicon"
|
||||||
|
style={{
|
||||||
|
width: '48px',
|
||||||
|
height: '48px',
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
marginRight: '10px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Upload
|
||||||
|
name="favicon"
|
||||||
|
listType="picture"
|
||||||
|
className="avatar-uploader"
|
||||||
|
showUploadList={false}
|
||||||
|
accept={ACCEPTED_FAVICON_TYPES.join(',')}
|
||||||
|
beforeUpload={beforeUpload}
|
||||||
|
customRequest={handleFaviconUpdate}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoadingOutlined style={{ color: 'white' }} />
|
||||||
|
) : (
|
||||||
|
<Button icon={<UploadOutlined />} />
|
||||||
|
)}
|
||||||
|
</Upload>
|
||||||
|
<Popconfirm
|
||||||
|
title={t(Localization.Admin.EditFavicon.resetConfirmTitle)}
|
||||||
|
onConfirm={handleResetFavicon}
|
||||||
|
okText={t(Localization.Admin.EditFavicon.resetConfirmOk)}
|
||||||
|
cancelText={t(Localization.Admin.EditFavicon.resetConfirmCancel)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Button icon={<UndoOutlined />} disabled={loading} style={{ marginLeft: '8px' }}>
|
||||||
|
<Translation
|
||||||
|
translationKey={Localization.Admin.EditFavicon.resetButton}
|
||||||
|
defaultText="Reset"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
<FormStatusIndicator status={submitStatus} />
|
||||||
|
<p className="field-tip">
|
||||||
|
<Translation
|
||||||
|
translationKey={Localization.Admin.EditFavicon.tip}
|
||||||
|
defaultText="Upload a custom favicon (PNG or ICO format, max 200KB). This icon appears in browser tabs and bookmarks."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -311,7 +311,7 @@ export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
|
|||||||
<Layout id="admin-page" className={appClass}>
|
<Layout id="admin-page" className={appClass}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Owncast Admin</title>
|
<title>Owncast Admin</title>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
{serverError?.type === 'OWNCAST_SERVICE_UNREACHABLE' && (
|
{serverError?.type === 'OWNCAST_SERVICE_UNREACHABLE' && (
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { UpdateArgs } from '../../../../types/config-section';
|
import { UpdateArgs } from '../../../../types/config-section';
|
||||||
import { ToggleSwitch } from '../../ToggleSwitch';
|
import { ToggleSwitch } from '../../ToggleSwitch';
|
||||||
import { EditLogo } from '../../EditLogo';
|
import { EditLogo } from '../../EditLogo';
|
||||||
|
import { EditFavicon } from '../../EditFavicon';
|
||||||
import FormStatusIndicator from '../../FormStatusIndicator';
|
import FormStatusIndicator from '../../FormStatusIndicator';
|
||||||
import { createInputStatus, STATUS_SUCCESS } from '../../../../utils/input-statuses';
|
import { createInputStatus, STATUS_SUCCESS } from '../../../../utils/input-statuses';
|
||||||
import { Translation } from '../../../ui/Translation/Translation';
|
import { Translation } from '../../../ui/Translation/Translation';
|
||||||
@@ -269,6 +270,8 @@ export default function EditInstanceDetails() {
|
|||||||
{/* Logo section */}
|
{/* Logo section */}
|
||||||
<EditLogo />
|
<EditLogo />
|
||||||
|
|
||||||
|
<EditFavicon />
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
fieldName="hideViewerCount"
|
fieldName="hideViewerCount"
|
||||||
useSubmit
|
useSubmit
|
||||||
|
|||||||
@@ -66,28 +66,9 @@ export const Main: FC = () => {
|
|||||||
<Head>
|
<Head>
|
||||||
{isProduction && <ServerRenderedHydration />}
|
{isProduction && <ServerRenderedHydration />}
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="/img/favicon/apple-icon-57x57.png" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="/img/favicon/apple-icon-60x60.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="/img/favicon/apple-icon-72x72.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/img/favicon/apple-icon-76x76.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="114x114" href="/img/favicon/apple-icon-114x114.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/img/favicon/apple-icon-120x120.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="144x144" href="/img/favicon/apple-icon-144x144.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/img/favicon/apple-icon-152x152.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-icon-180x180.png" />
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="192x192"
|
|
||||||
href="/img/favicon/android-icon-192x192.png"
|
|
||||||
/>
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon/favicon-96x96.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="authorization_endpoint" href="/api/auth/provider/indieauth" />
|
<link rel="authorization_endpoint" href="/api/auth/provider/indieauth" />
|
||||||
<meta name="msapplication-TileColor" content="#ffffff" />
|
|
||||||
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png" />
|
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -28,6 +28,14 @@
|
|||||||
"titlePlaceholder": "Your action title (required)",
|
"titlePlaceholder": "Your action title (required)",
|
||||||
"urlPlaceholder": "https://myserver.com/action (required)"
|
"urlPlaceholder": "https://myserver.com/action (required)"
|
||||||
},
|
},
|
||||||
|
"EditFavicon": {
|
||||||
|
"label": "Favicon",
|
||||||
|
"tip": "Upload a custom favicon (PNG or ICO format, max 200KB). This icon appears in browser tabs and bookmarks.",
|
||||||
|
"resetButton": "Reset",
|
||||||
|
"resetConfirmTitle": "Reset to default favicon?",
|
||||||
|
"resetConfirmOk": "Yes",
|
||||||
|
"resetConfirmCancel": "No"
|
||||||
|
},
|
||||||
"EditInstanceDetails": {
|
"EditInstanceDetails": {
|
||||||
"directoryDescription": "Increase your audience by appearing in the <a href=\"https://owncast.directory\" target=\"_blank\" rel=\"noreferrer\"><strong>Owncast Directory</strong></a>. This is an external service run by the Owncast project. <a href=\"https://owncast.online/docs/directory/?source=admin\" target=\"_blank\" rel=\"noopener noreferrer\">Learn more</a>.",
|
"directoryDescription": "Increase your audience by appearing in the <a href=\"https://owncast.directory\" target=\"_blank\" rel=\"noreferrer\"><strong>Owncast Directory</strong></a>. This is an external service run by the Owncast project. <a href=\"https://owncast.online/docs/directory/?source=admin\" target=\"_blank\" rel=\"noopener noreferrer\">Learn more</a>.",
|
||||||
"offlineMessageDescription": "The offline message is displayed to your page visitors when you're not streaming. Markdown is supported.",
|
"offlineMessageDescription": "The offline message is displayed to your page visitors when you're not streaming. Markdown is supported.",
|
||||||
@@ -205,6 +213,7 @@
|
|||||||
"overLimit": "Over limit",
|
"overLimit": "Over limit",
|
||||||
"placeholder": "Your chat display name"
|
"placeholder": "Your chat display name"
|
||||||
},
|
},
|
||||||
|
"chatDisabled": "<strong><em>Missing translation Frontend.chatDisabled: Please report</em></strong>",
|
||||||
"chatOffline": "Chat is offline",
|
"chatOffline": "Chat is offline",
|
||||||
"componentError": "Error: {{message}}",
|
"componentError": "Error: {{message}}",
|
||||||
"helloWorld": "Hello world",
|
"helloWorld": "Hello world",
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ async function rewrites() {
|
|||||||
source: '/customjavascript',
|
source: '/customjavascript',
|
||||||
destination: 'http://localhost:8080/customjavascript', // Proxy to Backend to work around CORS.
|
destination: 'http://localhost:8080/customjavascript', // Proxy to Backend to work around CORS.
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: '/favicon.ico',
|
||||||
|
destination: 'http://localhost:8080/favicon.ico', // Proxy to Backend to work around CORS.
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14348,15 +14348,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/devtools-protocol": {
|
|
||||||
"version": "0.0.1572739",
|
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1572739.tgz",
|
|
||||||
"integrity": "sha512-Dd0z5UXw7TJBRtlbCK/X0hePwrUm6/zxxVkwGTQU7kclkxgept8e4xdHYCDX1t59dcpRxpJNGim9ViUOjKfjNg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz",
|
||||||
@@ -28614,6 +28605,22 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/puppeteer-core/node_modules/devtools-protocol": {
|
||||||
|
"version": "0.0.1312386",
|
||||||
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
|
||||||
|
"integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/puppeteer/node_modules/devtools-protocol": {
|
||||||
|
"version": "0.0.1312386",
|
||||||
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
|
||||||
|
"integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/pure-rand": {
|
"node_modules/pure-rand": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
|
||||||
|
Before Width: | Height: | Size: 661 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -2,40 +2,8 @@
|
|||||||
"name": "Owncast",
|
"name": "Owncast",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "\/img\/favicon\/android-icon-36x36.png",
|
"src": "/favicon.ico",
|
||||||
"sizes": "36x36",
|
"sizes": "any"
|
||||||
"type": "image\/png",
|
|
||||||
"density": "0.75"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/img\/favicon\/android-icon-48x48.png",
|
|
||||||
"sizes": "48x48",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/img\/favicon\/android-icon-72x72.png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "1.5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/img\/favicon\/android-icon-96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "2.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/img\/favicon\/android-icon-144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "3.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/img\/favicon\/android-icon-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "4.0"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"display": "fullscreen"
|
"display": "fullscreen"
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ export const Localization = {
|
|||||||
serverUrlRequiredForDirectory: 'Admin.EditInstanceDetails.serverUrlRequiredForDirectory',
|
serverUrlRequiredForDirectory: 'Admin.EditInstanceDetails.serverUrlRequiredForDirectory',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// EditFavicon component specific keys
|
||||||
|
EditFavicon: {
|
||||||
|
label: 'Admin.EditFavicon.label',
|
||||||
|
tip: 'Admin.EditFavicon.tip',
|
||||||
|
resetButton: 'Admin.EditFavicon.resetButton',
|
||||||
|
resetConfirmTitle: 'Admin.EditFavicon.resetConfirmTitle',
|
||||||
|
resetConfirmOk: 'Admin.EditFavicon.resetConfirmOk',
|
||||||
|
resetConfirmCancel: 'Admin.EditFavicon.resetConfirmCancel',
|
||||||
|
},
|
||||||
|
|
||||||
// VideoVariantForm component specific keys
|
// VideoVariantForm component specific keys
|
||||||
VideoVariantForm: {
|
VideoVariantForm: {
|
||||||
bitrateDisabledPassthrough: 'Admin.VideoVariantForm.bitrateDisabledPassthrough',
|
bitrateDisabledPassthrough: 'Admin.VideoVariantForm.bitrateDisabledPassthrough',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const MAX_IMAGE_FILESIZE = 2097152;
|
export const MAX_IMAGE_FILESIZE = 2097152;
|
||||||
export const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
export const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
||||||
|
|
||||||
|
export const MAX_FAVICON_FILESIZE = 204800; // 200KB
|
||||||
|
export const ACCEPTED_FAVICON_TYPES = ['image/png', 'image/x-icon', 'image/vnd.microsoft.icon'];
|
||||||
|
|
||||||
export function getBase64(img: File | Blob, callback: (imageUrl: string | ArrayBuffer) => void) {
|
export function getBase64(img: File | Blob, callback: (imageUrl: string | ArrayBuffer) => void) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener('load', () => callback(reader.result));
|
reader.addEventListener('load', () => callback(reader.result));
|
||||||
@@ -10,7 +13,7 @@ export function getBase64(img: File | Blob, callback: (imageUrl: string | ArrayB
|
|||||||
export function readableBytes(bytes: number): string {
|
export function readableBytes(bytes: number): string {
|
||||||
const index = Math.floor(Math.log(bytes) / Math.log(1024));
|
const index = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
const SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
const SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
const size = Number((bytes / Math.pow(1024, index)).toFixed(2)) * 1;
|
const size = Number((bytes / 1024 ** index).toFixed(2)) * 1;
|
||||||
|
|
||||||
return `${size} ${SIZE_UNITS[index]}`;
|
return `${size} ${SIZE_UNITS[index]}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -282,6 +283,100 @@ func SetLogo(w http.ResponseWriter, r *http.Request) {
|
|||||||
webutils.WriteSimpleResponse(w, true, "changed")
|
webutils.WriteSimpleResponse(w, true, "changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFavicon will handle a new favicon image file being uploaded.
|
||||||
|
func SetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !requirePOST(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse multipart form with 200KB max
|
||||||
|
if err := r.ParseMultipartForm(200 << 10); err != nil {
|
||||||
|
webutils.WriteSimpleResponse(w, false, "no file provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, header, err := r.FormFile("favicon")
|
||||||
|
if err != nil {
|
||||||
|
webutils.WriteSimpleResponse(w, false, "no file provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Validate content type (PNG or ICO only)
|
||||||
|
contentType := header.Header.Get("Content-Type")
|
||||||
|
var extension string
|
||||||
|
switch contentType {
|
||||||
|
case "image/png":
|
||||||
|
extension = ".png"
|
||||||
|
case "image/x-icon", "image/vnd.microsoft.icon":
|
||||||
|
extension = ".ico"
|
||||||
|
default:
|
||||||
|
webutils.WriteSimpleResponse(w, false, "favicon must be PNG or ICO format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
bytes, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
webutils.WriteSimpleResponse(w, false, "unable to read file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce 200KB size limit
|
||||||
|
const maxFaviconSize = 200 * 1024 // 200KB
|
||||||
|
if len(bytes) > maxFaviconSize {
|
||||||
|
webutils.WriteSimpleResponse(w, false, "file too large, max 200KB")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imgPath := filepath.Join("data", "favicon"+extension)
|
||||||
|
if err := os.WriteFile(imgPath, bytes, 0o600); err != nil {
|
||||||
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if err := configRepository.SetFaviconPath("favicon" + extension); err != nil {
|
||||||
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
webutils.WriteSimpleResponse(w, true, "favicon updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetFavicon will reset the favicon to the default by removing the custom one.
|
||||||
|
func ResetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
// Get the current favicon path before clearing it
|
||||||
|
currentFavicon := configRepository.GetFaviconPath()
|
||||||
|
|
||||||
|
// Clear the favicon path in the database
|
||||||
|
if err := configRepository.SetFaviconPath(""); err != nil {
|
||||||
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the favicon file if it exists
|
||||||
|
if currentFavicon != "" {
|
||||||
|
faviconPath := filepath.Join("data", currentFavicon)
|
||||||
|
if err := os.Remove(faviconPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
log.Debugln("error removing favicon file:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try to remove any favicon files that might exist with different extensions
|
||||||
|
for _, ext := range []string{".ico", ".png"} {
|
||||||
|
faviconPath := filepath.Join("data", "favicon"+ext)
|
||||||
|
if err := os.Remove(faviconPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
log.Debugln("error removing favicon file:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webutils.WriteSimpleResponse(w, true, "favicon reset to default")
|
||||||
|
}
|
||||||
|
|
||||||
// SetNSFW will handle the web config request to set the NSFW flag.
|
// SetNSFW will handle the web config request to set the NSFW flag.
|
||||||
func SetNSFW(w http.ResponseWriter, r *http.Request) {
|
func SetNSFW(w http.ResponseWriter, r *http.Request) {
|
||||||
if !requirePOST(w, r) {
|
if !requirePOST(w, r) {
|
||||||
|
|||||||
@@ -175,6 +175,18 @@ func (*ServerInterfaceImpl) SetLogoOptions(w http.ResponseWriter, r *http.Reques
|
|||||||
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
|
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*ServerInterfaceImpl) SetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
middleware.RequireAdminAuth(admin.SetFavicon)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerInterfaceImpl) SetFaviconOptions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
middleware.RequireAdminAuth(admin.SetFavicon)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerInterfaceImpl) ResetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
middleware.RequireAdminAuth(admin.ResetFavicon)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
func (*ServerInterfaceImpl) SetTags(w http.ResponseWriter, r *http.Request) {
|
func (*ServerInterfaceImpl) SetTags(w http.ResponseWriter, r *http.Request) {
|
||||||
middleware.RequireAdminAuth(admin.SetTags)(w, r)
|
middleware.RequireAdminAuth(admin.SetTags)(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/config"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
"github.com/owncast/owncast/static"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetFavicon will return the favicon image as a response.
|
||||||
|
func GetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
faviconFilename := configRepository.GetFaviconPath()
|
||||||
|
if faviconFilename == "" {
|
||||||
|
returnDefaultFavicon(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
faviconPath := filepath.Join(config.DataDirectory, faviconFilename)
|
||||||
|
faviconBytes, err := os.ReadFile(faviconPath) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
returnDefaultFavicon(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := "image/x-icon"
|
||||||
|
if filepath.Ext(faviconFilename) == ".png" {
|
||||||
|
contentType = "image/png" //nolint:goconst
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheTime := utils.GetCacheDurationSecondsForPath(faviconPath)
|
||||||
|
writeFaviconResponse(faviconBytes, contentType, w, cacheTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func returnDefaultFavicon(w http.ResponseWriter) {
|
||||||
|
faviconBytes := static.GetFavicon()
|
||||||
|
cacheTime := utils.GetCacheDurationSecondsForPath("favicon.png")
|
||||||
|
writeFaviconResponse(faviconBytes, "image/png", w, cacheTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFaviconResponse(data []byte, contentType string, w http.ResponseWriter, cacheSeconds int) {
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheSeconds))
|
||||||
|
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
log.Println("unable to write favicon.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/oapi-codegen/runtime"
|
"github.com/oapi-codegen/runtime"
|
||||||
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -569,26 +570,29 @@ type Viewer struct {
|
|||||||
|
|
||||||
// WebConfig defines model for WebConfig.
|
// WebConfig defines model for WebConfig.
|
||||||
type WebConfig struct {
|
type WebConfig struct {
|
||||||
AppearanceVariables *map[string]string `json:"appearanceVariables,omitempty"`
|
AppearanceVariables *map[string]string `json:"appearanceVariables,omitempty"`
|
||||||
Authentication *AuthenticationConfig `json:"authentication,omitempty"`
|
Authentication *AuthenticationConfig `json:"authentication,omitempty"`
|
||||||
ChatDisabled *bool `json:"chatDisabled,omitempty"`
|
ChatDisabled *bool `json:"chatDisabled,omitempty"`
|
||||||
CustomStyles *string `json:"customStyles,omitempty"`
|
|
||||||
ExternalActions *[]ExternalAction `json:"externalActions,omitempty"`
|
// ChatRequireAuthentication Whether users must authenticate before sending chat messages
|
||||||
ExtraPageContent *string `json:"extraPageContent,omitempty"`
|
ChatRequireAuthentication *bool `json:"chatRequireAuthentication,omitempty"`
|
||||||
Federation *FederationConfig `json:"federation,omitempty"`
|
CustomStyles *string `json:"customStyles,omitempty"`
|
||||||
HideViewerCount *bool `json:"hideViewerCount,omitempty"`
|
ExternalActions *[]ExternalAction `json:"externalActions,omitempty"`
|
||||||
Logo *string `json:"logo,omitempty"`
|
ExtraPageContent *string `json:"extraPageContent,omitempty"`
|
||||||
MaxSocketPayloadSize *int `json:"maxSocketPayloadSize,omitempty"`
|
Federation *FederationConfig `json:"federation,omitempty"`
|
||||||
Name *string `json:"name,omitempty"`
|
HideViewerCount *bool `json:"hideViewerCount,omitempty"`
|
||||||
Notifications *NotificationConfig `json:"notifications,omitempty"`
|
Logo *string `json:"logo,omitempty"`
|
||||||
Nsfw *bool `json:"nsfw,omitempty"`
|
MaxSocketPayloadSize *int `json:"maxSocketPayloadSize,omitempty"`
|
||||||
OfflineMessage *string `json:"offlineMessage,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
SocialHandles *[]SocialHandle `json:"socialHandles,omitempty"`
|
Notifications *NotificationConfig `json:"notifications,omitempty"`
|
||||||
SocketHostOverride *string `json:"socketHostOverride,omitempty"`
|
Nsfw *bool `json:"nsfw,omitempty"`
|
||||||
StreamTitle *string `json:"streamTitle,omitempty"`
|
OfflineMessage *string `json:"offlineMessage,omitempty"`
|
||||||
Summary *string `json:"summary,omitempty"`
|
SocialHandles *[]SocialHandle `json:"socialHandles,omitempty"`
|
||||||
Tags *[]string `json:"tags,omitempty"`
|
SocketHostOverride *string `json:"socketHostOverride,omitempty"`
|
||||||
Version *string `json:"version,omitempty"`
|
StreamTitle *string `json:"streamTitle,omitempty"`
|
||||||
|
Summary *string `json:"summary,omitempty"`
|
||||||
|
Tags *[]string `json:"tags,omitempty"`
|
||||||
|
Version *string `json:"version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webhook defines model for Webhook.
|
// Webhook defines model for Webhook.
|
||||||
@@ -695,6 +699,12 @@ type SetExternalActionsJSONBody struct {
|
|||||||
Value *[]ExternalAction `json:"value,omitempty"`
|
Value *[]ExternalAction `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFaviconMultipartBody defines parameters for SetFavicon.
|
||||||
|
type SetFaviconMultipartBody struct {
|
||||||
|
// Favicon Favicon file (PNG or ICO, max 200KB)
|
||||||
|
Favicon *openapi_types.File `json:"favicon,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// SetBrowserNotificationConfigurationJSONBody defines parameters for SetBrowserNotificationConfiguration.
|
// SetBrowserNotificationConfigurationJSONBody defines parameters for SetBrowserNotificationConfiguration.
|
||||||
type SetBrowserNotificationConfigurationJSONBody struct {
|
type SetBrowserNotificationConfigurationJSONBody struct {
|
||||||
Value *BrowserNotificationConfiguration `json:"value,omitempty"`
|
Value *BrowserNotificationConfiguration `json:"value,omitempty"`
|
||||||
@@ -945,6 +955,9 @@ type SetDisableSearchIndexingJSONRequestBody = AdminConfigValue
|
|||||||
// SetExternalActionsJSONRequestBody defines body for SetExternalActions for application/json ContentType.
|
// SetExternalActionsJSONRequestBody defines body for SetExternalActions for application/json ContentType.
|
||||||
type SetExternalActionsJSONRequestBody SetExternalActionsJSONBody
|
type SetExternalActionsJSONRequestBody SetExternalActionsJSONBody
|
||||||
|
|
||||||
|
// SetFaviconMultipartRequestBody defines body for SetFavicon for multipart/form-data ContentType.
|
||||||
|
type SetFaviconMultipartRequestBody SetFaviconMultipartBody
|
||||||
|
|
||||||
// SetFederationBlockDomainsJSONRequestBody defines body for SetFederationBlockDomains for application/json ContentType.
|
// SetFederationBlockDomainsJSONRequestBody defines body for SetFederationBlockDomains for application/json ContentType.
|
||||||
type SetFederationBlockDomainsJSONRequestBody = AdminConfigValue
|
type SetFederationBlockDomainsJSONRequestBody = AdminConfigValue
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,15 @@ type ServerInterface interface {
|
|||||||
// Update external action links
|
// Update external action links
|
||||||
// (POST /admin/config/externalactions)
|
// (POST /admin/config/externalactions)
|
||||||
SetExternalActions(w http.ResponseWriter, r *http.Request)
|
SetExternalActions(w http.ResponseWriter, r *http.Request)
|
||||||
|
// Reset favicon to default
|
||||||
|
// (DELETE /admin/config/favicon)
|
||||||
|
ResetFavicon(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
|
// (OPTIONS /admin/config/favicon)
|
||||||
|
SetFaviconOptions(w http.ResponseWriter, r *http.Request)
|
||||||
|
// Upload custom favicon
|
||||||
|
// (POST /admin/config/favicon)
|
||||||
|
SetFavicon(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
// (OPTIONS /admin/config/federation/blockdomains)
|
// (OPTIONS /admin/config/federation/blockdomains)
|
||||||
SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request)
|
SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request)
|
||||||
@@ -970,6 +979,23 @@ func (_ Unimplemented) SetExternalActions(w http.ResponseWriter, r *http.Request
|
|||||||
w.WriteHeader(http.StatusNotImplemented)
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset favicon to default
|
||||||
|
// (DELETE /admin/config/favicon)
|
||||||
|
func (_ Unimplemented) ResetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// (OPTIONS /admin/config/favicon)
|
||||||
|
func (_ Unimplemented) SetFaviconOptions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload custom favicon
|
||||||
|
// (POST /admin/config/favicon)
|
||||||
|
func (_ Unimplemented) SetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
// (OPTIONS /admin/config/federation/blockdomains)
|
// (OPTIONS /admin/config/federation/blockdomains)
|
||||||
func (_ Unimplemented) SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request) {
|
func (_ Unimplemented) SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
@@ -2758,6 +2784,57 @@ func (siw *ServerInterfaceWrapper) SetExternalActions(w http.ResponseWriter, r *
|
|||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetFavicon operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) ResetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, BasicAuthScopes, []string{})
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.ResetFavicon(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFaviconOptions operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) SetFaviconOptions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.SetFaviconOptions(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFavicon operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) SetFavicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, BasicAuthScopes, []string{})
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.SetFavicon(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
// SetFederationBlockDomainsOptions operation middleware
|
// SetFederationBlockDomainsOptions operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request) {
|
func (siw *ServerInterfaceWrapper) SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request) {
|
||||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -5852,6 +5929,15 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
|
|||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Post(options.BaseURL+"/admin/config/externalactions", wrapper.SetExternalActions)
|
r.Post(options.BaseURL+"/admin/config/externalactions", wrapper.SetExternalActions)
|
||||||
})
|
})
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Delete(options.BaseURL+"/admin/config/favicon", wrapper.ResetFavicon)
|
||||||
|
})
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Options(options.BaseURL+"/admin/config/favicon", wrapper.SetFaviconOptions)
|
||||||
|
})
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Post(options.BaseURL+"/admin/config/favicon", wrapper.SetFavicon)
|
||||||
|
})
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Options(options.BaseURL+"/admin/config/federation/blockdomains", wrapper.SetFederationBlockDomainsOptions)
|
r.Options(options.BaseURL+"/admin/config/federation/blockdomains", wrapper.SetFederationBlockDomainsOptions)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func GetLogo(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else if filepath.Ext(imageFilename) == ".gif" {
|
} else if filepath.Ext(imageFilename) == ".gif" {
|
||||||
contentType = "image/gif"
|
contentType = "image/gif"
|
||||||
} else if filepath.Ext(imageFilename) == ".png" {
|
} else if filepath.Ext(imageFilename) == ".png" {
|
||||||
contentType = "image/png"
|
contentType = "image/png" //nolint:goconst
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc {
|
|||||||
w.Header().Set("Access-Control-Allow-Origin", validAdminHost)
|
w.Header().Set("Access-Control-Allow-Origin", validAdminHost)
|
||||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
|
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
|
||||||
// For request needing CORS, send a 204.
|
// For request needing CORS, send a 204.
|
||||||
if r.Method == "OPTIONS" {
|
if r.Method == "OPTIONS" {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ func addStaticFileEndpoints(r chi.Router) {
|
|||||||
r.HandleFunc("/thumbnail.jpg", handlers.GetThumbnail)
|
r.HandleFunc("/thumbnail.jpg", handlers.GetThumbnail)
|
||||||
r.HandleFunc("/preview.gif", handlers.GetPreview)
|
r.HandleFunc("/preview.gif", handlers.GetPreview)
|
||||||
r.HandleFunc("/logo", handlers.GetLogo)
|
r.HandleFunc("/logo", handlers.GetLogo)
|
||||||
|
r.HandleFunc("/favicon.ico", handlers.GetFavicon)
|
||||||
// return a logo that's compatible with external social networks
|
// return a logo that's compatible with external social networks
|
||||||
r.HandleFunc("/logo/external", handlers.GetCompatibleLogo)
|
r.HandleFunc("/logo/external", handlers.GetCompatibleLogo)
|
||||||
|
|
||||||
|
|||||||