Skip to content

Commit

Permalink
local-phone-storage - cloudflare turnstile
Browse files Browse the repository at this point in the history
  • Loading branch information
dcordz committed Jun 19, 2024
1 parent 0c851b4 commit 6d192bc
Show file tree
Hide file tree
Showing 16 changed files with 162 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ tf/plans
litestream/config/*
litestream/dbs/*


wireguard.conf

!**/**/.keep
6 changes: 4 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,16 @@ def redirect_component(page, props = {})
def route_component(route)
T.unsafe(self).route_home if route.nil?

phone = session[:verified_phone]

u = current_user
if u.nil?
render json: { route: ROUTES[:HOME] }
elsif !u.is_registration_complete
render json: { route: ROUTES[:REGISTRATION] }
render json: { route: ROUTES[:REGISTRATION], phone: }
else
Rails.logger.info "ServerRendering.route - Route to page - #{route}"
render json: { route: }
render json: { route:, phone: }
end
end

Expand Down
1 change: 1 addition & 0 deletions app/controllers/users/webauthn/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def callback
stored_passkey.update!(sign_count: verified_webauthn_passkey.sign_count)
sign_in(user)

session[:verified_phone] = user.phone
if user.is_registration_complete
T.unsafe(self).route_legislators
else
Expand Down
28 changes: 0 additions & 28 deletions app/frontend/components/SwaySvg.tsx

This file was deleted.

5 changes: 3 additions & 2 deletions app/frontend/components/bill/BillSummaryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ButtonUnstyled from "app/frontend/components/ButtonUnstyled";
import SuspenseFullScreen from "app/frontend/components/dialogs/SuspenseFullScreen";
import OrganizationIcon from "app/frontend/components/organizations/OrganizationIcon";
import BillSummaryMarkdown from "./BillSummaryMarkdown";
import { IS_MOBILE_PHONE } from "app/frontend/sway_constants";
const DialogWrapper = lazy(() => import("../dialogs/DialogWrapper"));

