Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup Guide in GUI #19

Merged
merged 5 commits into from
Mar 25, 2023
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ NEO4J_PASSWORD="password"
NEO4J_AUTH="${NEO4J_USER}/${NEO4J_PASSWORD}"
JWT_SECRET="unicourse-jwt-secret"
CLOUDFLARED_TOKEN=""
UNICOURSE_FIRST_INVITATION_CODE="first_code"
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,24 @@ A POC for Hybrid UniCourse Server.

## First Run

> You'll need to create the first invitation to invite yourself.

1. Open a terminal in VSCode.
2. Run `pnpm dev` to start the server in development mode.
3. Go to `http://localhost:3000/`, you should see there are 0 courses, 0 posts, and 1 user.
4. Go to `http://localhost:7474/`, which is the database management interface.
5. Login as `neo4j` with password `password`.
6. Run the following query to create an invitation:
3. Go to `http://localhost:5173/`, you should see there are 0 courses, 0 posts, and 1 user.
4. You should also see a panel with title "Welcome aboard", this panel only appears for your first account (specifically, if these only 1 user in DB).
5. Get started with entering usernames and roles, then you would be navigated to register page to complete the account information.
6. Voilà, your first account is ready!

```cypher
MATCH (x:User {username: "admin"}) MERGE (x)<-[:OWNED_BY]-(invitation:Invitation { code: "first_code", created: datetime(), revoked: false })
```
In case the above approach doesn't work, you can follow these instructions to create your account in DB.

7. Then, you can use the invitation code to register your account.
8. Next, you can give yourself roles `Verified`, `CoursePacker`, `Moderator` by running queries like:
1. Go to `http://localhost:7474/`, which is the database management interface.
2. Login as `neo4j` with password `password`.
3. In the browser view, click on the `*` node labels to see the data in graph and see the invitation code owned by admin.
4. Then, you can use the invitation code to register your account in the realm website.
5. Next, you can give yourself roles `Verified`, `CoursePacker`, `Moderator` by running queries like:

```cypher
MATCH (u:User {username: "your_username"}) SET u:Verified
```

9. Remember to re-login to refresh the token.
10. Now you can import some course packs and start using UniCourse Realm!
6. Remember to re-login to refresh the token.
7. Now you can import some course packs and start using UniCourse Realm!
JacobLinCool marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 12 additions & 0 deletions src/lib/actions/Auth.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script lang="ts">
import { invalidateAll } from "$app/navigation";
import type { RoleType } from "$lib/constants";
import { onMount } from "svelte";
import { t } from "svelte-i18n";
import { hash } from "unicourse";

Expand All @@ -12,12 +14,21 @@
let password_confirm = "";
let email = "";
let invitation = "";
let roles: RoleType[] = [];

$: {
console.log("switched to mode: " + mode);
err = "";
}

onMount(() => {
const params = new URLSearchParams(location.search);
if (params.has("username")) username = params.get("username")!;
if (params.has("roles")) roles = params.get("roles")!.split(",") as RoleType[];
if (params.has("code")) invitation = params.get("code")!;
if (roles || invitation) mode = "register";
});

async function login() {
if (username === "" || password === "") {
err = $t("auth.please-fill-in-username-and-password");
Expand Down Expand Up @@ -78,6 +89,7 @@
password: await hash(password),
email,
invitation,
roles,
}),
});

Expand Down
2 changes: 2 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export const Role = {
Moderator: "Moderator",
CoursePacker: "CoursePacker",
} as const;

export type RoleType = (typeof Role)[keyof typeof Role];
1 change: 1 addition & 0 deletions src/lib/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const NEO4J_URI = process.env.NEO4J_URI || "neo4j://db:7687";
export const NEO4J_USER = process.env.NEO4J_USER || "neo4j";
export const NEO4J_PASSWORD = process.env.NEO4J_PASSWORD || "password";
export const JWT_SECRET = process.env.JWT_SECRET || "unicourse-jwt-secret";
export const FIRST_INVITATION_CODE = process.env.UNICOURSE_FIRST_INVITATION_CODE || "first_code";
14 changes: 13 additions & 1 deletion src/lib/server/db.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { building } from "$app/environment";
import neo4j from "neo4j-driver";
import { DB } from "neo4j-ogm";
import { NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD } from "./config";
import { NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD, FIRST_INVITATION_CODE } from "./config";
import { constraint } from "./db-utils";

