Changes to login flow and API

This commit is contained in:
Fergal Moran
2023-02-27 09:19:48 +00:00
parent c1996356e1
commit 0b7b09088c
10 changed files with 159 additions and 242 deletions

View File

@@ -9,4 +9,4 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cache outstanding events - name: Cache outstanding events
run: curl -X GET https://external.fergl.ie/api/cron/cache run: curl -X GET https://otherway.fergl.ie/api/cron/cache

View File

@@ -9,4 +9,4 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cache outstanding events - name: Cache outstanding events
run: curl -X GET https://external.fergl.ie/api/cron/reminders run: curl -X GET https://otherway.fergl.ie/api/cron/reminders

View File

@@ -8,9 +8,10 @@ const LoginPage = () => {
const { signInWithGoogle, signInWithFacebook, signInWithTwitter, profile, signIn } = const { signInWithGoogle, signInWithFacebook, signInWithTwitter, profile, signIn } =
useFirebaseAuth(); useFirebaseAuth();
const router = useRouter(); const router = useRouter();
const [error, setError] = React.useState("");
const [email, setEmail] = React.useState(""); const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState(""); const [password, setPassword] = React.useState("");
const [forgot, setForgot] = React.useState(false);
const login = async ( const login = async (
event: React.SyntheticEvent<HTMLButtonElement> event: React.SyntheticEvent<HTMLButtonElement>
@@ -19,144 +20,118 @@ const LoginPage = () => {
}; };
return ( return (
<div className="max-w-lg p-10 rounded-md shadow-md font-body bg-base-100 text-base-content md:flex-1"> <div className="max-w-lg p-10 rounded-md shadow-md font-body bg-base-100 text-base-content md:flex-1">
<> <h3 className="my-4 text-2xl font-semibold font-title">
<h3 className="my-4 text-2xl font-semibold font-title"> Account Login
Account Login </h3>
</h3> <form action="#" className="flex flex-col space-y-5">
<form action="#" className="flex flex-col space-y-5"> <div className="flex flex-col space-y-1">
<div className="flex flex-col space-y-1"> <label htmlFor="email" className="text-sm">
<label htmlFor="email" className="text-sm"> Email address
Email address </label>
<input
type="email"
id="email"
autoFocus
className="input-bordered input-primary input input-sm"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</div>
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-between">
<label htmlFor="password" className="text-sm">
Password
</label> </label>
<input
type="email"
id="email"
autoFocus
className="input-bordered input-primary input input-sm"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</div>
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-between">
<label htmlFor="password" className="text-sm">
Password
</label>
<button
type="button"
onClick={() => {
setForgot(true);
}}
className="text-sm text-blue-600 hover:underline focus:text-blue-800"
>
Forgot Password?
</button>
</div>
<input
type="password"
id="password"
className="input-bordered input-primary input input-sm"
value={password}
onChange={(event) => {
setPassword(event.target.value);
}}
/>
</div>
<div>
<button <button
className="w-full btn-primary btn" type="button"
onClick={(event) => { onClick={() => {
void login(event); setForgot(true);
}} }}
className="text-sm text-blue-600 hover:underline focus:text-blue-800"
> >
Log in Forgot Password?
</button> </button>
</div> </div>
<div className="flex flex-col space-y-5"> <input
<span className="flex items-center justify-center space-x-2"> type="password"
<span className="h-px bg-gray-400 w-14" /> id="password"
<span className="font-normal text-gray-500">or login with</span> className="input-bordered input-primary input input-sm"
<span className="h-px bg-gray-400 w-14" /> value={password}
</span> onChange={(event) => {
<div className="flex items-center justify-center gap-4"> setPassword(event.target.value);
<button }}
type="button" />
className="w-1/3 gap-2 btn" </div>
onClick={signInWithTwitter} <div>
> <button
<div className="text-base-content"> className="w-full btn-primary btn"
<IoLogoTwitter /> onClick={(event) => {
</div> void login(event);
<span className="text-sm font-medium text-base-content"> }}
Twitter >
</span> Log in
</button> </button>
<button </div>
type="button" <div className="flex flex-col space-y-5">
className="w-1/3 gap-2 btn" <span className="flex items-center justify-center space-x-2">
onClick={signInWithGoogle} <span className="h-px bg-gray-400 w-14" />
> <span className="font-normal text-gray-500">or login with</span>
<div className="text-base-content"> <span className="h-px bg-gray-400 w-14" />
<IoLogoGoogle /> </span>
</div> <div className="flex items-center justify-center gap-4">
<span className="text-sm font-medium text-base-content"> <button
Gmail type="button"
</span> className="w-1/3 gap-2 btn"
</button> onClick={signInWithTwitter}
<button >
type="button" <div className="text-base-content">
className="w-1/3 gap-2 btn" <IoLogoTwitter />
onClick={signInWithFacebook} </div>
> <span className="text-sm font-medium text-base-content">
<div className="text-base-content"> Twitter
<IoLogoFacebook /> </span>
</div> </button>
<span className="text-sm font-medium text-base-content"> <button
Facebook type="button"
</span> className="w-1/3 gap-2 btn"
</button> onClick={signInWithGoogle}
</div> >
{/* <div className="flex flex-row space-x-1"> <div className="text-base-content">
<button <IoLogoGoogle />
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md group border-base-200 hover:bg-base-300 focus:outline-none" </div>
onClick={signInWithTwitter} <span className="text-sm font-medium text-base-content">
> Gmail
<div className="text-base-content"> </span>
<IoLogoTwitter /> </button>
</div> <button
<span className="text-sm font-medium text-base-content"> type="button"
Twitter className="w-1/3 gap-2 btn"
</span> onClick={signInWithFacebook}
</button> >
<button <div className="text-base-content">
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md group border-base-200 hover:bg-base-300 focus:outline-none" <IoLogoFacebook />
onClick={signInWithGoogle} </div>
> <span className="text-sm font-medium text-base-content">
<div className="text-base-content"> Facebook
<IoLogoGoogle /> </span>
</div> </button>
<span className="text-sm font-medium text-base-content">
Gmail
</span>
</button>
<button
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md group border-base-200 hover:bg-base-300 focus:outline-none"
onClick={signInWithFacebook}
>
<div className="text-base-content">
<IoLogoFacebook />
</div>
<span className="text-sm font-medium text-base-content">
Facebook
</span>
</button>
</div> */}
</div> </div>
</form> {error && (
</> <div className="shadow-lg alert alert-error">
<div>
<svg xmlns="http://www.w3.org/2000/svg" className="flex-shrink-0 w-6 h-6 stroke-current" fill="none"
viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{error}</span>
</div>
</div>
)}
</div>
</form>
</div> </div>
); );
}; };