interface IProps {
Expand Down Expand Up @@ -65,8 +66,8 @@ const BillSummaryModal: React.FC<IProps> = ({
<SuspenseFullScreen>
<DialogWrapper
open={true}
size="xl"
fullscreen
size={IS_MOBILE_PHONE ? "xl" : "lg"}
fullscreen={IS_MOBILE_PHONE || undefined}
setOpen={() => setSelectedOrganization(undefined)}
style={{ margin: 0 }}
>
Expand Down
3 changes: 2 additions & 1 deletion app/frontend/components/dialogs/FileUploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ const FileUploadModal: React.FC<IProps> = ({ fileName, currentFilePath, accept,
if (!file) return;

const cacheBust = new Date().valueOf();
const name = `${fileName}-${cacheBust}.${file.name.split(".").last()}`;

createPresignedFileUpload({ name: `${fileName}-${cacheBust}`, mime_type: file.type })
createPresignedFileUpload({ name, mime_type: file.type })
.then((fileUpload) => {
if (!fileUpload) return;

Expand Down
50 changes: 38 additions & 12 deletions app/frontend/components/organizations/OrganizationIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SWAY_ASSETS_BUCKET_BASE_URL } from "app/frontend/sway_constants/google_cloud_storage";
import { useCallback, useState } from "react";

import { useCallback, useMemo, useState } from "react";
import { Image } from "react-bootstrap";
import { sway } from "sway";

Expand All @@ -11,21 +12,46 @@ interface IProps {
const DEFAULT_ICON_PATH = "/images/sway-us-light.png";

const OrganizationIcon: React.FC<IProps> = ({ organization, maxWidth }) => {
const [icon, setIcon] = useState<string>(organization?.iconPath || DEFAULT_ICON_PATH);
const [isError, setError] = useState<boolean>(false);

const handleError = useCallback(() => setIcon(DEFAULT_ICON_PATH), []);
const icon = useMemo(
() => (organization?.iconPath ? organization.iconPath : DEFAULT_ICON_PATH),
[organization?.iconPath],
);
const src = useMemo(
() =>
icon === DEFAULT_ICON_PATH
? DEFAULT_ICON_PATH
: `${SWAY_ASSETS_BUCKET_BASE_URL}${(icon.startsWith("/") ? icon : "/" + icon).replace("//", "/")}`,
[icon],
);

if (icon === DEFAULT_ICON_PATH) {
return <Image src={icon} />;
const handleError = useCallback(() => {
setError(true);
}, []);

if (isError) {
return (
<div className="col">
<Image src={DEFAULT_ICON_PATH} alt="" style={{ maxWidth: maxWidth || 300 }} className="m-auto" />
<div>{organization?.name}</div>
</div>
);
}

return (
<Image
alt={organization?.name}
style={{ maxWidth: maxWidth || 300 }}
src={`${SWAY_ASSETS_BUCKET_BASE_URL}/${icon}`}
className="m-auto"
onError={handleError}
/>
<div className="col">
<Image
alt={""}
src={src}
style={{ maxWidth: maxWidth || 300 }}
className="m-auto"
onError={handleError}
fetchPriority="high"
decoding="sync"
/>
<div>{organization?.name}</div>
</div>
);
};

Expand Down
15 changes: 11 additions & 4 deletions app/frontend/hooks/useAxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { sway } from "sway";
import { DEFAULT_ERROR_MESSAGE, handleError, logDev, notify } from "../sway_utils";
import { isFailedRequest } from "../sway_utils/http";
import { useCancellable } from "./useCancellable";
import { removeNonDigits } from "app/frontend/sway_utils/phone";

type TPayload = Record<number, any> | Record<string, any> | FormData;

Expand All @@ -18,6 +19,7 @@ type TBodyRequest = (

interface IRoutableResponse extends Record<string, any> {
route?: string;
phone?: string;
}

/*
Expand Down Expand Up @@ -49,6 +51,11 @@ const handleAxiosError = (ex: AxiosError | Error) => {
}
};

const handleRoutedResponse = (result: IRoutableResponse) => {
result.phone && localStorage.setItem("@sway/phone", removeNonDigits(result.phone));
result.route && router.visit(result.route);
};

/*
*
* SECURE/SESSION METHODS
Expand Down Expand Up @@ -113,7 +120,7 @@ export const useAxiosGet = <T extends IRoutableResponse>(
// message: (result as sway.IValidationResult)?.message || DEFAULT_ERROR_MESSAGE,
});
} else if ("route" in result && result.route) {
return router.visit(result.route);
return handleRoutedResponse(result);
} else if (options?.defaultValue) {
setItems(options.defaultValue);
}
Expand Down Expand Up @@ -181,7 +188,7 @@ export const useAxiosPost = <T extends IRoutableResponse>(
if (!result) {
return null;
} else if ("route" in result && result.route) {
router.visit(result.route);
handleRoutedResponse(result);
return null;
} else if (isFailedRequest(result)) {
if (options?.notifyOnValidationResultFailure) {
Expand Down Expand Up @@ -376,7 +383,7 @@ export const useAxios_NOT_Authenticated_GET = <T extends IRoutableResponse>(
if (!result) {
return null;
} else if ("route" in result && result.route) {
router.visit(result.route);
handleRoutedResponse(result);
return null;
} else if (isFailedRequest(result)) {
if (options?.notifyOnValidationResultFailure) {
Expand Down Expand Up @@ -448,7 +455,7 @@ export const useAxios_NOT_Authenticated_POST_PUT = <T extends IRoutableResponse>
if (!result) {
return null;
} else if ("route" in result && result.route) {
router.visit(result.route);
handleRoutedResponse(result);
return null;
} else if (isFailedRequest(result)) {
if (notifyOnValidationResultFailure) {
Expand Down
62 changes: 57 additions & 5 deletions app/frontend/pages/Passkey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useDispatch } from "react-redux";
import { sway } from "sway";

import { setUser } from "app/frontend/redux/actions/userActions";
import { logDev, notify } from "app/frontend/sway_utils";
import { handleError, logDev, notify } from "app/frontend/sway_utils";
import { PHONE_INPUT_TRANSFORMER, isValidPhoneNumber } from "app/frontend/sway_utils/phone";
import { ErrorMessage, Field, FieldAttributes, Form, Formik, FormikProps } from "formik";
import { Form as BootstrapForm, Button } from "react-bootstrap";
Expand All @@ -14,6 +14,7 @@ import { useSendPhoneVerification } from "app/frontend/hooks/authentication/phon
import { useWebAuthnAuthentication } from "app/frontend/hooks/authentication/useWebAuthnAuthentication";
import { AxiosError } from "axios";
import { Animate } from "react-simple-animate";
import Turnstile, { useTurnstile } from "react-turnstile";
import * as yup from "yup";

interface ISigninValues {
Expand Down Expand Up @@ -41,6 +42,33 @@ const Passkey: React.FC = () => {

const dispatch = useDispatch();

const turnstile = useTurnstile();
const [turnstileVerified, setTurnstileVerified] = useState<boolean>(false);
const handleTurnstileVerify = useCallback(
(token: string) => {
fetch("https://turnstile.sway.vote", {
method: "POST",
body: JSON.stringify({ token }),
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((j) => {
setTurnstileVerified(j.success);
if (!j.success) {
setTurnstileVerified(false);
handleError(new Error(j.message));
}
})
.catch((e) => {
console.error(e);
turnstile.reset();
});
},
[turnstile],
);

const onAuthenticated = useCallback(
(user: sway.IUser) => {
if (!user) return;
Expand Down Expand Up @@ -75,10 +103,12 @@ const Passkey: React.FC = () => {

const handleSubmit = useCallback(
async ({ phone, code }: { phone: string; code?: string }) => {
if (!turnstileVerified) return;

if (code && isConfirmingPhone) {
confirmPhoneVerification(phone, code);
} else {
startAuthentication(phone)
return startAuthentication(phone)
.then((publicKey) => {
if (typeof publicKey === "boolean") {
if (!publicKey) {
Expand Down Expand Up @@ -106,7 +136,14 @@ const Passkey: React.FC = () => {
});
}
},
[confirmPhoneVerification, isConfirmingPhone, sendPhoneVerification, startAuthentication, verifyAuthentication],
[
turnstileVerified,
isConfirmingPhone,
confirmPhoneVerification,
startAuthentication,
verifyAuthentication,
sendPhoneVerification,
],
);

const handleCancel = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
Expand Down Expand Up @@ -145,8 +182,18 @@ const Passkey: React.FC = () => {
</Field>
</BootstrapForm.Group>
<ErrorMessage name={"phone"} className="bold white" />

<div className="my-2">
<Turnstile
appearance="always"
theme="light"
action="passkey-phone"
// sitekey={"3x00000000000000000000FF"}
sitekey={import.meta.env.VITE_CLOUDFLARE_TURNSTILE_SITE_KEY}
onVerify={handleTurnstileVerify}
/>
</div>
</div>
<div className="col-lg-4 col-1">&nbsp;</div>
</div>
<Animate
play={isConfirmingPhone}
Expand Down Expand Up @@ -193,7 +240,12 @@ const Passkey: React.FC = () => {
</Animate>
</div>
<div className="col">
<Button className="w-100" variant="primary" type="submit" disabled={isLoading}>
<Button
className="w-100"
variant="primary"
type="submit"
disabled={isLoading || !turnstileVerified}
>
Submit
</Button>
</div>
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# typed: strict

Rails.application.routes.draw do

default_url_options protocol: :https

# ServerRendering
Expand Down
2 changes: 1 addition & 1 deletion fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ console_command = '/rails/bin/rails console'

[[mounts]]
source = 'prodswaysqlite'
destination = '/storage'
destination = '/rails/storage'

[http_service]
internal_port = 3000
Expand Down
1 change: 1 addition & 0 deletions lib/sway_google_cloud_storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# https://cloud.google.com/storage/docs/access-control/signing-urls-with-helpers#client-libraries
# private writes, public reads
# https://cloud.google.com/storage/docs/access-control/making-data-public

module SwayGoogleCloudStorage
extend ActiveSupport::Concern

Expand Down
Loading

0 comments on commit 6d192bc

Please sign in to comment.