const driver = neo4j.driver(NEO4J_URI, neo4j.auth.basic(NEO4J_USER, NEO4J_PASSWORD));
Expand Down Expand Up @@ -53,5 +53,17 @@ export const ready = (async () => {
`,
);

await db.run(
`
MATCH (x:User {username: "admin"})
MERGE (x)<-[:OWNED_BY]-(invitation:Invitation {
code: $invitation_code
})
ON CREATE SET invitation.created = datetime(),
invitation.revoked = false
`,
{ invitation_code: FIRST_INVITATION_CODE },
);

console.timeEnd("Database Ready");
})();
2 changes: 2 additions & 0 deletions src/lib/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const en = {
invitation_invalid: "Invitation is invalid",
wrong_password: "Wrong password",
wrong_invitation: "Wrong invitation code",
wrong_roles: "Wrong roles",
register_with_roles: "You can't register with roles",
not_logged_in: "Not logged in",
permission_denied: "Permission denied",
},
Expand Down
3 changes: 2 additions & 1 deletion src/routes/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FIRST_INVITATION_CODE } from "$lib/server/config";
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ fetch }) => {
const response = await fetch("/api/stats");
const { data: stats } = await response.json();
return { stats };
return { stats, code: FIRST_INVITATION_CODE };
};
61 changes: 61 additions & 0 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { Role } from "$lib/constants";
import type { RoleType } from "$lib/constants";
import { t } from "svelte-i18n";
import { fly, fade } from "svelte/transition";
import type { PageData } from "./$types";
Expand All @@ -11,10 +14,68 @@
}

const start = 300;

const roleOptions: RoleType[] = [Role.Verified, Role.CoursePacker, Role.Moderator];
let username = "";
let roles: RoleType[] = [];
$: isComplete = username.length > 0 && roles.length > 0;
function getStarted() {
if (isComplete) {
goto(`/auth?username=${username}&roles=${roles.join(",")}&code=${data.code}`);
}
}
</script>

<section class="h-full pt-12">
<div class="flex h-full w-full flex-col justify-center">
{#if data.stats.users == 1}
<div in:fly={{ y: -40, delay: start + 2000, duration: 800 }} class="mb-20">
<h1 in:fade={{ duration: 300 }} class="mb-4 text-5xl font-bold">
Welcome aboard, developer!
</h1>
<h2
in:fade={{ delay: start + 2100, duration: 300 }}
class="mb-4 pl-2 font-medium text-primary/90"
>
Register your very first account and initiate UniCourse Realm.
</h2>
<div in:fade={{ delay: start + 2300, duration: 300 }} class="input-group">
<input
placeholder="Enter Username"
class="input bg-white/70 placeholder:text-sm placeholder:font-semibold placeholder:text-gray-500 focus:outline-none"
bind:value={username}
/>
<span class="bg-white/70 text-sm font-semibold lowercase text-primary">as</span>
<div class="dropdown-start dropdown-left dropdown">
<button
tabindex="0"
class="btn-ghost btn rounded-none bg-white/70 normal-case focus:outline-none"
class:text-gray-500={roles.length === 0}
>{roles.join(", ") || "Select Roles"}</button
>
<button tabindex="0" class="dropdown-content rounded-box w-48 bg-white p-4">
{#each roleOptions as role}
<label class="label cursor-pointer">
<span class="label-text bg-transparent">{role}</span>
<input
type="checkbox"
class="checkbox"
bind:group={roles}
value={role}
/>
</label>
{/each}
</button>
</div>
<button
class="btn-primary btn"
class:btn-disabled={!isComplete}
on:click={getStarted}>Get Started</button
>
</div>
</div>
{/if}

<div class="mb-10">
<h1 in:fly={{ y: 40, delay: start, duration: 500 }} class="text-4xl">UniCourse</h1>
<h1 in:fly={{ y: -20, delay: start + 500, duration: 600 }} class="text-8xl font-bold">
Expand Down
33 changes: 31 additions & 2 deletions src/routes/api/auth/register/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { v } from "@unicourse-tw/validation";
export const POST: RequestHandler = async ({ request, cookies }) => {
await ready;

let { username, password, email, invitation } = await request.json();
let { username, password, email, invitation, roles } = await request.json();

try {
username = v.username.parse(username);
Expand All @@ -40,11 +40,39 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
return json({ error: en.auth.invitation_invalid }, { status: 400 });
}

if (roles) {
try {
roles = z
.array(
z.union([
z.literal("Verified"),
z.literal("CoursePacker"),
z.literal("Moderator"),
]),
)
.parse(roles);
roles.unshift("User");
} catch {
return json({ error: en.auth.wrong_roles }, { status: 400 });
}
try {
const { records } = await db.run(
`MATCH (u:User) RETURN count(u) > 1 AS hasMoreThanOneUser`,
);
const hasMoreThanOneUser = records[0].get("hasMoreThanOneUser");
if (hasMoreThanOneUser) throw new Error();
} catch {
return json({ error: en.auth.register_with_roles }, { status: 403 });
}
} else {
roles = ["User"];
}

const payload = {
id: createId(),
username: username,
expires: Date.now() + 1000 * 60 * 60,
roles: ["User"],
roles,
};

const jwt = JWT.sign(payload, JWT_SECRET, {
Expand All @@ -62,6 +90,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
WITH user, owner
CREATE (user)-[:FOLLOWS]->(owner)
CREATE (token:Token $token)-[:OWNED_BY]->(user)
SET ${roles.map((x: string) => `user:${x}`).join(", ")}
RETURN user, owner.username as referrer
`,
{
Expand Down