Merge remote-tracking branch 'origin/main' into face_small_improvements

This commit is contained in:
laurenspriem 2024-05-24 09:37:53 +05:30
commit a74943698f
7 changed files with 136 additions and 119 deletions

View file

@ -203,7 +203,7 @@ class SearchWidgetState extends State<SearchWidget> {
String query,
) {
int resultCount = 0;
final maxResultCount = _isYearValid(query) ? 13 : 12;
final maxResultCount = _isYearValid(query) ? 12 : 11;
final streamController = StreamController<List<SearchResult>>();
if (query.isEmpty) {
@ -260,10 +260,11 @@ class SearchWidgetState extends State<SearchWidget> {
onResultsReceived(locationResult);
},
);
_searchService.getAllFace(null).then(
(locationResult) {
(faceResult) {
final List<GenericSearchResult> filteredResults = [];
for (final result in locationResult) {
for (final result in faceResult) {
if (result.name().toLowerCase().contains(query.toLowerCase())) {
filteredResults.add(result);
}

View file

@ -6,6 +6,7 @@
"@/next": "*",
"@ente/accounts": "*",
"@ente/eslint-config": "*",
"@ente/shared": "*"
"@ente/shared": "*",
"otpauth": "^9"
}
}

View file

@ -15,10 +15,9 @@ import MoreHoriz from "@mui/icons-material/MoreHoriz";
import { Button, ButtonBase, Snackbar, TextField } from "@mui/material";
import { t } from "i18next";
import { useRouter } from "next/router";
import { HOTP, TOTP } from "otpauth";
import { AppContext } from "pages/_app";
import React, { useContext, useEffect, useState } from "react";
import { Code } from "services/code";
import { generateOTPs, type Code } from "services/code";
import { getAuthCodes } from "services/remote";
const AuthenticatorCodesPage = () => {
@ -43,7 +42,7 @@ const AuthenticatorCodesPage = () => {
}
setHasFetched(true);
};
fetchCodes();
void fetchCodes();
appContext.showNavBar(false);
}, []);
@ -122,12 +121,12 @@ const AuthenticatorCodesPage = () => {
</div>
) : (
filteredCodes.map((code) => (
<CodeDisplay codeInfo={code} key={code.id} />
<CodeDisplay key={code.id} code={code} />
))
)}
</div>
<div style={{ marginBottom: "2rem" }} />
<AuthFooter />
<Footer />
<div style={{ marginBottom: "4rem" }} />
</div>
</>
@ -163,97 +162,67 @@ const AuthNavbar: React.FC = () => {
};
interface CodeDisplay {
codeInfo: Code;
code: Code;
}
const CodeDisplay: React.FC<CodeDisplay> = ({ codeInfo }) => {
const CodeDisplay: React.FC<CodeDisplay> = ({ code }) => {
const [otp, setOTP] = useState("");
const [nextOTP, setNextOTP] = useState("");
const [codeErr, setCodeErr] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [hasCopied, setHasCopied] = useState(false);
const generateCodes = () => {
const regen = () => {
try {
const currentTime = new Date().getTime();
if (codeInfo.type === "totp") {
const totp = new TOTP({
secret: codeInfo.secret,
algorithm: codeInfo.algorithm,
period: codeInfo.period,
digits: codeInfo.digits,
});
setOTP(totp.generate());
setNextOTP(
totp.generate({
timestamp: currentTime + codeInfo.period * 1000,
}),
);
} else if (codeInfo.type === "hotp") {
const hotp = new HOTP({
secret: codeInfo.secret,
counter: 0,
algorithm: codeInfo.algorithm,
});
setOTP(hotp.generate());
setNextOTP(hotp.generate({ counter: 1 }));
}
} catch (err) {
setCodeErr(err.message);
const [m, n] = generateOTPs(code);
setOTP(m);
setNextOTP(n);
} catch (e) {
setErrorMessage(e instanceof Error ? e.message : String(e));
}
};
const copyCode = () => {
navigator.clipboard.writeText(otp);
setHasCopied(true);
setTimeout(() => {
setHasCopied(false);
}, 2000);
setTimeout(() => setHasCopied(false), 2000);
};
useEffect(() => {
// this is to set the initial code and nextCode on component mount
generateCodes();
const codeType = codeInfo.type;
const codePeriodInMs = codeInfo.period * 1000;
// Generate to set the initial otp and nextOTP on component mount.
regen();
const codeType = code.type;
const codePeriodInMs = code.period * 1000;
const timeToNextCode =
codePeriodInMs - (new Date().getTime() % codePeriodInMs);
const intervalId = null;
// wait until we are at the start of the next code period,
// and then start the interval loop
const interval = null;
// Wait until we are at the start of the next code period, and then
// start the interval loop.
setTimeout(() => {
// we need to call generateCodes() once before the interval loop
// to set the initial code and nextCode
generateCodes();
// We need to call regen() once before the interval loop to set the
// initial otp and nextOTP.
regen();
codeType.toLowerCase() === "totp" ||
codeType.toLowerCase() === "hotp"
? setInterval(() => {
generateCodes();
regen();
}, codePeriodInMs)
: null;
}, timeToNextCode);
return () => {
if (intervalId) clearInterval(intervalId);
if (interval) clearInterval(interval);
};
}, [codeInfo]);
}, [code]);
return (
<div style={{ padding: "8px" }}>
{codeErr === "" ? (
<ButtonBase
component="div"
onClick={() => {
copyCode();
}}
>
<OTPDisplay code={codeInfo} otp={otp} nextOTP={nextOTP} />
<Snackbar
open={hasCopied}
message="Code copied to clipboard"
/>
</ButtonBase>
{errorMessage ? (
<UnparseableCode {...{ code, errorMessage }} />
) : (
<BadCodeInfo codeInfo={codeInfo} codeErr={codeErr} />
<ButtonBase component="div" onClick={copyCode}>
<OTPDisplay {...{ code, otp, nextOTP }} />
<Snackbar open={hasCopied} message={t("COPIED")} />
</ButtonBase>
)}
</div>
);
@ -376,19 +345,15 @@ interface TimerProgressProps {
const TimerProgress: React.FC<TimerProgressProps> = ({ period }) => {
const [progress, setProgress] = useState(0);
const microSecondsInPeriod = period * 1000000;
const us = period * 1e6;
useEffect(() => {
const updateTimeRemaining = () => {
const timeRemaining =
microSecondsInPeriod -
((new Date().getTime() * 1000) % microSecondsInPeriod);
setProgress(timeRemaining / microSecondsInPeriod);
const advance = () => {
const timeRemaining = us - ((new Date().getTime() * 1000) % us);
setProgress(timeRemaining / us);
};
const ticker = setInterval(() => {
updateTimeRemaining();
}, 10);
const ticker = setInterval(advance, 10);
return () => clearInterval(ticker);
}, []);
@ -407,17 +372,25 @@ const TimerProgress: React.FC<TimerProgressProps> = ({ period }) => {
);
};
function BadCodeInfo({ codeInfo, codeErr }) {
interface UnparseableCodeProps {
code: Code;
errorMessage: string;
}
const UnparseableCode: React.FC<UnparseableCodeProps> = ({
code,
errorMessage,
}) => {
const [showRawData, setShowRawData] = useState(false);
return (
<div className="code-info">
<div>{codeInfo.title}</div>
<div>{codeErr}</div>
<div>{code.issuer}</div>
<div>{errorMessage}</div>
<div>
{showRawData ? (
<div onClick={() => setShowRawData(false)}>
{codeInfo.uriString ?? "(no raw data)"}
{code.uriString}
</div>
) : (
<div onClick={() => setShowRawData(true)}>Show rawData</div>
@ -425,9 +398,9 @@ function BadCodeInfo({ codeInfo, codeErr }) {
</div>
</div>
);
}
};
const AuthFooter: React.FC = () => {
const Footer: React.FC = () => {
return (
<div
style={{

View file

@ -1,12 +1,13 @@
import { HOTP, TOTP } from "otpauth";
import { URI } from "vscode-uri";
/**
* A parsed representation of an xOTP code URI.
* A parsed representation of an *OTP code URI.
*
* This is all the data we need to drive a OTP generator.
*/
export interface Code {
/** The uniquue id for the corresponding auth entity. */
/** A unique id for the corresponding "auth entity" in our system. */
id?: String;
/** The type of the code. */
type: "totp" | "hotp";
@ -14,16 +15,21 @@ export interface Code {
account: string;
/** The name of the entity that issued this code. */
issuer: string;
/** Number of digits in the code. */
/** Number of digits in the generated OTP. */
digits: number;
/**
* The time period (in seconds) for which a single OTP generated from this
* code remains valid.
*/
period: number;
/** The secret that is used to drive the OTP generator. */
/**
* The secret that is used to drive the OTP generator.
*
* This is an arbitrary key encoded in Base32 that drives the HMAC (in a
* {@link type}-specific manner).
*/
secret: string;
/** The (hashing) algorithim used by the OTP generator. */
/** The (HMAC) algorithm used by the OTP generator. */
algorithm: "sha1" | "sha256" | "sha512";
/** The original string from which this code was generated. */
uriString?: string;
@ -38,22 +44,15 @@ export interface Code {
* code. These strings are of the form:
*
* - (TOTP)
* otpauth://totp/account:user@example.org?algorithm=SHA1&digits=6&issuer=issuer&period=30&secret=ALPHANUM
* otpauth://totp/ACME:user@example.org?algorithm=SHA1&digits=6&issuer=acme&period=30&secret=ALPHANUM
*/
export const codeFromURIString = (id: string, uriString: string): Code => {
let santizedRawData = uriString
.replace(/\+/g, "%2B")
.replace(/:/g, "%3A")
.replaceAll("\r", "");
if (santizedRawData.startsWith('"')) {
santizedRawData = santizedRawData.substring(1);
}
if (santizedRawData.endsWith('"')) {
santizedRawData = santizedRawData.substring(
0,
santizedRawData.length - 1,
);
}
const santizedRawData = uriString
.replaceAll("+", "%2B")
.replaceAll(":", "%3A")
.replaceAll("\r", "")
// trim quotes
.replace(/^"|"$/g, "");
const uriParams = {};
const searchParamsString =
@ -78,12 +77,22 @@ export const codeFromURIString = (id: string, uriString: string): Code => {
issuer: _getIssuer(uriPath, uriParams),
digits: parseDigits(uriParams),
period: parsePeriod(uriParams),
secret: getSanitizedSecret(uriParams),
secret: parseSecret(uriParams),
algorithm: parseAlgorithm(uriParams),
uriString,
};
};
const _getType = (uriPath: string): Code["type"] => {
const oauthType = uriPath.split("/")[0].substring(0);
if (oauthType.toLowerCase() === "totp") {
return "totp";
} else if (oauthType.toLowerCase() === "hotp") {
return "hotp";
}
throw new Error(`Unsupported format with host ${oauthType}`);
};
const _getAccount = (uriPath: string): string => {
try {
const path = decodeURIComponent(uriPath);
@ -139,16 +148,45 @@ const parseAlgorithm = (uriParams): Code["algorithm"] => {
}
};
const _getType = (uriPath: string): Code["type"] => {
const oauthType = uriPath.split("/")[0].substring(0);
if (oauthType.toLowerCase() === "totp") {
return "totp";
} else if (oauthType.toLowerCase() === "hotp") {
return "hotp";
}
throw new Error(`Unsupported format with host ${oauthType}`);
};
const parseSecret = (uriParams): string =>
uriParams["secret"].replaceAll(" ", "").toUpperCase();
const getSanitizedSecret = (uriParams): string => {
return uriParams["secret"].replace(/ /g, "").toUpperCase();
/**
* Generate a pair of OTPs (one time passwords) from the given {@link code}.
*
* @param code The parsed code data, including the secret and code type.
*
* @returns a pair of OTPs, the current one and the next one, using the given
* {@link code}.
*/
export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
let otp: string;
let nextOTP: string;
switch (code.type) {
case "totp": {
const totp = new TOTP({
secret: code.secret,
algorithm: code.algorithm,
period: code.period,
digits: code.digits,
});
otp = totp.generate();
nextOTP = totp.generate({
timestamp: new Date().getTime() + code.period * 1000,
});
break;
}
case "hotp": {
const hotp = new HOTP({
secret: code.secret,
counter: 0,
algorithm: code.algorithm,
});
otp = hotp.generate();
nextOTP = hotp.generate({ counter: 1 });
break;
}
}
return [otp, nextOTP];
};

View file

@ -28,7 +28,6 @@
"localforage": "^1.9.0",
"memoize-one": "^6.0.0",
"ml-matrix": "^6.11",
"otpauth": "^9.0.2",
"p-debounce": "^4.0.0",
"p-queue": "^7.1.0",
"photoswipe": "file:./thirdparty/photoswipe",

View file

@ -193,3 +193,8 @@ some cases.
- [hdbscan](https://github.com/shaileshpandit/hdbscan-js) is used for face
clustering.
## Auth app specific
- [otpauth](https://github.com/hectorm/otpauth) is used for the generation of
the actual OTP from the user's TOTP/HOTP secret.

View file

@ -3700,10 +3700,10 @@ optionator@^0.9.3:
prelude-ls "^1.2.1"
type-check "^0.4.0"
otpauth@^9.0.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/otpauth/-/otpauth-9.2.2.tgz#64bda9ea501a5d86e69a964a45062f1f17f740f4"
integrity sha512-2VcnYRUmq1dNckIfySNYP32ITWp1bvTeAEW0BSCR6G3GBf3a5zb9E+ubY62t3Dma9RjoHlvd7QpmzHfJZRkiNg==
otpauth@^9:
version "9.2.4"
resolved "https://registry.yarnpkg.com/otpauth/-/otpauth-9.2.4.tgz#3b7941a689f63c31db43ab2494d3c2d90bc1f150"
integrity sha512-t0Nioq2Up2ZaT5AbpXZLTjrsNtLc/g/rVSaEThmKLErAuT9mrnAKJryiPOKc3rCH+3ycWBgKpRHYn+DHqfaPiQ==
dependencies:
jssha "~3.3.1"