Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ If Fireshare is useful to you, [GitHub Sponsors](https://github.com/sponsors/Sha
- [Notifications to Discord and others](./docs/Notifications.md)
- RSS feed for new public videos
- [LDAP support](./docs/LDAP.md)
- [Two-factor authentication (TOTP authenticator apps)](./docs/Security.md#two-factor-authentication-mfa)
- [Login IP whitelisting](./docs/Security.md#login-ip-whitelist)

## Supported Video Formats

Expand Down Expand Up @@ -192,6 +194,11 @@ Use the lite image by appending `-lite` to your tag:

See [LDAP.md](./docs/LDAP.md) for setup instructions.

### Security (IP Whitelist & Two-Factor Authentication)

Fireshare can restrict logins to a whitelist of IP addresses/CIDR ranges and supports TOTP two-factor
authentication with any authenticator app. See [Security.md](./docs/Security.md) for setup instructions.

### Transcoding (Optional)

When enabled, Fireshare will create lower quality versions of your original supported file type videos. Your viewers can then choose to play your videos at lower qualities that their internet can handle. Fireshare will also attempt to automatically downgrade the quality of a viewer who is constantly buffering.
Expand Down
4 changes: 2 additions & 2 deletions app/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fireshare",
"version": "1.7.6",
"version": "1.7.7",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
Expand Down Expand Up @@ -39,4 +39,4 @@
"build": "vite build",
"preview": "vite preview"
}
}
}
2 changes: 1 addition & 1 deletion app/client/src/components/cards/CompactImageCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ const CompactImageCard = ({
<Typography sx={{ fontWeight: 600, fontSize: 14, color: 'white', fontFamily: 'monospace' }}>
{viewCount}
</Typography>
<VisibilityIcon sx={{ fontSize: 18, color: 'white' }} />
<VisibilityIcon sx={{ fontSize: 18, color: privateView ? '#FF6B6B' : 'white' }} />
</Box>
</Box>

Expand Down
4 changes: 2 additions & 2 deletions app/client/src/components/cards/CompactVideoCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const CompactVideoCard = ({

const cardRef = React.useRef(null)
const isTouchDevice = React.useRef(
typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),
typeof window !== 'undefined' && window.matchMedia('(hover: none) and (pointer: coarse)').matches
)

React.useEffect(() => {
Expand Down Expand Up @@ -605,7 +605,7 @@ const CompactVideoCard = ({
<Typography sx={{ fontWeight: 600, fontSize: 14, color: 'white', fontFamily: 'monospace' }}>
{viewCount}
</Typography>
<VisibilityIcon sx={{ fontSize: 18, color: 'white' }} />
<VisibilityIcon sx={{ fontSize: 18, color: privateView ? '#FF6B6B' : 'white' }} />
</Box>
</Box>

Expand Down
2 changes: 1 addition & 1 deletion app/client/src/components/cards/MasonryImageCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ const MasonryImageCard = ({
<Typography sx={{ fontWeight: 600, fontSize: 14, color: 'white', fontFamily: 'monospace' }}>
{viewCount}
</Typography>
<VisibilityIcon sx={{ fontSize: 20, color: 'white' }} />
<VisibilityIcon sx={{ fontSize: 20, color: privateView ? '#FF6B6B' : 'white' }} />
</Box>

{/* Visibility toggle - shows on hover */}
Expand Down
201 changes: 146 additions & 55 deletions app/client/src/components/forms/LoginForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,109 @@ const inputSx = {
},
}

const submitButtonSx = {
mt: 1,
py: 1.25,
borderRadius: '10px',
fontSize: 15,
fontWeight: 600,
textTransform: 'none',
bgcolor: '#2684FF',
'&:hover': { bgcolor: '#1a6fd4' },
'&.Mui-disabled': { bgcolor: 'rgba(38, 132, 255, 0.2)', color: 'rgba(255,255,255,0.3)' },
}

const LoginForm = function () {
const demoMode = getSetting('demo_mode')
const [username, setUsername] = React.useState('')
const [password, setPassword] = React.useState('')
const [step, setStep] = React.useState('credentials')
const [code, setCode] = React.useState('')
const [loading, setLoading] = React.useState(false)
const [alert, setAlert] = React.useState({ open: false })
const navigate = useNavigate()

async function completeLogin() {
const config = (await ConfigService.getConfig()).data
setSetting('demo_mode', config.demo_mode || false)
setSetting('is_demo_user', config.is_demo_user || false)
navigate('/')
}

function errorMessage(err) {
const data = err.response?.data
if (typeof data === 'string' && data) return data
if (data?.error) return data.error
return 'An unknown error occurred while trying to log in.'
}

async function login() {
if (!username || !password) {
setAlert({ type: 'error', message: 'Username and password are required.', open: true })
return
}
setLoading(true)
try {
await AuthService.login(username, password)
const config = (await ConfigService.getConfig()).data
setSetting('demo_mode', config.demo_mode || false)
setSetting('is_demo_user', config.is_demo_user || false)
navigate('/')
const res = await AuthService.login(username, password)
if (res.data?.mfa_required) {
setStep('mfa')
setCode('')
setLoading(false)
return
}
await completeLogin()
} catch (err) {
const status = err.response?.status
setAlert({
type: status === 401 ? 'warning' : 'error',
message:
status === 401 ? err.response.data : 'An unknown error occurred while trying to log in.',
status === 401 || status === 403 ? errorMessage(err) : 'An unknown error occurred while trying to log in.',
open: true,
})
setLoading(false)
}
}

async function verifyCode() {
if (!code) return
setLoading(true)
try {
await AuthService.loginMfa(code)
await completeLogin()
} catch (err) {
if (err.response?.data?.restart) {
setStep('credentials')
setPassword('')
setCode('')
} else {
setCode('')
}
setAlert({ type: 'warning', message: errorMessage(err), open: true })
setLoading(false)
}
}

function backToSignIn() {
setStep('credentials')
setPassword('')
setCode('')
setAlert({})
}

const handleKeyDown = (e) => {
if (e.key === 'Enter' && username && password) {
e.preventDefault()
login()
}
}

const handleCodeKeyDown = (e) => {
if (e.key === 'Enter' && code) {
e.preventDefault()
verifyCode()
}
}

return (
<Box
sx={{
Expand Down Expand Up @@ -89,14 +153,14 @@ const LoginForm = function () {
Fireshare
</Typography>
<Typography sx={{ fontSize: 13, color: 'rgba(194, 224, 255, 0.5)', fontWeight: 400 }}>
Sign in to your account
{step === 'mfa' ? 'Two-factor authentication' : 'Sign in to your account'}
</Typography>
</Box>

<Divider sx={{ borderColor: 'rgba(194, 224, 255, 0.08)', mb: 3 }} />

{/* Demo mode callout */}
{demoMode && (
{step === 'credentials' && demoMode && (
<Box
sx={{
display: 'flex',
Expand Down Expand Up @@ -129,53 +193,80 @@ const LoginForm = function () {
</Box>
)}

{/* Fields */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
fullWidth
id="username"
label="Username"
variant="outlined"
autoFocus
autoComplete="username"
value={username}
onChange={(e) => { setAlert({}); setUsername(e.target.value) }}
onKeyDown={handleKeyDown}
sx={inputSx}
/>
<TextField
fullWidth
id="password"
label="Password"
type="password"
variant="outlined"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={handleKeyDown}
sx={inputSx}
/>
<Button
variant="contained"
size="large"
fullWidth
disabled={!username || !password || loading}
onClick={login}
sx={{
mt: 1,
py: 1.25,
borderRadius: '10px',
fontSize: 15,
fontWeight: 600,
textTransform: 'none',
bgcolor: '#2684FF',
'&:hover': { bgcolor: '#1a6fd4' },
'&.Mui-disabled': { bgcolor: 'rgba(38, 132, 255, 0.2)', color: 'rgba(255,255,255,0.3)' },
}}
>
{loading ? 'Signing in…' : 'Sign in'}
</Button>
</Box>
{step === 'credentials' ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
fullWidth
id="username"
label="Username"
variant="outlined"
autoFocus
autoComplete="username"
value={username}
onChange={(e) => { setAlert({}); setUsername(e.target.value) }}
onKeyDown={handleKeyDown}
sx={inputSx}
/>
<TextField
fullWidth
id="password"
label="Password"
type="password"
variant="outlined"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={handleKeyDown}
sx={inputSx}
/>
<Button
variant="contained"
size="large"
fullWidth
disabled={!username || !password || loading}
onClick={login}
sx={submitButtonSx}
>
{loading ? 'Signing in…' : 'Sign in'}
</Button>
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography sx={{ fontSize: 13, color: 'rgba(194, 224, 255, 0.5)', textAlign: 'center' }}>
Enter the 6-digit code from your authenticator app.
</Typography>
<TextField
fullWidth
id="mfa-code"
label="Authentication code"
variant="outlined"
autoFocus
autoComplete="one-time-code"
inputProps={{ inputMode: 'numeric', maxLength: 6, style: { letterSpacing: '0.3em', textAlign: 'center' } }}
value={code}
onChange={(e) => { setAlert({}); setCode(e.target.value.replace(/\D/g, '')) }}
onKeyDown={handleCodeKeyDown}
sx={inputSx}
/>
<Button
variant="contained"
size="large"
fullWidth
disabled={!code || loading}
onClick={verifyCode}
sx={submitButtonSx}
>
{loading ? 'Verifying…' : 'Verify'}
</Button>
<Button
fullWidth
onClick={backToSignIn}
sx={{ textTransform: 'none', color: 'rgba(194, 224, 255, 0.5)', fontSize: 13 }}
>
Back to sign in
</Button>
</Box>
)}
</Box>
)
}
Expand Down
Loading
Loading