Added users page and user viewing
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
// Do not edit this file manually
|
// Do not edit this file manually
|
||||||
|
|
||||||
|
|
||||||
import type { AshRpcError, ConditionalPaginatedResultMixed, InferResult, SortString, UUID, UnifiedFieldSelection, ValidationResult, mediaFilterInput, mediaResourceSchema, mediaSortField, tweetsFilterInput, tweetsResourceSchema, tweetsSortField } from "./ash_types";
|
import type { AshRpcError, ConditionalPaginatedResultMixed, InferResult, SortString, UUID, UnifiedFieldSelection, ValidationResult, mediaFilterInput, mediaResourceSchema, mediaSortField, tweetsFilterInput, tweetsResourceSchema, tweetsSortField, usersFilterInput, usersResourceSchema, usersSortField } from "./ash_types";
|
||||||
export type * from "./ash_types";
|
export type * from "./ash_types";
|
||||||
|
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
@@ -201,6 +201,107 @@ export async function executeValidationRpcRequest<T>(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type ReadUserFields = UnifiedFieldSelection<usersResourceSchema>[];
|
||||||
|
|
||||||
|
|
||||||
|
export type InferReadUserResult<
|
||||||
|
Fields extends ReadUserFields | undefined,
|
||||||
|
Page extends ReadUserConfig["page"] = undefined
|
||||||
|
> = ConditionalPaginatedResultMixed<Page, Array<InferResult<usersResourceSchema, Fields>>, {
|
||||||
|
results: Array<InferResult<usersResourceSchema, Fields>>;
|
||||||
|
hasMore: boolean;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
count?: number | null;
|
||||||
|
type: "offset";
|
||||||
|
}, {
|
||||||
|
results: Array<InferResult<usersResourceSchema, Fields>>;
|
||||||
|
hasMore: boolean;
|
||||||
|
limit: number;
|
||||||
|
after: string | null;
|
||||||
|
before: string | null;
|
||||||
|
previousPage: string;
|
||||||
|
nextPage: string;
|
||||||
|
count?: number | null;
|
||||||
|
type: "keyset";
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type ReadUserConfig = {
|
||||||
|
tenant?: string;
|
||||||
|
fields: ReadUserFields;
|
||||||
|
filter?: usersFilterInput;
|
||||||
|
sort?: SortString<usersSortField> | SortString<usersSortField>[];
|
||||||
|
page?: (
|
||||||
|
{
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
count?: boolean;
|
||||||
|
} | {
|
||||||
|
limit?: number;
|
||||||
|
after?: string;
|
||||||
|
before?: string;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
fetchOptions?: RequestInit;
|
||||||
|
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReadUserResult<Fields extends ReadUserFields, Page extends ReadUserConfig["page"] = undefined> = | { success: true; data: InferReadUserResult<Fields, Page>; }
|
||||||
|
| { success: false; errors: AshRpcError[]; }
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read User records
|
||||||
|
*
|
||||||
|
* @ashActionType :read
|
||||||
|
*/
|
||||||
|
export async function readUser<Fields extends ReadUserFields, Config extends ReadUserConfig = ReadUserConfig>(
|
||||||
|
config: Config & { fields: Fields }
|
||||||
|
): Promise<ReadUserResult<Fields, Config["page"]>> {
|
||||||
|
const payload = {
|
||||||
|
action: "read_user",
|
||||||
|
...(config.tenant !== undefined && { tenant: config.tenant }),
|
||||||
|
...(config.fields !== undefined && { fields: config.fields }),
|
||||||
|
...(config.filter && { filter: config.filter }),
|
||||||
|
...(config.sort && { sort: Array.isArray(config.sort) ? config.sort.join(",") : config.sort }),
|
||||||
|
...(config.page && { page: config.page })
|
||||||
|
};
|
||||||
|
|
||||||
|
return executeActionRpcRequest<ReadUserResult<Fields, Config["page"]>>(
|
||||||
|
payload,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate: Read User records
|
||||||
|
*
|
||||||
|
* @ashActionType :read
|
||||||
|
* @validation true
|
||||||
|
*/
|
||||||
|
export async function validateReadUser(
|
||||||
|
config: {
|
||||||
|
tenant?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
fetchOptions?: RequestInit;
|
||||||
|
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||||
|
}
|
||||||
|
): Promise<ValidationResult> {
|
||||||
|
const payload = {
|
||||||
|
action: "read_user",
|
||||||
|
...(config.tenant !== undefined && { tenant: config.tenant })
|
||||||
|
};
|
||||||
|
|
||||||
|
return executeValidationRpcRequest<ValidationResult>(
|
||||||
|
payload,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type ReadMediaFields = UnifiedFieldSelection<mediaResourceSchema>[];
|
export type ReadMediaFields = UnifiedFieldSelection<mediaResourceSchema>[];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,24 @@
|
|||||||
export type UUID = string;
|
export type UUID = string;
|
||||||
export type UtcDateTimeUsec = string;
|
export type UtcDateTimeUsec = string;
|
||||||
|
|
||||||
|
// users Schema
|
||||||
|
export type usersResourceSchema = {
|
||||||
|
__type: "Resource";
|
||||||
|
__primitiveFields: "id" | "email";
|
||||||
|
id: UUID;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type usersAttributesOnlySchema = {
|
||||||
|
__type: "Resource";
|
||||||
|
__primitiveFields: "id" | "email";
|
||||||
|
id: UUID;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// media Schema
|
// media Schema
|
||||||
export type mediaResourceSchema = {
|
export type mediaResourceSchema = {
|
||||||
__type: "Resource";
|
__type: "Resource";
|
||||||
@@ -14,6 +32,7 @@ export type mediaResourceSchema = {
|
|||||||
s3Key: string;
|
s3Key: string;
|
||||||
userId: UUID;
|
userId: UUID;
|
||||||
tweetId: UUID | null;
|
tweetId: UUID | null;
|
||||||
|
user: { __type: "Relationship"; __resource: usersResourceSchema; };
|
||||||
tweet: { __type: "Relationship"; __resource: tweetsResourceSchema | null; };
|
tweet: { __type: "Relationship"; __resource: tweetsResourceSchema | null; };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,6 +60,7 @@ export type tweetsResourceSchema = {
|
|||||||
state: "posted" | "drafted";
|
state: "posted" | "drafted";
|
||||||
likedByMe: boolean;
|
likedByMe: boolean;
|
||||||
userEmail: string | null;
|
userEmail: string | null;
|
||||||
|
user: { __type: "Relationship"; __resource: usersResourceSchema; };
|
||||||
media: { __type: "Relationship"; __array: true; __resource: mediaResourceSchema; };
|
media: { __type: "Relationship"; __array: true; __resource: mediaResourceSchema; };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,6 +78,26 @@ export type tweetsAttributesOnlySchema = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type usersFilterInput = {
|
||||||
|
and?: Array<usersFilterInput>;
|
||||||
|
or?: Array<usersFilterInput>;
|
||||||
|
not?: Array<usersFilterInput>;
|
||||||
|
|
||||||
|
id?: {
|
||||||
|
eq?: UUID;
|
||||||
|
notEq?: UUID;
|
||||||
|
in?: Array<UUID>;
|
||||||
|
};
|
||||||
|
|
||||||
|
email?: {
|
||||||
|
eq?: string;
|
||||||
|
notEq?: string;
|
||||||
|
in?: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
export type mediaFilterInput = {
|
export type mediaFilterInput = {
|
||||||
and?: Array<mediaFilterInput>;
|
and?: Array<mediaFilterInput>;
|
||||||
or?: Array<mediaFilterInput>;
|
or?: Array<mediaFilterInput>;
|
||||||
@@ -89,6 +129,8 @@ export type mediaFilterInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
user?: usersFilterInput;
|
||||||
|
|
||||||
tweet?: tweetsFilterInput;
|
tweet?: tweetsFilterInput;
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -154,11 +196,16 @@ export type tweetsFilterInput = {
|
|||||||
isNil?: boolean;
|
isNil?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
user?: usersFilterInput;
|
||||||
|
|
||||||
media?: mediaFilterInput;
|
media?: mediaFilterInput;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const usersFilterFields = ["id", "email"] as const;
|
||||||
|
export type usersFilterField = (typeof usersFilterFields)[number];
|
||||||
|
|
||||||
export const mediaFilterFields = ["id", "s3Key", "userId", "tweetId", "user", "tweet"] as const;
|
export const mediaFilterFields = ["id", "s3Key", "userId", "tweetId", "user", "tweet"] as const;
|
||||||
export type mediaFilterField = (typeof mediaFilterFields)[number];
|
export type mediaFilterField = (typeof mediaFilterFields)[number];
|
||||||
|
|
||||||
@@ -166,6 +213,9 @@ export const tweetsFilterFields = ["id", "content", "likes", "userId", "inserted
|
|||||||
export type tweetsFilterField = (typeof tweetsFilterFields)[number];
|
export type tweetsFilterField = (typeof tweetsFilterFields)[number];
|
||||||
|
|
||||||
|
|
||||||
|
export const usersSortFields = ["id", "email"] as const;
|
||||||
|
export type usersSortField = (typeof usersSortFields)[number];
|
||||||
|
|
||||||
export const mediaSortFields = ["id", "s3Key", "userId", "tweetId"] as const;
|
export const mediaSortFields = ["id", "s3Key", "userId", "tweetId"] as const;
|
||||||
export type mediaSortField = (typeof mediaSortFields)[number];
|
export type mediaSortField = (typeof mediaSortFields)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
likeTweet,
|
likeTweet,
|
||||||
unlikeTweet,
|
unlikeTweet,
|
||||||
updateTweet,
|
updateTweet,
|
||||||
|
readUser,
|
||||||
buildCSRFHeaders,
|
buildCSRFHeaders,
|
||||||
} from "./ash_rpc";
|
} from "./ash_rpc";
|
||||||
import { uploadFile } from "./upload";
|
import { uploadFile } from "./upload";
|
||||||
@@ -25,6 +26,7 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type User = { id: string; email: string };
|
||||||
type MediaItem = { id: string; s3Key: string };
|
type MediaItem = { id: string; s3Key: string };
|
||||||
type Tweet = {
|
type Tweet = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -772,54 +774,145 @@ function RefreshButton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UserCard({ user }: { user: User }) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className="mx-tweet"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={() => { window.location.href = `/users/${user.id}`; }}
|
||||||
|
>
|
||||||
|
<div className="mx-tweet-avatar">
|
||||||
|
<span>M</span>
|
||||||
|
</div>
|
||||||
|
<div className="mx-tweet-body">
|
||||||
|
<div className="mx-tweet-header">
|
||||||
|
<span className="mx-tweet-handle">{user.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserList() {
|
||||||
|
const { data, isLoading, isError, error } = useQuery({
|
||||||
|
queryKey: ["users"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await readUser({
|
||||||
|
fields: ["id", "email"],
|
||||||
|
headers: buildCSRFHeaders(),
|
||||||
|
});
|
||||||
|
if (!res.success) throw new Error("Failed to load users");
|
||||||
|
const users = Array.isArray(res.data) ? res.data : (res.data as any)?.results ?? [];
|
||||||
|
return users as User[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <Spinner />;
|
||||||
|
if (isError) return <ErrorBanner message={(error as Error)?.message ?? "Could not load users"} />;
|
||||||
|
|
||||||
|
const users = data ?? [];
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="mx-empty">
|
||||||
|
<div className="mx-empty-icon">◎</div>
|
||||||
|
<p className="mx-empty-title">No users yet</p>
|
||||||
|
<p className="mx-empty-sub">Be the first to sign up.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-feed">
|
||||||
|
{users.map((u) => (
|
||||||
|
<UserCard key={u.id} user={u} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserDetail({ userId }: { userId: string }) {
|
||||||
|
const { data: user, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["user", userId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await readUser({
|
||||||
|
fields: ["id", "email"],
|
||||||
|
filter: { id: { eq: userId } },
|
||||||
|
headers: buildCSRFHeaders(),
|
||||||
|
});
|
||||||
|
if (!res.success) throw new Error("Failed to load user");
|
||||||
|
const results = Array.isArray(res.data) ? res.data : (res.data as any)?.results ?? [];
|
||||||
|
return (results[0] as User) ?? null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <Spinner />;
|
||||||
|
if (isError || !user) return <ErrorBanner message="Could not load user" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-detail">
|
||||||
|
<div className="mx-detail-header">
|
||||||
|
<a href="/users" className="mx-back-btn">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
||||||
|
</svg>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="mx-detail-body">
|
||||||
|
<div className="mx-detail-author">
|
||||||
|
<div className="mx-tweet-avatar">
|
||||||
|
<span>M</span>
|
||||||
|
</div>
|
||||||
|
<span className="mx-tweet-handle">{user.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const appEl = document.getElementById("app")!;
|
const appEl = document.getElementById("app")!;
|
||||||
const email = appEl.dataset.currentUserEmail ?? "";
|
const email = appEl.dataset.currentUserEmail ?? "";
|
||||||
const userId = appEl.dataset.currentUserId ?? "";
|
const userId = appEl.dataset.currentUserId ?? "";
|
||||||
const tweetId = appEl.dataset.tweetId || null;
|
const tweetId = appEl.dataset.tweetId || null;
|
||||||
|
const page = appEl.dataset.page ?? "feed";
|
||||||
|
const profileUserId = appEl.dataset.userId || null;
|
||||||
|
|
||||||
|
const onFeedPage = page === "feed" || page === "tweet";
|
||||||
|
const onUsersPage = page === "users" || page === "user-detail";
|
||||||
|
|
||||||
|
function renderMain() {
|
||||||
|
switch (page) {
|
||||||
|
case "tweet":
|
||||||
return (
|
return (
|
||||||
<AuthCtx.Provider value={{ email, userId }}>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<div className="mx-root">
|
|
||||||
<aside className="mx-sidebar">
|
|
||||||
<div className="mx-logo">
|
|
||||||
<span className="mx-logo-icon">⬡</span>
|
|
||||||
<span className="mx-logo-text">Mixer</span>
|
|
||||||
</div>
|
|
||||||
<nav className="mx-nav">
|
|
||||||
<a className="mx-nav-item mx-nav-active" href="/feed">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
|
|
||||||
</svg>
|
|
||||||
Feed
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
<div className="mx-sidebar-footer">
|
|
||||||
{email ? (
|
|
||||||
<>
|
|
||||||
<span className="mx-version" style={{ color: "var(--mx-fg2)" }}>{email}</span>
|
|
||||||
<a className="mx-auth-link" href="/auth/sign-out">Sign out</a>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<a className="mx-auth-link" href="/register">Create account</a>
|
|
||||||
<a className="mx-auth-link" href="/auth/sign-in">Sign in</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span className="mx-version">v0.1.0</span>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main className="mx-main">
|
|
||||||
{tweetId ? (
|
|
||||||
<>
|
<>
|
||||||
<header className="mx-header">
|
<header className="mx-header">
|
||||||
<h1 className="mx-header-title">Tweet</h1>
|
<h1 className="mx-header-title">Tweet</h1>
|
||||||
</header>
|
</header>
|
||||||
<TweetDetail tweetId={tweetId} />
|
<TweetDetail tweetId={tweetId!} />
|
||||||
</>
|
</>
|
||||||
) : (
|
);
|
||||||
|
case "users":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="mx-header">
|
||||||
|
<h1 className="mx-header-title">Users</h1>
|
||||||
|
</header>
|
||||||
|
<UserList />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case "user-detail":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="mx-header">
|
||||||
|
<h1 className="mx-header-title">Profile</h1>
|
||||||
|
</header>
|
||||||
|
<UserDetail userId={profileUserId!} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="mx-header">
|
<header className="mx-header">
|
||||||
<h1 className="mx-header-title">Feed</h1>
|
<h1 className="mx-header-title">Feed</h1>
|
||||||
@@ -841,7 +934,51 @@ function App() {
|
|||||||
|
|
||||||
<Feed />
|
<Feed />
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthCtx.Provider value={{ email, userId }}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<div className="mx-root">
|
||||||
|
<aside className="mx-sidebar">
|
||||||
|
<div className="mx-logo">
|
||||||
|
<span className="mx-logo-icon">⬡</span>
|
||||||
|
<span className="mx-logo-text">Mixer</span>
|
||||||
|
</div>
|
||||||
|
<nav className="mx-nav">
|
||||||
|
<a className={`mx-nav-item${onFeedPage ? " mx-nav-active" : ""}`} href="/feed">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
|
||||||
|
</svg>
|
||||||
|
Feed
|
||||||
|
</a>
|
||||||
|
<a className={`mx-nav-item${onUsersPage ? " mx-nav-active" : ""}`} href="/users">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z" />
|
||||||
|
</svg>
|
||||||
|
Users
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div className="mx-sidebar-footer">
|
||||||
|
{email ? (
|
||||||
|
<>
|
||||||
|
<span className="mx-version" style={{ color: "var(--mx-fg2)" }}>{email}</span>
|
||||||
|
<a className="mx-auth-link" href="/auth/sign-out">Sign out</a>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<a className="mx-auth-link" href="/register">Create account</a>
|
||||||
|
<a className="mx-auth-link" href="/auth/sign-in">Sign in</a>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
<span className="mx-version">v0.1.0</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="mx-main">
|
||||||
|
{renderMain()}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div className="mx-rightbar">
|
<div className="mx-rightbar">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
defmodule Mixer.Accounts do
|
defmodule Mixer.Accounts do
|
||||||
use Ash.Domain, otp_app: :mixer, extensions: [AshAdmin.Domain]
|
use Ash.Domain, otp_app: :mixer, extensions: [AshTypescript.Rpc, AshAdmin.Domain]
|
||||||
|
|
||||||
admin do
|
admin do
|
||||||
show? true
|
show? true
|
||||||
@@ -10,4 +10,10 @@ defmodule Mixer.Accounts do
|
|||||||
resource Mixer.Accounts.User
|
resource Mixer.Accounts.User
|
||||||
resource Mixer.Accounts.ApiKey
|
resource Mixer.Accounts.ApiKey
|
||||||
end
|
end
|
||||||
|
|
||||||
|
typescript_rpc do
|
||||||
|
resource Mixer.Accounts.User do
|
||||||
|
rpc_action :read_user, :read
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ defmodule Mixer.Accounts.User do
|
|||||||
domain: Mixer.Accounts,
|
domain: Mixer.Accounts,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
authorizers: [Ash.Policy.Authorizer],
|
authorizers: [Ash.Policy.Authorizer],
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication, AshTypescript.Resource]
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
add_ons do
|
add_ons do
|
||||||
@@ -66,6 +66,10 @@ defmodule Mixer.Accounts.User do
|
|||||||
repo Mixer.Repo
|
repo Mixer.Repo
|
||||||
end
|
end
|
||||||
|
|
||||||
|
typescript do
|
||||||
|
type_name "users"
|
||||||
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:read]
|
defaults [:read]
|
||||||
|
|
||||||
@@ -282,6 +286,10 @@ defmodule Mixer.Accounts.User do
|
|||||||
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
|
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
|
||||||
authorize_if always()
|
authorize_if always()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
policy action_type(:read) do
|
||||||
|
authorize_if always()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
|
|||||||
@@ -6,14 +6,22 @@ defmodule MixerWeb.PageController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
render_spa(conn, nil)
|
render_spa(conn, %{page: "feed", tweet_id: nil, user_id: nil})
|
||||||
end
|
end
|
||||||
|
|
||||||
def show(conn, %{"tweet_id" => tweet_id}) do
|
def show(conn, %{"tweet_id" => tweet_id}) do
|
||||||
render_spa(conn, tweet_id)
|
render_spa(conn, %{page: "tweet", tweet_id: tweet_id, user_id: nil})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_spa(conn, tweet_id) do
|
def users_index(conn, _params) do
|
||||||
|
render_spa(conn, %{page: "users", tweet_id: nil, user_id: nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_show(conn, %{"user_id" => user_id}) do
|
||||||
|
render_spa(conn, %{page: "user-detail", tweet_id: nil, user_id: user_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_spa(conn, %{page: page, tweet_id: tweet_id, user_id: user_id}) do
|
||||||
asset_host = Application.get_env(:waffle, :asset_host, "http://localhost:3900")
|
asset_host = Application.get_env(:waffle, :asset_host, "http://localhost:3900")
|
||||||
bucket = Application.get_env(:waffle, :bucket, "mixer-bucket")
|
bucket = Application.get_env(:waffle, :bucket, "mixer-bucket")
|
||||||
|
|
||||||
@@ -22,7 +30,9 @@ defmodule MixerWeb.PageController do
|
|||||||
|> render(:index,
|
|> render(:index,
|
||||||
current_user: conn.assigns[:current_user],
|
current_user: conn.assigns[:current_user],
|
||||||
media_host: "#{asset_host}/#{bucket}",
|
media_host: "#{asset_host}/#{bucket}",
|
||||||
tweet_id: tweet_id
|
page: page,
|
||||||
|
tweet_id: tweet_id,
|
||||||
|
user_id: user_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
data-current-user-id={if @current_user, do: @current_user.id, else: ""}
|
data-current-user-id={if @current_user, do: @current_user.id, else: ""}
|
||||||
data-current-user-email={if @current_user, do: @current_user.email, else: ""}
|
data-current-user-email={if @current_user, do: @current_user.email, else: ""}
|
||||||
data-asset-host={@media_host}
|
data-asset-host={@media_host}
|
||||||
data-tweet-id={@tweet_id || ""}>
|
data-page={@page}
|
||||||
|
data-tweet-id={@tweet_id || ""}
|
||||||
|
data-user-id={@user_id || ""}>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ defmodule MixerWeb.Router do
|
|||||||
get "/", PageController, :home
|
get "/", PageController, :home
|
||||||
get "/feed", PageController, :index
|
get "/feed", PageController, :index
|
||||||
get "/feed/:tweet_id", PageController, :show
|
get "/feed/:tweet_id", PageController, :show
|
||||||
|
get "/users", PageController, :users_index
|
||||||
|
get "/users/:user_id", PageController, :user_show
|
||||||
post "/rpc/run", AshTypescriptRpcController, :run
|
post "/rpc/run", AshTypescriptRpcController, :run
|
||||||
post "/rpc/validate", AshTypescriptRpcController, :validate
|
post "/rpc/validate", AshTypescriptRpcController, :validate
|
||||||
post "/upload", UploadController, :create
|
post "/upload", UploadController, :create
|
||||||
|
|||||||
Reference in New Issue
Block a user