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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package site.remlit.aster.common.model.request

import kotlinx.serialization.Serializable
import kotlin.js.JsExport

@JsExport
@Serializable
data class LoginRequest(
val username: String,
val password: String,
val totp: Int? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package site.remlit.aster.common.model.request

import kotlinx.serialization.Serializable
import kotlin.js.JsExport

@JsExport
@Serializable
data class LoginRequirementsRequest(
val username: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package site.remlit.aster.common.model.request

import kotlinx.serialization.Serializable
import kotlin.js.JsExport

@JsExport
@Serializable
data class TotpConfirmRequest(
val code: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package site.remlit.aster.common.model.response

import kotlinx.serialization.Serializable
import kotlin.js.JsExport

@JsExport
@Serializable
data class LoginRequirementsResponse(
val totp: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package site.remlit.aster.common.model.response

import kotlinx.serialization.Serializable
import kotlin.js.JsExport

@JsExport
@Serializable
data class RegisterTotpResponse(
val secret: String,
)
38 changes: 36 additions & 2 deletions common/src/jsMain/kotlin/site/remlit/aster/common/api/Api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import site.remlit.aster.common.model.User
import site.remlit.aster.common.model.Visibility
import site.remlit.aster.common.model.request.CreateNoteRequest
import site.remlit.aster.common.model.response.AuthResponse
import site.remlit.aster.common.model.response.LoginRequirementsResponse
import site.remlit.aster.common.model.response.RegisterTotpResponse
import site.remlit.aster.common.util.Https
import site.remlit.aster.common.util.toObject
import kotlin.js.Promise
Expand All @@ -32,16 +34,27 @@ class Api {
).unsafeCast<Promise<AuthResponse?>>()

@JsStatic
fun login(username: String, password: String) =
fun login(username: String, password: String, totp: Int? = null) =
Https.post(
"/api/login",
false,
mapOf(
"username" to username,
"password" to password
"password" to password,
"totp" to totp
).toObject()
).unsafeCast<Promise<AuthResponse?>>()

@JsStatic
fun getLoginRequirements(username: String) =
Https.post(
"/api/login/requirements",
false,
mapOf(
"username" to username,
).toObject()
).unsafeCast<Promise<LoginRequirementsResponse?>>()

@JsStatic
fun passwordReset(code: String, password: String) =
Https.post(
Expand All @@ -53,6 +66,27 @@ class Api {
).toObject()
)

@JsStatic
fun userRegisterTotp() =
Https.post(
"/api/user/totp/register",
true
).unsafeCast<Promise<RegisterTotpResponse?>>()

@JsStatic
fun userConfirmTotp() =
Https.post(
"/api/user/totp/confirm",
true
)

@JsStatic
fun userUnregisterTotp() =
Https.post(
"/api/user/totp/unregister",
true
)

@JsStatic
fun getTimeline(timeline: String, since: String? = null) =
Https.get("/api/timeline/$timeline${if (since != null) "?since=$since" else ""}", true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {Dispatch, SetStateAction} from "react";
import Modal from "./Modal.tsx";
import Container from "../Container.tsx";
import Button from "../Button.tsx";

function ConfirmationModal({title, body = "Are you sure you want to do this?", action, show, setShow}: {
title: string;
body: string;
action: () => void;
show: boolean;
setShow: Dispatch<SetStateAction<boolean>>;
}) {
return (
<Modal
title={title}
show={show}
setShow={setShow}
actions={<>
<Button primary onClick={() => {
setShow(false); action()
}}>Continue</Button>
<Button onClick={() => setShow(false)}>Cancel</Button>
</>}
>
<Container gap={"md"}>
<p>{body}</p>
</Container>
</Modal>
)
}

export default ConfirmationModal;
59 changes: 59 additions & 0 deletions frontend/packages/app/src/lib/components/modal/Modal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.modal {
--content-buffer: 25px;

&:open {
display: flex;
}

padding: 22px 16px;

flex-direction: column;
justify-content: center;
position: relative;

color: var(--tx-2);
background: var(--bg-2);
border-radius: var(--br-lg);
border: none;

box-shadow: var(--funky-effect),
0 10px 50px #00000050;

width: 100%;
max-width: 375px;

&::backdrop {
display: none;
background: transparent;
pointer-events: none;
}

h1 {
color: var(--tx-1);
font-size: var(--fs-xxl);

margin: 0 0 var(--content-buffer);
}

p { margin: 0; }

.actions {
display: flex;
align-items: center;
justify-content: end;
gap: 10px;

margin: var(--content-buffer) 0 0;
bottom: 0;
}
}

.modalBackdrop {
position: absolute;
top: 0;
left: 0;

width: 100vw;
height: 100vh;
background: #00000040;
}
42 changes: 42 additions & 0 deletions frontend/packages/app/src/lib/components/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {type Dispatch, type ReactNode, type SetStateAction, useEffect, useState} from "react";
import {createRef} from "preact";
import "./Modal.scss";
import Container from "../Container.tsx";

function Modal({title, children, show = false, setShow, actions = undefined}: {
title: string;
children: ReactNode;
show: boolean;
setShow: Dispatch<SetStateAction<boolean>>;
actions?: ReactNode;
}) {
const dialog = createRef<HTMLDialogElement>()

useEffect(() => {
if (show && !dialog.current?.open) dialog.current?.showModal()
if (!show && dialog.current?.open) dialog.current?.close()
}, [show])

return (
<>
<dialog
ref={dialog}
className={"modal"}
onClose={() => setShow(false)}
>
<h1>{title}</h1>
{children}
{actions ? (
<div className={"actions"}>
{actions}
</div>
) : null}
</dialog>
{show ? (
<div className={"modalBackdrop"} onClick={() => dialog.current?.close()}></div>
): null}
</>
)
}

export default Modal;
21 changes: 21 additions & 0 deletions frontend/packages/app/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Route as LoginRouteImport } from './routes/login'
import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
import { Route as FollowRequestsRouteImport } from './routes/follow-requests'
import { Route as DriveRouteImport } from './routes/drive'
import { Route as ComposeRouteImport } from './routes/compose'
import { Route as BookmarksRouteImport } from './routes/bookmarks'
import { Route as AboutRouteImport } from './routes/about'
import { Route as SplatRouteImport } from './routes/$'
Expand Down Expand Up @@ -71,6 +72,11 @@ const DriveRoute = DriveRouteImport.update({
path: '/drive',
getParentRoute: () => rootRouteImport,
} as any)
const ComposeRoute = ComposeRouteImport.update({
id: '/compose',
path: '/compose',
getParentRoute: () => rootRouteImport,
} as any)
const BookmarksRoute = BookmarksRouteImport.update({
id: '/bookmarks',
path: '/bookmarks',
Expand Down Expand Up @@ -112,6 +118,7 @@ export interface FileRoutesByFullPath {
'/$': typeof SplatRoute
'/about': typeof AboutRoute
'/bookmarks': typeof BookmarksRoute
'/compose': typeof ComposeRoute
'/drive': typeof DriveRoute
'/follow-requests': typeof FollowRequestsRoute
'/forgot-password': typeof ForgotPasswordRoute
Expand All @@ -130,6 +137,7 @@ export interface FileRoutesByTo {
'/$': typeof SplatRoute
'/about': typeof AboutRoute
'/bookmarks': typeof BookmarksRoute
'/compose': typeof ComposeRoute
'/drive': typeof DriveRoute
'/follow-requests': typeof FollowRequestsRoute
'/forgot-password': typeof ForgotPasswordRoute
Expand All @@ -149,6 +157,7 @@ export interface FileRoutesById {
'/$': typeof SplatRoute
'/about': typeof AboutRoute
'/bookmarks': typeof BookmarksRoute
'/compose': typeof ComposeRoute
'/drive': typeof DriveRoute
'/follow-requests': typeof FollowRequestsRoute
'/forgot-password': typeof ForgotPasswordRoute
Expand All @@ -169,6 +178,7 @@ export interface FileRouteTypes {
| '/$'
| '/about'
| '/bookmarks'
| '/compose'
| '/drive'
| '/follow-requests'
| '/forgot-password'
Expand All @@ -187,6 +197,7 @@ export interface FileRouteTypes {
| '/$'
| '/about'
| '/bookmarks'
| '/compose'
| '/drive'
| '/follow-requests'
| '/forgot-password'
Expand All @@ -205,6 +216,7 @@ export interface FileRouteTypes {
| '/$'
| '/about'
| '/bookmarks'
| '/compose'
| '/drive'
| '/follow-requests'
| '/forgot-password'
Expand All @@ -224,6 +236,7 @@ export interface RootRouteChildren {
SplatRoute: typeof SplatRoute
AboutRoute: typeof AboutRoute
BookmarksRoute: typeof BookmarksRoute
ComposeRoute: typeof ComposeRoute
DriveRoute: typeof DriveRoute
FollowRequestsRoute: typeof FollowRequestsRoute
ForgotPasswordRoute: typeof ForgotPasswordRoute
Expand Down Expand Up @@ -303,6 +316,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DriveRouteImport
parentRoute: typeof rootRouteImport
}
'/compose': {
id: '/compose'
path: '/compose'
fullPath: '/compose'
preLoaderRoute: typeof ComposeRouteImport
parentRoute: typeof rootRouteImport
}
'/bookmarks': {
id: '/bookmarks'
path: '/bookmarks'
Expand Down Expand Up @@ -360,6 +380,7 @@ const rootRouteChildren: RootRouteChildren = {
SplatRoute: SplatRoute,
AboutRoute: AboutRoute,
BookmarksRoute: BookmarksRoute,
ComposeRoute: ComposeRoute,
DriveRoute: DriveRoute,
FollowRequestsRoute: FollowRequestsRoute,
ForgotPasswordRoute: ForgotPasswordRoute,
Expand Down
23 changes: 23 additions & 0 deletions frontend/packages/app/src/routes/compose.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createFileRoute } from '@tanstack/react-router'
import PageHeader from "../lib/components/PageHeader.tsx";
import {IconPencil} from "@tabler/icons-react";
import PageWrapper from "../lib/components/PageWrapper.tsx";
import Compose from "../lib/components/Compose.tsx";

export const Route = createFileRoute('/compose')({
component: RouteComponent,
})

function RouteComponent() {
return (
<>
<PageHeader
icon={<IconPencil size={18}/>}
title={"Compose"}
/>
<PageWrapper padding={"full"}>
<Compose />
</PageWrapper>
</>
)
}
Loading