View File

@@ -1,5 +1,3 @@
import "./Boilerplate.css";
const TosComponent = () => { const TosComponent = () => {
// @ts-ignore // @ts-ignore
return ( return (
@@ -15,26 +13,26 @@ const TosComponent = () => {
<h2>2. Use License</h2>; <h2>2. Use License</h2>;
<p>Permission is granted to temporarily download one copy of the materials on Radio Otherway's Website for personal, <p>Permission is granted to temporarily download one copy of the materials on Radio Otherway&apos;s Website for personal,
non-commercial transitory viewing only. This is the grant of a license, not a transfer of title, and under this non-commercial transitory viewing only. This is the grant of a license, not a transfer of title, and under this
license you may not:</p>; license you may not:</p>;
<ul> <ul>
<li>modify or copy the materials;</li> <li>modify or copy the materials;</li>
<li>use the materials for any commercial purpose or for any public display;</li> <li>use the materials for any commercial purpose or for any public display;</li>
<li>attempt to reverse engineer any software contained on Radio Otherway's Website;</li> <li>attempt to reverse engineer any software contained on Radio Otherway&apos;s Website;</li>
<li>remove any copyright or other proprietary notations from the materials; or</li> <li>remove any copyright or other proprietary notations from the materials; or</li>
<li>transferring the materials to another person or "mirror" the materials on any other server.</li> <li>transferring the materials to another person or `&ldquo;mirror`&ldquo; the materials on any other server.</li>
</ul>; </ul>;
<p>This will let Radio Otherway to terminate upon violations of any of these restrictions. Upon termination, your <p>This will let Radio Otherway to terminate upon violations of any of these restrictions. Upon termination, your
viewing right will also be terminated and you should destroy any downloaded materials in your possession whether it viewing right will also be terminated and you should destroy any downloaded materials in your possession whether it
is printed or electronic format. These Terms of Service has been created with the help of the <a is printed or electronic format.
href="https://www.termsofservicegenerator.net">Terms Of Service Generator</a>.</p>; </p>;
<h2>3. Disclaimer</h2>; <h2>3. Disclaimer</h2>;
<p>All the materials on Radio Otherways Website are provided "as is". Radio Otherway makes no warranties, may it be <p>All the materials on Radio Otherways Website are provided `&ldquo;as is`&ldquo;. Radio Otherway makes no warranties, may it be
expressed or implied, therefore negates all other warranties. Furthermore, Radio Otherway does not make any expressed or implied, therefore negates all other warranties. Furthermore, Radio Otherway does not make any
representations concerning the accuracy or reliability of the use of the materials on its Website or otherwise representations concerning the accuracy or reliability of the use of the materials on its Website or otherwise
relating to such materials or any sites linked to this Website.</p>; relating to such materials or any sites linked to this Website.</p>;
@@ -71,7 +69,7 @@ const TosComponent = () => {
<h2>9. Governing Law</h2>; <h2>9. Governing Law</h2>;
<p>Any claim related to Radio Otherway's Website shall be governed by the laws of ie without regards to its conflict <p>Any claim related to Radio Otherway&apos;s Website shall be governed by the laws of ie without regards to its conflict
of law provisions.</p>;</> of law provisions.</p>;</>
); );
}; };

View File

@@ -5,13 +5,14 @@ import { Profile } from "@/models";
interface IAuthUserContext { interface IAuthUserContext {
loading: boolean; loading: boolean;
profile: Profile | undefined, profile: Profile | undefined,
logOut: () => Promise<boolean>; logOut: () => void
} }
const authUserContext = createContext<IAuthUserContext>({ const authUserContext = createContext<IAuthUserContext>({
loading: true, loading: true,
profile: undefined, profile: undefined,
logOut: () => Promise.resolve(false) logOut: () => {
}
}); });
export function AuthUserProvider({ children }: { children: React.ReactNode }) { export function AuthUserProvider({ children }: { children: React.ReactNode }) {

View File

@@ -1,60 +0,0 @@
const LoginFunctions = {
// PROVIDER_ID of auth provider, the credential object, and the email of the user
signInOrLink: async function(provider, credential, email) {
this.saveCredential(provider, credential);
await auth().signInWithCredential(credential).catch(
async (error) => {
try {
if (error.code != "auth/account-exists-with-different-credential") {
throw error;
}
let methods = await auth().fetchSignInMethodsForEmail(email);
let oldCred = await this.getCredential(methods[0]);
let prevUser = await auth().signInWithCredential(oldCred);
auth().currentUser.linkWithCredential(credential);
} catch (error) {
throw error;
}
}
);
},
getCredential: async function(provider) {
try {
let value = await AsyncStorage.getItem(provider);
if (value !== null) {
let [token, secret] = JSON.parse(value);
return this.getProvider(provider).credential(token, secret);
}
} catch (error) {
throw error;
}
},
saveCredential: async function(provider, credential) {
try {
let saveData = JSON.stringify([credential.token, credential.secret]);
await AsyncStorage.setItem(
provider,
saveData
);
} catch (error) {
throw error;
}
},
getProvider: function(providerId) {
switch (providerId) {
case auth.GoogleAuthProvider.PROVIDER_ID:
return auth.GoogleAuthProvider;
case auth.FacebookAuthProvider.PROVIDER_ID:
return auth.FacebookAuthProvider;
case auth.TwitterAuthProvider.PROVIDER_ID:
return auth.TwitterAuthProvider;
default:
throw new Error(`No provider implemented for ${providerId}`);
}
}
};
export default LoginFunctions;

View File

@@ -3,19 +3,18 @@ import {
createUserWithEmailAndPassword, createUserWithEmailAndPassword,
FacebookAuthProvider, FacebookAuthProvider,
getAuth, getAuth,
GoogleAuthProvider, linkWithPopup, GoogleAuthProvider, linkWithPopup, OAuthProvider,
onAuthStateChanged, onAuthStateChanged,
signInWithEmailAndPassword, signInWithEmailAndPassword,
signInWithPopup, signInWithRedirect, signInWithPopup, signInWithRedirect,
signOut, signOut,
TwitterAuthProvider TwitterAuthProvider, UserCredential
} from "firebase/auth"; } from "firebase/auth";
import { app } from "./firebase"; import { app } from "./firebase";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Profile } from "@/models"; import { Profile } from "@/models";
import { FirebaseAuth } from "@firebase/auth-types"; import { FirebaseAuth } from "@firebase/auth-types";
import firebase from "firebase/app"; import firebase from "firebase/app";
import LoginFunctions from "@/lib/auth/loginFunctions";
export default function useFirebaseAuth() { export default function useFirebaseAuth() {
@@ -54,7 +53,7 @@ export default function useFirebaseAuth() {
setLoading(true); setLoading(true);
}; };
const signIn = (email: string, password: string) => const signIn = (email: string, password: string): Promise<> =>
signInWithEmailAndPassword(auth, email, password); signInWithEmailAndPassword(auth, email, password);
const signUp = (email: string, password: string) => const signUp = (email: string, password: string) =>
@@ -63,45 +62,49 @@ export default function useFirebaseAuth() {
const logOut = () => const logOut = () =>
signOut(auth).then(clear); signOut(auth).then(clear);
const _processSignIn = async (provider: any) => { const _processSignIn = async (provider: any): Promise<UserCredential | undefined> => {
try { try {
try { const result = await signInWithPopup(auth, provider);
const result = await signInWithPopup(auth, provider); return result;
} catch (error: any) { } catch (err: any) {
if (error.code === "auth/account-exists-with-different-credential") { console.log("useFirebaseAuth", "_processSignIn", err);
linkWithPopup(result.currentUser, provider).then((result) => { if (err.code === "auth/account-exists-with-different-credential") {
// Accounts successfully linked. const credential = OAuthProvider.credentialFromError(err);
console.log("useFirebaseAuth", "_processSignIn_duplicateAccount", err);
const auth = getAuth();
if (auth?.currentUser) {
linkWithPopup(auth.currentUser, provider).then((result) => {
const credential = GoogleAuthProvider.credentialFromResult(result); const credential = GoogleAuthProvider.credentialFromResult(result);
return credential;
}).catch((error) => { }).catch((error) => {
console.log("useFirebaseAuth", "_processSignIn", "Failure in _processSignIn", err); console.log("useFirebaseAuth", "_processSignIn", "Failure in _processSignIn", err);
// Handle Errors here.
// ...
}); });
} }
} }
if (result.user) {
const profile = getUserProfile();
setProfile(profile);
router.push("/");
}
} catch (error: any) {
const errorCode = error.code;
const errorMessage = error.message;
const email = error.email;
const credential = GoogleAuthProvider.credentialFromError(error);
} }
}; };
const signInWithGoogle = async () => { const signInWithGoogle = async () => {
const provider = new GoogleAuthProvider(); const provider = new GoogleAuthProvider();
provider.setCustomParameters({ prompt: "select_account" }); provider.setCustomParameters({ prompt: "select_account" });
provider.addScope("https://www.googleapis.com/auth/userinfo.profile"); provider.addScope("https://www.googleapis.com/auth/userinfo.profile");
const credential = await signInWithPopup(auth, provider);
return LoginFunctions.signInOrLink(GoogleAuthProvider.PROVIDER_ID, credential, credential.user.email); const result = await _processSignIn(provider);
// await _processSignIn(provider); if (result) {
const credential = GoogleAuthProvider.credentialFromResult(result);
const profile = getUserProfile();
setProfile(profile);
router.push("/");
}
}; };
const signInWithTwitter = async () => { const signInWithTwitter = async () => {
const credential = new TwitterAuthProvider(); const provider = new TwitterAuthProvider();
return LoginFunctions.signInOrLink(GoogleAuthProvider.PROVIDER_ID, credential, credential.user.email); const result = await _processSignIn(provider);
if (result) {
const credential = TwitterAuthProvider.credentialFromResult(result);
const profile = getUserProfile();
setProfile(profile);
router.push("/");
}
}; };
const signInWithFacebook = async () => { const signInWithFacebook = async () => {
const provider = new FacebookAuthProvider(); const provider = new FacebookAuthProvider();

View File

@@ -19,10 +19,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}, { merge: true }); }, { merge: true });
} }
logger.debug("Stored show", res); logger.debug("Stored show", res);
res.status(200); res.status(200).json({ status: "OK" });
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
res.status(500); res.status(500).json({status: "Error"});
} }
res.end(); res.end();
}; };

View File

@@ -47,7 +47,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
} }
} }
} }
res.status(200); res.status(200).json({status: "OK"});
res.end(); res.end();
}; };
export default handler; export default handler;

View File

@@ -463,9 +463,9 @@
yargs "^16.2.0" yargs "^16.2.0"
"@headlessui/react@^1.7.11": "@headlessui/react@^1.7.11":
version "1.7.11" version "1.7.12"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.11.tgz#1cc5750226abe5af2c94f72e975c0c8d2f5cc5a6" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.12.tgz#9ab2baa3c4f632782631e00937f9531a34033619"
integrity sha512-EaDbVgcyiylhtskZZf4Qb/JiiByY7cYbd0qgZ9xm2pm2X7hKojG0P4TaQYKgPOV3vojPhd/pZyQh3nmRkkcSyw== integrity sha512-FhSx5V+Qp0GvbTpaxyS+ymGDDNntCacClWsk/d8Upbr19g3AsPbjfPk4+m2CgJGcuCB5Dz7LpUIOAbvQTyjL2g==
dependencies: dependencies:
client-only "^0.0.1" client-only "^0.0.1"
@@ -1568,9 +1568,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.4.284: electron-to-chromium@^1.4.284:
version "1.4.309" version "1.4.311"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.309.tgz#82158606da135ffffc16a2e1eb853fa0a10955e7" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz#953bc9a4767f5ce8ec125f9a1ad8e00e8f67e479"
integrity sha512-U7DTiKe4h+irqBG6h4EZ0XXaZuJj4md3xIXXaGSYhwiumPZ4BSc6rgf9UD0hVUMaeP/jB0q5pKWCPxvhO8fvZA== integrity sha512-RoDlZufvrtr2Nx3Yx5MB8jX3aHIxm8nRWPJm3yVvyHmyKaRvn90RjzB6hNnt0AkhS3IInJdyRfQb4mWhPvUjVw==
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"