mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge branch 'main' into fix/android-theme
This commit is contained in:
commit
771a0f090f
12 changed files with 103 additions and 76 deletions
|
|
@ -6,3 +6,5 @@
|
|||
**/.dockerignore
|
||||
**/*.yaml
|
||||
.secrets*.yaml
|
||||
apple
|
||||
android
|
||||
|
|
|
|||
|
|
@ -85,12 +85,11 @@ This will start postgres, initialize the database, and start the web and api ser
|
|||
|
||||
Open <http://localhost:3000> and confirm Omnivore is running
|
||||
|
||||
### 3. Create a test account
|
||||
### 3. Login with the test account
|
||||
|
||||
Omnivore uses social login, but for testing there is an email + password
|
||||
option.
|
||||
During database setup docker-compose creates an account `demo@omnivore.app`, password: `demo`.
|
||||
|
||||
Go to <http://localhost:3000/auth/email-signup> in your browser.
|
||||
Go to <http://localhost:3000/> in your browser and choose `Continue with Email` to login.
|
||||
|
||||
### Frontend Development
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export const saveUrlResolver = authorized<
|
|||
return { errorCodes: [SaveErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
return (await saveUrl(ctx, user, input)) as SaveSuccess
|
||||
return (await saveUrl({ ...ctx, uid }, user, input)) as SaveSuccess
|
||||
})
|
||||
|
||||
export const saveFileResolver = authorized<
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ import {
|
|||
import { createWebAuthToken } from './jwt_helpers'
|
||||
import { createSsoToken, ssoRedirectURL } from '../../utils/sso'
|
||||
|
||||
export interface SignupRequest {
|
||||
email: string
|
||||
password: string
|
||||
name: string
|
||||
username: string
|
||||
bio?: string
|
||||
pictureUrl?: string
|
||||
}
|
||||
|
||||
const logger = buildLogger('app.dispatch')
|
||||
const signToken = promisify(jwt.sign)
|
||||
|
||||
|
|
@ -62,6 +71,19 @@ const cookieParams = {
|
|||
maxAge: 365 * 24 * 60 * 60 * 1000,
|
||||
}
|
||||
|
||||
export const isValidSignupRequest = (obj: any): obj is SignupRequest => {
|
||||
return (
|
||||
'email' in obj &&
|
||||
obj.email.trim().length > 0 && // email must not be empty
|
||||
'password' in obj &&
|
||||
obj.password.length >= 8 && // password must be at least 8 characters
|
||||
'name' in obj &&
|
||||
obj.name.trim().length > 0 && // name must not be empty
|
||||
'username' in obj &&
|
||||
obj.username.trim().length > 0 // username must not be empty
|
||||
)
|
||||
}
|
||||
|
||||
export function authRouter() {
|
||||
const router = express.Router()
|
||||
|
||||
|
|
@ -443,26 +465,6 @@ export function authRouter() {
|
|||
'/email-signup',
|
||||
cors<express.Request>(corsConfig),
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
interface SignupRequest {
|
||||
email: string
|
||||
password: string
|
||||
name: string
|
||||
username: string
|
||||
bio?: string
|
||||
pictureUrl?: string
|
||||
}
|
||||
function isValidSignupRequest(obj: any): obj is SignupRequest {
|
||||
return (
|
||||
'email' in obj &&
|
||||
obj.email.trim().length > 0 && // email must not be empty
|
||||
'password' in obj &&
|
||||
obj.password.length >= 8 && // password must be at least 8 characters
|
||||
'name' in obj &&
|
||||
obj.name.trim().length > 0 && // name must not be empty
|
||||
'username' in obj &&
|
||||
obj.username.trim().length > 0 // username must not be empty
|
||||
)
|
||||
}
|
||||
if (!isValidSignupRequest(req.body)) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/auth/email-signup?errorCodes=INVALID_CREDENTIALS`
|
||||
|
|
|
|||
|
|
@ -32,13 +32,8 @@ export function mobileAuthRouter() {
|
|||
})
|
||||
|
||||
router.post('/email-sign-up', async (req, res) => {
|
||||
const { email, password, username, name } = req.body
|
||||
const payload = await createMobileEmailSignUpResponse(
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
name
|
||||
)
|
||||
const payload = await createMobileEmailSignUpResponse(req.body)
|
||||
|
||||
res.status(payload.statusCode).json(payload.json)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import UserModel from '../../../datalayer/user'
|
||||
import { StatusType } from '../../../datalayer/user/model'
|
||||
import { getUserByEmail } from '../../../services/create_user'
|
||||
import { sendConfirmationEmail } from '../../../services/send_emails'
|
||||
import { comparePassword } from '../../../utils/auth'
|
||||
import { decodeAppleToken } from '../apple_auth'
|
||||
import { decodeGoogleToken } from '../google_auth'
|
||||
import {
|
||||
AuthProvider,
|
||||
DecodeTokenResult,
|
||||
JsonResponsePayload,
|
||||
AuthProvider,
|
||||
} from '../auth_types'
|
||||
import { decodeGoogleToken } from '../google_auth'
|
||||
import { createMobileAuthPayload } from '../jwt_helpers'
|
||||
import UserModel from '../../../datalayer/user'
|
||||
import { initModels } from '../../../server'
|
||||
import { sendConfirmationEmail } from '../../../services/send_emails'
|
||||
import { kx } from '../../../datalayer/knex_config'
|
||||
import { StatusType } from '../../../datalayer/user/model'
|
||||
import { comparePassword } from '../../../utils/auth'
|
||||
|
||||
export async function createMobileSignInResponse(
|
||||
isAndroid: boolean,
|
||||
|
|
@ -46,11 +45,7 @@ export async function createMobileEmailSignInResponse(
|
|||
throw new Error('Missing username or password')
|
||||
}
|
||||
|
||||
const models = initModels(kx, false)
|
||||
const user = await models.user.getWhere({
|
||||
email,
|
||||
})
|
||||
|
||||
const user = await getUserByEmail(email.trim())
|
||||
if (!user?.id || !user?.password) {
|
||||
throw new Error('user not found')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { createPendingUserToken, suggestedUsername } from '../jwt_helpers'
|
|||
import UserModel from '../../../datalayer/user'
|
||||
import { hashPassword } from '../../../utils/auth'
|
||||
import { createUser } from '../../../services/create_user'
|
||||
import { isValidSignupRequest } from '../auth_router'
|
||||
|
||||
export async function createMobileSignUpResponse(
|
||||
isAndroid: boolean,
|
||||
|
|
@ -45,24 +46,24 @@ export async function createMobileSignUpResponse(
|
|||
}
|
||||
|
||||
export async function createMobileEmailSignUpResponse(
|
||||
email?: string,
|
||||
password?: string,
|
||||
username?: string,
|
||||
name?: string
|
||||
requestBody: any
|
||||
): Promise<JsonResponsePayload> {
|
||||
try {
|
||||
if (!email || !password || !username || !name) {
|
||||
if (!isValidSignupRequest(requestBody)) {
|
||||
throw new Error('Missing username, password, name, or username')
|
||||
}
|
||||
const { email, password, name, username } = requestBody
|
||||
|
||||
// trim whitespace in email address
|
||||
const trimmedEmail = email.trim()
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
await createUser({
|
||||
email,
|
||||
email: trimmedEmail,
|
||||
provider: 'EMAIL',
|
||||
sourceUserId: email,
|
||||
name,
|
||||
username: username.toLowerCase(),
|
||||
sourceUserId: trimmedEmail,
|
||||
name: name.trim(),
|
||||
username: username.trim().toLowerCase(),
|
||||
password: hashedPassword,
|
||||
pendingConfirmation: true,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ export const createUser = async (input: {
|
|||
password?: string
|
||||
pendingConfirmation?: boolean
|
||||
}): Promise<[User, Profile]> => {
|
||||
const existingUser = await getUserByEmail(input.email)
|
||||
const trimmedEmail = input.email.trim()
|
||||
const existingUser = await getUserByEmail(trimmedEmail)
|
||||
if (existingUser) {
|
||||
if (existingUser.profile) {
|
||||
return Promise.reject({ errorCode: SignupErrorCode.UserExists })
|
||||
|
|
@ -63,7 +64,7 @@ export const createUser = async (input: {
|
|||
const user = await t.getRepository(User).save({
|
||||
source: input.provider,
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
email: trimmedEmail,
|
||||
sourceUserId: input.sourceUserId,
|
||||
password: input.password,
|
||||
status: input.pendingConfirmation
|
||||
|
|
|
|||
|
|
@ -159,6 +159,44 @@ export function Article(props: ArticleProps): JSX.Element {
|
|||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Get all images with initial sizes, if they are small
|
||||
// make sure they get displayed small
|
||||
const sizedImages = Array.from(
|
||||
document.querySelectorAll('img[data-omnivore-width]')
|
||||
)
|
||||
|
||||
sizedImages.forEach((element) => {
|
||||
const img = element as HTMLImageElement
|
||||
const width = Number(img.getAttribute('data-omnivore-width'))
|
||||
const height = Number(img.getAttribute('data-omnivore-height'))
|
||||
console.log('width and height: ', width, height)
|
||||
|
||||
if (!isNaN(width) && !isNaN(height) && width < 100 && height < 100) {
|
||||
img.style.setProperty('width', `${width}px`)
|
||||
img.style.setProperty('height', `${height}px`)
|
||||
img.style.setProperty('max-width', 'unset')
|
||||
}
|
||||
})
|
||||
|
||||
const fallbackImages = Array.from(
|
||||
document.querySelectorAll('img[data-omnivore-original-src]')
|
||||
)
|
||||
|
||||
fallbackImages.forEach((element) => {
|
||||
const img = element as HTMLImageElement
|
||||
const fallbackSrc = img.getAttribute('data-omnivore-original-src')
|
||||
if (fallbackSrc) {
|
||||
img.onerror = () => {
|
||||
console.log('image falling back to original: ', fallbackSrc)
|
||||
// If the image fails to load fallback to the original
|
||||
img.onerror = null
|
||||
img.src = fallbackSrc
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [props.content])
|
||||
|
||||
return (
|
||||
<>
|
||||
<link
|
||||
|
|
|
|||
|
|
@ -66,8 +66,16 @@ export function LandingFooter(): JSX.Element {
|
|||
</FooterList>
|
||||
</VStack>
|
||||
<VStack>
|
||||
<StyledText style="aboutFooter">Get Help</StyledText>
|
||||
<StyledText style="aboutFooter">About</StyledText>
|
||||
<FooterList>
|
||||
<li>
|
||||
<a href="https://docs.omnivore.app/about/pricing">Pricing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.omnivore.app/about/privacy-statement">
|
||||
Privacy
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="mailto:feedback@omnivore.app">Contact us via email</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -96,6 +96,11 @@ const moduleExports = {
|
|||
destination: '/.well-known/security.txt',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/privacy',
|
||||
destination: 'https://docs.omnivore.app/about/privacy-policy',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/install/chrome',
|
||||
destination:
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { PrivacyPolicy } from '../components/templates/PrivacyPolicy'
|
||||
import { SettingsLayout } from '../components/templates/SettingsLayout'
|
||||
|
||||
export default function Privacy(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const appEmbedViewQuery = router.query.isAppEmbedView as string | undefined
|
||||
const isAppEmbedView = (appEmbedViewQuery ?? '').length > 0
|
||||
|
||||
if (isAppEmbedView) {
|
||||
return <PrivacyPolicy />
|
||||
} else {
|
||||
return (
|
||||
<SettingsLayout title="Privacy Policy">
|
||||
<PrivacyPolicy />
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue